From 73f343c27b4d5e500d9777e523cf637b3c649f18 Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:08:30 +0800 Subject: [PATCH 1/4] fix(strategy): handle division by zero in portfolio summary calculation --- python/valuecell/server/services/strategy_service.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/python/valuecell/server/services/strategy_service.py b/python/valuecell/server/services/strategy_service.py index 2d13940e7..6d024f143 100644 --- a/python/valuecell/server/services/strategy_service.py +++ b/python/valuecell/server/services/strategy_service.py @@ -117,9 +117,10 @@ 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 = ( - total_pnl / (total_value - total_pnl) if total_pnl is not None else 0.0 - ) + total_pnl_pct = 0.0 + if total_value - total_pnl != 0: + total_pnl_pct = total_pnl / (total_value - total_pnl) + if baseline := _to_optional_float(first_snapshot.total_value): total_pnl = total_value - baseline total_pnl_pct = total_pnl / baseline @@ -138,11 +139,11 @@ async def get_strategy_portfolio_summary( ) @staticmethod - def _combine_realized_unrealized(snapshot) -> Optional[float]: + def _combine_realized_unrealized(snapshot) -> float: realized = _to_optional_float(getattr(snapshot, "total_realized_pnl", None)) unrealized = _to_optional_float(getattr(snapshot, "total_unrealized_pnl", None)) if realized is None and unrealized is None: - return None + return 0.0 return (realized or 0.0) + (unrealized or 0.0) @staticmethod From 824760fd83509b19170c64cd2bf1216d8e0874fa Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:25:12 +0800 Subject: [PATCH 2/4] fix(strategy): handle potential division by zero in portfolio summary calculations --- .../server/services/strategy_service.py | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/python/valuecell/server/services/strategy_service.py b/python/valuecell/server/services/strategy_service.py index 6d024f143..081026f1a 100644 --- a/python/valuecell/server/services/strategy_service.py +++ b/python/valuecell/server/services/strategy_service.py @@ -1,6 +1,8 @@ from datetime import datetime from typing import List, Optional +from loguru import logger + from valuecell.server.api.schemas.strategy import ( PositionHoldingItem, StrategyActionCard, @@ -118,12 +120,30 @@ async def get_strategy_portfolio_summary( total_value = _to_optional_float(snapshot.total_value) total_pnl = StrategyService._combine_realized_unrealized(snapshot) total_pnl_pct = 0.0 - if total_value - total_pnl != 0: - total_pnl_pct = total_pnl / (total_value - total_pnl) - if baseline := _to_optional_float(first_snapshot.total_value): - total_pnl = total_value - baseline - total_pnl_pct = total_pnl / baseline + # Option A: use total_value - total_pnl as baseline if available + try: + if total_value is not None: + denom = total_value - (total_pnl or 0.0) + if denom != 0: + total_pnl_pct = (total_pnl or 0.0) / denom + except Exception: + logger.warning( + "Failed to compute total_pnl_pct for strategy_id={}", strategy_id + ) + + # Option B: if first snapshot baseline is present, prefer it (avoid divide-by-zero) + first_baseline = _to_optional_float(first_snapshot.total_value) + if first_baseline is not None: + try: + total_pnl = (total_value or 0.0) - first_baseline + if first_baseline != 0: + total_pnl_pct = total_pnl / first_baseline + + except Exception: + logger.warning( + "Failed to compute total_pnl_pct for strategy_id={}", strategy_id + ) return StrategyPortfolioSummaryData( strategy_id=strategy_id, @@ -131,7 +151,7 @@ async def get_strategy_portfolio_summary( cash=_to_optional_float(snapshot.cash), total_value=total_value, total_pnl=total_pnl, - total_pnl_pct=_to_optional_float(total_pnl_pct) * 100.0, + total_pnl_pct=total_pnl_pct * 100.0, gross_exposure=_to_optional_float( getattr(snapshot, "gross_exposure", None) ), @@ -142,8 +162,6 @@ async def get_strategy_portfolio_summary( def _combine_realized_unrealized(snapshot) -> float: realized = _to_optional_float(getattr(snapshot, "total_realized_pnl", None)) unrealized = _to_optional_float(getattr(snapshot, "total_unrealized_pnl", None)) - if realized is None and unrealized is None: - return 0.0 return (realized or 0.0) + (unrealized or 0.0) @staticmethod From 749c0321f4df197d87a0e4cf8e1050289f3013db Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:31:13 +0800 Subject: [PATCH 3/4] fix(strategy): improve handling of division by zero in portfolio summary calculations --- .../server/services/strategy_service.py | 41 ++++++++----------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/python/valuecell/server/services/strategy_service.py b/python/valuecell/server/services/strategy_service.py index 081026f1a..9cef2a0dd 100644 --- a/python/valuecell/server/services/strategy_service.py +++ b/python/valuecell/server/services/strategy_service.py @@ -118,32 +118,25 @@ async def get_strategy_portfolio_summary( ts = snapshot.snapshot_ts or datetime.now(datetime.timezone.utc) total_value = _to_optional_float(snapshot.total_value) + base_value = _to_optional_float(first_snapshot.total_value) + total_pnl = StrategyService._combine_realized_unrealized(snapshot) total_pnl_pct = 0.0 - - # Option A: use total_value - total_pnl as baseline if available - try: - if total_value is not None: - denom = total_value - (total_pnl or 0.0) - if denom != 0: - total_pnl_pct = (total_pnl or 0.0) / denom - except Exception: - logger.warning( - "Failed to compute total_pnl_pct for strategy_id={}", strategy_id - ) - - # Option B: if first snapshot baseline is present, prefer it (avoid divide-by-zero) - first_baseline = _to_optional_float(first_snapshot.total_value) - if first_baseline is not None: - try: - total_pnl = (total_value or 0.0) - first_baseline - if first_baseline != 0: - total_pnl_pct = total_pnl / first_baseline - - except Exception: - logger.warning( - "Failed to compute total_pnl_pct for strategy_id={}", strategy_id - ) + if base_value is not None and base_value != 0: + # Option B: Use explicit baseline + # Note: This overrides the local variable total_pnl used in the return object + total_pnl = (total_value or 0.0) - base_value + total_pnl_pct = total_pnl / base_value + else: + # Option A: Infer baseline from current PnL + # Uses the existing total_pnl variable + current_pnl = total_pnl or 0.0 + denom = total_value - current_pnl + + if denom != 0: + total_pnl_pct = current_pnl / denom + else: + logger.warning(f"Cannot compute pnl_pct: denom is 0 for strategy_id={strategy_id}") return StrategyPortfolioSummaryData( strategy_id=strategy_id, From f3c841e1b54e3023cd88654a24b66b3622f2769c Mon Sep 17 00:00:00 2001 From: Felix <24791380+vcfgv@users.noreply.github.com> Date: Fri, 5 Dec 2025 11:32:54 +0800 Subject: [PATCH 4/4] fix(strategy): improve logging for division by zero in portfolio summary calculations --- python/valuecell/server/services/strategy_service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/python/valuecell/server/services/strategy_service.py b/python/valuecell/server/services/strategy_service.py index 9cef2a0dd..e0bacfa7c 100644 --- a/python/valuecell/server/services/strategy_service.py +++ b/python/valuecell/server/services/strategy_service.py @@ -132,11 +132,13 @@ async def get_strategy_portfolio_summary( # Uses the existing total_pnl variable current_pnl = total_pnl or 0.0 denom = total_value - current_pnl - + if denom != 0: total_pnl_pct = current_pnl / denom else: - logger.warning(f"Cannot compute pnl_pct: denom is 0 for strategy_id={strategy_id}") + logger.warning( + f"Cannot compute pnl_pct: denom is 0 for strategy_id={strategy_id}" + ) return StrategyPortfolioSummaryData( strategy_id=strategy_id,