Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions python/valuecell/server/api/routers/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
StrategyHoldingFlatResponse,
StrategyListData,
StrategyListResponse,
StrategyPerformanceResponse,
StrategyPortfolioSummaryResponse,
StrategyStatusSuccessResponse,
StrategyStatusUpdateResponse,
Expand Down Expand Up @@ -198,6 +199,37 @@ def normalize_strategy_type(
status_code=500, detail=f"Failed to retrieve strategy list: {str(e)}"
)

@router.get(
"/performance",
response_model=StrategyPerformanceResponse,
summary="Get strategy performance and configuration overview",
description=(
"Return ROI, model/provider, and final prompt strictly from templates (no fallback)."
),
)
async def get_strategy_performance(
id: str = Query(..., description="Strategy ID"),
) -> StrategyPerformanceResponse:
try:
data = await StrategyService.get_strategy_performance(id)
if not data:
return SuccessResponse.create(
data=None,
msg="Strategy not found or no performance data",
)

return SuccessResponse.create(
data=data,
msg="Successfully retrieved strategy performance and configuration",
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to retrieve strategy performance: {str(e)}",
)

@router.get(
"/holding",
response_model=StrategyHoldingFlatResponse,
Expand Down
31 changes: 31 additions & 0 deletions python/valuecell/server/api/schemas/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,34 @@ class PromptCreateRequest(BaseModel):

PromptListResponse = SuccessResponse[list[PromptItem]]
PromptCreateResponse = SuccessResponse[PromptItem]


class StrategyPerformanceData(BaseModel):
"""Performance overview for a strategy including ROI and config."""

strategy_id: str = Field(..., description="Strategy identifier")
initial_capital: Optional[float] = Field(
None, description="Initial capital used by the strategy"
)
return_rate_pct: Optional[float] = Field(
None, description="Return rate percentage relative to initial capital"
)
# Flattened config fields (only the requested subset)
llm_provider: Optional[str] = Field(
None, description="Model provider (e.g., openrouter, google, openai)"
)
llm_model_id: Optional[str] = Field(
None, description="Model identifier (e.g., deepseek-ai/deepseek-v3.1)"
)
exchange_id: Optional[str] = Field(None, description="Exchange identifier")
strategy_type: Optional[StrategyType] = Field(
None, description="Strategy type (PromptBasedStrategy/GridStrategy)"
)
max_leverage: Optional[float] = Field(None, description="Maximum leverage")
symbols: Optional[List[str]] = Field(None, description="Symbols universe")
prompt: Optional[str] = Field(
None, description="Final resolved prompt text used by the strategy"
)


StrategyPerformanceResponse = SuccessResponse[StrategyPerformanceData]
118 changes: 118 additions & 0 deletions python/valuecell/server/services/strategy_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
StrategyActionCard,
StrategyCycleDetail,
StrategyHoldingData,
StrategyPerformanceData,
StrategyPortfolioSummaryData,
StrategyType,
)
from valuecell.server.db.repositories import get_strategy_repository

Expand Down Expand Up @@ -130,6 +132,122 @@ def _combine_realized_unrealized(snapshot) -> Optional[float]:
return None
return (realized or 0.0) + (unrealized or 0.0)

@staticmethod
def _normalize_strategy_type(meta: dict, cfg: dict) -> Optional[StrategyType]:
try:
from valuecell.server.api.schemas.strategy import StrategyType as ST
except Exception:
return None

val = meta.get("strategy_type")
if not val:
val = (cfg.get("trading_config", {}) or {}).get("strategy_type")
if val is None:
agent_name = str(meta.get("agent_name") or "").lower()
if "prompt" in agent_name:
return ST.PROMPT
if "grid" in agent_name:
return ST.GRID
return None

raw = str(val).strip().lower()
if raw.startswith("strategytype."):
raw = raw.split(".", 1)[1]
raw_compact = "".join(ch for ch in raw if ch.isalnum())

if raw in ("prompt based strategy", "grid strategy"):
return ST.PROMPT if raw.startswith("prompt") else ST.GRID
if raw_compact in ("promptbasedstrategy", "gridstrategy"):
return ST.PROMPT if raw_compact.startswith("prompt") else ST.GRID
if raw in ("prompt", "grid"):
return ST.PROMPT if raw == "prompt" else ST.GRID

agent_name = str(meta.get("agent_name") or "").lower()
if "prompt" in agent_name:
return ST.PROMPT
if "grid" in agent_name:
return ST.GRID
return None

@staticmethod
async def get_strategy_performance(
strategy_id: str,
) -> Optional[StrategyPerformanceData]:
repo = get_strategy_repository()
strategy = repo.get_strategy_by_strategy_id(strategy_id)
if not strategy:
return None

snapshot = repo.get_latest_portfolio_snapshot(strategy_id)
# Reference timestamp no longer included in performance response

# Extract flattened config fields from original config/meta
cfg = strategy.config or {}
meta = strategy.strategy_metadata or {}

llm = cfg.get("llm_model_config") or {}
ex = cfg.get("exchange_config") or {}
tr = cfg.get("trading_config") or {}

llm_provider = (
llm.get("provider") or meta.get("provider") or meta.get("llm_provider")
)
llm_model_id = (
llm.get("model_id") or meta.get("model_id") or meta.get("llm_model_id")
)
exchange_id = ex.get("exchange_id") or meta.get("exchange_id")
strategy_type = StrategyService._normalize_strategy_type(meta, cfg)
initial_capital = _to_optional_float(tr.get("initial_capital"))
max_leverage = _to_optional_float(tr.get("max_leverage"))
symbols = tr.get("symbols") if tr.get("symbols") is not None else None
# Resolve final prompt strictly via template_id from strategy_prompts (no fallback)
template_id = (
tr.get("template_id") if tr.get("template_id") is not None else None
)
final_prompt: Optional[str] = None
if template_id:
try:
prompt_item = repo.get_prompt_by_id(template_id)
if prompt_item and getattr(prompt_item, "content", None):
final_prompt = prompt_item.content
except Exception:
# Strict mode: do not fallback; leave final_prompt as None
final_prompt = None

total_value = (
_to_optional_float(getattr(snapshot, "total_value", None))
if snapshot
else None
)
pnl_value = (
StrategyService._combine_realized_unrealized(snapshot) if snapshot else None
)

return_rate_pct: Optional[float] = None
try:
if initial_capital and initial_capital > 0:
if total_value is not None:
return_rate_pct = (
(total_value - initial_capital) / initial_capital
) * 100.0
elif pnl_value is not None:
return_rate_pct = (pnl_value / initial_capital) * 100.0
except Exception:
return_rate_pct = None

return StrategyPerformanceData(
strategy_id=strategy_id,
initial_capital=initial_capital,
return_rate_pct=return_rate_pct,
llm_provider=llm_provider,
llm_model_id=llm_model_id,
exchange_id=exchange_id,
strategy_type=strategy_type,
max_leverage=max_leverage,
symbols=symbols,
prompt=final_prompt,
)

@staticmethod
async def get_strategy_detail(
strategy_id: str,
Expand Down