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
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const TradeStrategyCard: FC<TradeStrategyCardProps> = ({
onDelete,
}) => {
const stockColors = useStockColors();
const changeType = getChangeType(strategy.unrealized_pnl_pct);
const changeType = getChangeType(strategy.total_pnl_pct);
return (
<div
onClick={onClick}
Expand Down Expand Up @@ -93,8 +93,8 @@ const TradeStrategyCard: FC<TradeStrategyCardProps> = ({
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)})
</p>

{/* Status Badge */}
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/types/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 14 additions & 4 deletions python/valuecell/server/api/routers/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,17 +160,27 @@ 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,
strategy_type=normalize_strategy_type(meta, cfg),
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=(
Expand Down
9 changes: 5 additions & 4 deletions python/valuecell/server/api/schemas/strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
)
Expand Down
15 changes: 13 additions & 2 deletions python/valuecell/server/db/repositories/strategy_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,15 +231,19 @@ 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()
try:
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)
Expand Down Expand Up @@ -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]:
Expand Down
19 changes: 16 additions & 3 deletions python/valuecell/server/services/strategy_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,27 @@ 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 = (
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 = total_value - baseline
total_pnl_pct = total_pnl / baseline

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)
),
Expand Down