From 6258a432f23b028b16c61cbb89e855694403e4eb Mon Sep 17 00:00:00 2001 From: paisley Date: Thu, 27 Nov 2025 17:40:39 +0800 Subject: [PATCH 1/2] feat: add get strategy details --- .../valuecell/server/api/routers/strategy.py | 32 +++++ .../valuecell/server/api/schemas/strategy.py | 31 +++++ .../server/services/strategy_service.py | 124 ++++++++++++++++++ 3 files changed, 187 insertions(+) diff --git a/python/valuecell/server/api/routers/strategy.py b/python/valuecell/server/api/routers/strategy.py index 57cf4a4e9..8244e8599 100644 --- a/python/valuecell/server/api/routers/strategy.py +++ b/python/valuecell/server/api/routers/strategy.py @@ -17,6 +17,7 @@ StrategyHoldingFlatResponse, StrategyListData, StrategyListResponse, + StrategyPerformanceResponse, StrategyPortfolioSummaryResponse, StrategyStatusSuccessResponse, StrategyStatusUpdateResponse, @@ -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, diff --git a/python/valuecell/server/api/schemas/strategy.py b/python/valuecell/server/api/schemas/strategy.py index 845939c62..f8ae88df5 100644 --- a/python/valuecell/server/api/schemas/strategy.py +++ b/python/valuecell/server/api/schemas/strategy.py @@ -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] diff --git a/python/valuecell/server/services/strategy_service.py b/python/valuecell/server/services/strategy_service.py index 88a089ab4..abe148583 100644 --- a/python/valuecell/server/services/strategy_service.py +++ b/python/valuecell/server/services/strategy_service.py @@ -6,7 +6,9 @@ StrategyActionCard, StrategyCycleDetail, StrategyHoldingData, + StrategyPerformanceData, StrategyPortfolioSummaryData, + StrategyType, ) from valuecell.server.db.repositories import get_strategy_repository @@ -130,6 +132,128 @@ 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 + def _sanitize_config(strategy): + # Deprecated in favor of flattened fields in StrategyPerformanceData + # Kept for backward reference; not used by current code path. + raise NotImplementedError("StrategyConfigView removed; use flattened fields") + + @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, From 4012fcbf11606ae4f17b896cb6219629457506e5 Mon Sep 17 00:00:00 2001 From: paisley Date: Fri, 28 Nov 2025 10:09:05 +0800 Subject: [PATCH 2/2] remove unused code --- python/valuecell/server/services/strategy_service.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/python/valuecell/server/services/strategy_service.py b/python/valuecell/server/services/strategy_service.py index abe148583..2e1883bba 100644 --- a/python/valuecell/server/services/strategy_service.py +++ b/python/valuecell/server/services/strategy_service.py @@ -169,12 +169,6 @@ def _normalize_strategy_type(meta: dict, cfg: dict) -> Optional[StrategyType]: return ST.GRID return None - @staticmethod - def _sanitize_config(strategy): - # Deprecated in favor of flattened fields in StrategyPerformanceData - # Kept for backward reference; not used by current code path. - raise NotImplementedError("StrategyConfigView removed; use flattened fields") - @staticmethod async def get_strategy_performance( strategy_id: str,