From 6d13fb68e88ffce3a9fe3ccfa7e855e8549a6456 Mon Sep 17 00:00:00 2001 From: Zhaofeng Zhang <24791380+vcfgv@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:05:15 +0800 Subject: [PATCH] feat: add closed timestamp to PositionSnapshot and update position handling in InMemoryPortfolioService --- python/valuecell/agents/strategy_agent/models.py | 3 +++ .../agents/strategy_agent/portfolio/in_memory.py | 12 ++++++++++-- python/valuecell/server/services/strategy_service.py | 5 ++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/python/valuecell/agents/strategy_agent/models.py b/python/valuecell/agents/strategy_agent/models.py index cbf762d1a..b9e5bd174 100644 --- a/python/valuecell/agents/strategy_agent/models.py +++ b/python/valuecell/agents/strategy_agent/models.py @@ -355,6 +355,9 @@ class PositionSnapshot(BaseModel): entry_ts: Optional[int] = Field( default=None, description="Entry timestamp (ms) for the current position" ) + closed_ts: Optional[int] = Field( + default=None, description="Close timestamp (ms) for recently closed positions" + ) pnl_pct: Optional[float] = Field( default=None, description="Unrealized P&L as a percent of position value" ) diff --git a/python/valuecell/agents/strategy_agent/portfolio/in_memory.py b/python/valuecell/agents/strategy_agent/portfolio/in_memory.py index 2e56c27d6..b1095e185 100644 --- a/python/valuecell/agents/strategy_agent/portfolio/in_memory.py +++ b/python/valuecell/agents/strategy_agent/portfolio/in_memory.py @@ -98,8 +98,16 @@ def apply_trades( # Handle position quantity transitions and avg price if new_qty == 0.0: - # Fully closed - self._view.positions.pop(symbol, None) + # Fully closed — do NOT remove the position immediately. + # Keep a tombstone snapshot so downstream callers (UI / API) + # that poll holdings immediately after execution can still see + # the just-closed position. Mark it closed with a timestamp. + position.quantity = 0.0 + position.mark_price = price + # preserve avg_price and entry_ts for auditing; record closed_ts + position.closed_ts = int(datetime.now(timezone.utc).timestamp() * 1000) + position.unrealized_pnl = 0.0 + position.unrealized_pnl_pct = None elif current_qty == 0.0: # Opening new position position.quantity = new_qty diff --git a/python/valuecell/server/services/strategy_service.py b/python/valuecell/server/services/strategy_service.py index 0a5e5476a..da9f79ce5 100644 --- a/python/valuecell/server/services/strategy_service.py +++ b/python/valuecell/server/services/strategy_service.py @@ -28,7 +28,10 @@ async def get_strategy_holding(strategy_id: str) -> Optional[StrategyHoldingData for h in holdings: try: t = h.type - qty = float(h.quantity) if h.quantity is not None else 0.0 + if h.quantity is None or h.quantity == 0.0: + # Skip fully closed positions + continue + qty = float(h.quantity) positions.append( PositionHoldingItem( symbol=h.symbol,