From fb0575af636a4233451ad8a0441d0947696b54bc Mon Sep 17 00:00:00 2001 From: paisley Date: Mon, 1 Dec 2025 14:15:10 +0800 Subject: [PATCH 1/2] feat(strategy): integrate total PnL and percentage in strategy summary --- .../strategy-items/trade-strategy-group.tsx | 6 +++--- frontend/src/types/strategy.ts | 4 ++-- .../valuecell/server/api/routers/strategy.py | 18 ++++++++++++++---- .../valuecell/server/api/schemas/strategy.py | 9 +++++---- .../db/repositories/strategy_repository.py | 15 +++++++++++++-- .../server/services/strategy_service.py | 16 +++++++++++++--- 6 files changed, 50 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/agent/components/strategy-items/trade-strategy-group.tsx b/frontend/src/app/agent/components/strategy-items/trade-strategy-group.tsx index c01c0b49e..24b8c6fd7 100644 --- a/frontend/src/app/agent/components/strategy-items/trade-strategy-group.tsx +++ b/frontend/src/app/agent/components/strategy-items/trade-strategy-group.tsx @@ -50,7 +50,7 @@ const TradeStrategyCard: FC = ({ onDelete, }) => { const stockColors = useStockColors(); - const changeType = getChangeType(strategy.unrealized_pnl_pct); + const changeType = getChangeType(strategy.total_pnl_pct); return (
= ({ className="font-medium text-sm" style={{ color: stockColors[changeType] }} > - {formatChange(strategy.unrealized_pnl, "", 2)} ( - {formatChange(strategy.unrealized_pnl_pct, "%", 2)}) + {formatChange(strategy.total_pnl, "", 2)} ( + {formatChange(strategy.total_pnl_pct, "%", 2)})

{/* Status Badge */} diff --git a/frontend/src/types/strategy.ts b/frontend/src/types/strategy.ts index c3a2346bf..fb9965188 100644 --- a/frontend/src/types/strategy.ts +++ b/frontend/src/types/strategy.ts @@ -7,8 +7,8 @@ export interface Strategy { status: "running" | "stopped"; stop_reason?: string; trading_mode: "live" | "virtual"; - unrealized_pnl: number; - unrealized_pnl_pct: number; + total_pnl: number; + total_pnl_pct: number; created_at: string; exchange_id: string; model_id: string; diff --git a/python/valuecell/server/api/routers/strategy.py b/python/valuecell/server/api/routers/strategy.py index 1317c4ce6..001c63ea9 100644 --- a/python/valuecell/server/api/routers/strategy.py +++ b/python/valuecell/server/api/routers/strategy.py @@ -160,6 +160,18 @@ def normalize_strategy_type( f"{stop_reason_detail if stop_reason_detail else ''}".strip() ) or "..." + total_pnl, total_pnl_pct = 0.0, 0.0 + if ( + portfolio_summary + := await StrategyService.get_strategy_portfolio_summary( + s.strategy_id + ) + ): + total_pnl = to_optional_float(portfolio_summary.total_pnl) or 0.0 + total_pnl_pct = ( + to_optional_float(portfolio_summary.total_pnl_pct) or 0.0 + ) + item = StrategySummaryData( strategy_id=s.strategy_id, strategy_name=s.name, @@ -167,10 +179,8 @@ def normalize_strategy_type( status=status, stop_reason=stop_reason_display, trading_mode=normalize_trading_mode(meta, cfg), - unrealized_pnl=to_optional_float(meta.get("unrealized_pnl", 0.0)), - unrealized_pnl_pct=to_optional_float( - meta.get("unrealized_pnl_pct", 0.0) - ), + total_pnl=total_pnl, + total_pnl_pct=total_pnl_pct, created_at=s.created_at, exchange_id=(meta.get("exchange_id") or cfg.get("exchange_id")), model_id=( diff --git a/python/valuecell/server/api/schemas/strategy.py b/python/valuecell/server/api/schemas/strategy.py index f8ae88df5..5ba53d5b6 100644 --- a/python/valuecell/server/api/schemas/strategy.py +++ b/python/valuecell/server/api/schemas/strategy.py @@ -32,10 +32,8 @@ class StrategySummaryData(BaseModel): trading_mode: Optional[Literal["live", "virtual"]] = Field( None, description="Trading mode: live or virtual" ) - unrealized_pnl: Optional[float] = Field(None, description="Unrealized PnL value") - unrealized_pnl_pct: Optional[float] = Field( - None, description="Unrealized PnL percentage" - ) + total_pnl: Optional[float] = Field(None, description="Total PnL value") + total_pnl_pct: Optional[float] = Field(None, description="Total PnL percentage") created_at: Optional[datetime] = Field(None, description="Creation timestamp") exchange_id: Optional[str] = Field( None, description="Associated exchange identifier" @@ -117,6 +115,9 @@ class StrategyPortfolioSummaryData(BaseModel): None, description="Combined realized and unrealized PnL for the snapshot", ) + total_pnl_pct: Optional[float] = Field( + None, description="Total PnL percentage for the snapshot" + ) gross_exposure: Optional[float] = Field( None, description="Aggregate gross exposure at snapshot" ) diff --git a/python/valuecell/server/db/repositories/strategy_repository.py b/python/valuecell/server/db/repositories/strategy_repository.py index bc4b46f90..8ba8f3fd6 100644 --- a/python/valuecell/server/db/repositories/strategy_repository.py +++ b/python/valuecell/server/db/repositories/strategy_repository.py @@ -231,7 +231,7 @@ def get_latest_holdings(self, strategy_id: str) -> List[StrategyHolding]: session.close() def get_portfolio_snapshots( - self, strategy_id: str, limit: Optional[int] = None + self, strategy_id: str, limit: Optional[int] = None, descending: bool = True ) -> List[StrategyPortfolioView]: """Get aggregated portfolio snapshots for a strategy ordered by snapshot_ts desc.""" session = self._get_session() @@ -239,7 +239,11 @@ def get_portfolio_snapshots( query = ( session.query(StrategyPortfolioView) .filter(StrategyPortfolioView.strategy_id == strategy_id) - .order_by(desc(StrategyPortfolioView.snapshot_ts)) + .order_by( + desc(StrategyPortfolioView.snapshot_ts) + if descending + else asc(StrategyPortfolioView.snapshot_ts) + ) ) if limit: query = query.limit(limit) @@ -278,6 +282,13 @@ def get_latest_portfolio_snapshot( items = self.get_portfolio_snapshots(strategy_id, limit=1) return items[0] if items else None + def get_earliest_portfolio_snapshot( + self, strategy_id: str + ) -> Optional[StrategyPortfolioView]: + """Convenience: return the earliest portfolio snapshot or None.""" + items = self.get_portfolio_snapshots(strategy_id, limit=1, descending=False) + return items[0] if items else None + def get_holdings_by_snapshot( self, strategy_id: str, snapshot_ts: datetime ) -> List[StrategyHolding]: diff --git a/python/valuecell/server/services/strategy_service.py b/python/valuecell/server/services/strategy_service.py index c33d22319..d266520f4 100644 --- a/python/valuecell/server/services/strategy_service.py +++ b/python/valuecell/server/services/strategy_service.py @@ -110,14 +110,24 @@ async def get_strategy_portfolio_summary( if not snapshot: return None - ts = snapshot.snapshot_ts or datetime.utcnow() + first_snapshot = repo.get_first_portfolio_snapshot(strategy_id) + if not first_snapshot: + return None + + ts = snapshot.snapshot_ts or datetime.now(datetime.timezone.utc) + total_value = _to_optional_float(snapshot.total_value) + total_pnl = StrategyService._combine_realized_unrealized(snapshot) + total_pnl_pct = None + if baseline := _to_optional_float(first_snapshot.total_value): + total_pnl_pct = (total_pnl / baseline) if baseline else None return StrategyPortfolioSummaryData( strategy_id=strategy_id, ts=int(ts.timestamp() * 1000), cash=_to_optional_float(snapshot.cash), - total_value=_to_optional_float(snapshot.total_value), - total_pnl=StrategyService._combine_realized_unrealized(snapshot), + total_value=total_value, + total_pnl=total_pnl, + total_pnl_pct=total_pnl_pct, gross_exposure=_to_optional_float( getattr(snapshot, "gross_exposure", None) ), From 4200b772d47a364af30a2749e26616688170bfdb Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:37:47 +0800 Subject: [PATCH 2/2] feat(strategy): calculate total PnL percentage in portfolio summary --- python/valuecell/server/services/strategy_service.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/valuecell/server/services/strategy_service.py b/python/valuecell/server/services/strategy_service.py index d266520f4..1e9aaa295 100644 --- a/python/valuecell/server/services/strategy_service.py +++ b/python/valuecell/server/services/strategy_service.py @@ -117,9 +117,12 @@ async def get_strategy_portfolio_summary( ts = snapshot.snapshot_ts or datetime.now(datetime.timezone.utc) total_value = _to_optional_float(snapshot.total_value) total_pnl = StrategyService._combine_realized_unrealized(snapshot) - total_pnl_pct = None + total_pnl_pct = ( + total_pnl / (total_value - total_pnl) if total_pnl is not None else 0.0 + ) if baseline := _to_optional_float(first_snapshot.total_value): - total_pnl_pct = (total_pnl / baseline) if baseline else None + total_pnl = total_value - baseline + total_pnl_pct = total_pnl / baseline return StrategyPortfolioSummaryData( strategy_id=strategy_id,