Skip to content

Commit c4fcc06

Browse files
committed
feat: Initialize Portfolio with Existing Positions and Enhance Data Fetching Reliability
This pull request introduces two major enhancements to the portfolio initialization process: 1. Initial Position Loading: The system now correctly loads and integrates any existing open positions into the PortfolioView at startup. 2. Robust Data Fetching: A retry mechanism has been implemented for fetching portfolio data to improve reliability against transient network or API errors.
1 parent d85af0a commit c4fcc06

File tree

4 files changed

+100
-11
lines changed

4 files changed

+100
-11
lines changed

python/valuecell/agents/common/trading/_internal/runtime.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
)
1818
from ..models import Constraints, DecisionCycleResult, TradingMode, UserRequest
1919
from ..portfolio.in_memory import InMemoryPortfolioService
20-
from ..utils import fetch_free_cash_from_gateway
20+
from ..utils import fetch_free_cash_from_gateway, fetch_positions_from_gateway
2121
from .coordinator import DefaultDecisionCoordinator
2222

2323

@@ -32,6 +32,9 @@ async def _create_execution_gateway(request: UserRequest) -> BaseExecutionGatewa
3232
execution_gateway, request.trading_config.symbols
3333
)
3434
request.trading_config.initial_capital = float(free_cash)
35+
request.trading_config.initial_positions = (
36+
await fetch_positions_from_gateway(execution_gateway)
37+
)
3538
except Exception:
3639
# Log the error but continue - user might have set initial_capital manually
3740
logger.exception(
@@ -142,6 +145,7 @@ async def create_strategy_runtime(
142145
)
143146
portfolio_service = InMemoryPortfolioService(
144147
initial_capital=initial_capital,
148+
initial_positions=request.trading_config.initial_positions,
145149
trading_mode=request.exchange_config.trading_mode,
146150
market_type=request.exchange_config.market_type,
147151
constraints=constraints,

python/valuecell/agents/common/trading/models.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ class TradingConfig(BaseModel):
213213
description="Initial capital for trading in USD",
214214
gt=0,
215215
)
216+
initial_positions: Dict[str, "PositionSnapshot"] = Field(
217+
default={},
218+
description="Initial positions in portfolio",
219+
)
216220
max_leverage: float = Field(
217221
default=DEFAULT_MAX_LEVERAGE,
218222
description="Maximum leverage",

python/valuecell/agents/common/trading/portfolio/in_memory.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from datetime import datetime, timezone
2-
from typing import List, Optional
2+
from typing import Dict, List, Optional
33

44
from valuecell.agents.common.trading.models import (
55
Constraints,
@@ -37,6 +37,7 @@ class InMemoryPortfolioService(BasePortfolioService):
3737
def __init__(
3838
self,
3939
initial_capital: float,
40+
initial_positions: Dict[str, PositionSnapshot],
4041
trading_mode: TradingMode,
4142
market_type: MarketType,
4243
constraints: Optional[Constraints] = None,
@@ -45,16 +46,26 @@ def __init__(
4546
# Store owning strategy id on the view so downstream components
4647
# always see which strategy this portfolio belongs to.
4748
self._strategy_id = strategy_id
49+
position_value = sum(
50+
value.notional
51+
for value in initial_positions.values()
52+
if value.notional is not None
53+
)
54+
total_unrealized_pnl = sum(
55+
value.unrealized_pnl
56+
for value in initial_positions.values()
57+
if value.unrealized_pnl is not None
58+
)
4859
self._view = PortfolioView(
4960
strategy_id=strategy_id,
5061
ts=int(datetime.now(timezone.utc).timestamp() * 1000),
51-
account_balance=initial_capital,
52-
positions={},
53-
gross_exposure=0.0,
54-
net_exposure=0.0,
62+
account_balance=initial_capital + position_value,
63+
positions=initial_positions,
64+
gross_exposure=position_value,
65+
net_exposure=position_value,
5566
constraints=constraints or None,
56-
total_value=initial_capital,
57-
total_unrealized_pnl=0.0,
67+
total_value=initial_capital + position_value,
68+
total_unrealized_pnl=total_unrealized_pnl,
5869
total_realized_pnl=0.0,
5970
buying_power=initial_capital,
6071
free_cash=initial_capital,

python/valuecell/agents/common/trading/utils.py

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@
99
FEATURE_GROUP_BY_KEY,
1010
FEATURE_GROUP_BY_MARKET_SNAPSHOT,
1111
)
12-
from valuecell.agents.common.trading.models import FeatureVector
12+
from valuecell.agents.common.trading.models import (
13+
FeatureVector,
14+
InstrumentRef,
15+
PositionSnapshot,
16+
TradeType,
17+
)
1318

1419

1520
async def fetch_free_cash_from_gateway(
16-
execution_gateway, symbols: list[str]
21+
execution_gateway, symbols: list[str], retry_cnt: int = 0, max_retries: int = 3
1722
) -> Tuple[float, float]:
1823
"""Fetch exchange balance via `execution_gateway.fetch_balance()` and
1924
aggregate free cash for the given `symbols` (quote currencies).
@@ -26,7 +31,18 @@ async def fetch_free_cash_from_gateway(
2631
if not hasattr(execution_gateway, "fetch_balance"):
2732
return 0.0, 0.0
2833
balance = await execution_gateway.fetch_balance()
29-
except Exception:
34+
except Exception as e:
35+
if retry_cnt < max_retries:
36+
logger.warning(
37+
f"Failed to fetch free cash from exchange, retrying... ({retry_cnt + 1}/{max_retries})"
38+
)
39+
execution_gateway.new_exchange()
40+
return await fetch_free_cash_from_gateway(
41+
execution_gateway, symbols, retry_cnt + 1
42+
)
43+
logger.error(
44+
f"Failed to fetch free cash from exchange after {retry_cnt} retries, returning 0.0 {e}"
45+
)
3046
return 0.0, 0.0
3147

3248
logger.info(f"Raw balance response: {balance}")
@@ -94,6 +110,60 @@ async def fetch_free_cash_from_gateway(
94110
return float(free_cash), float(total_cash)
95111

96112

113+
async def fetch_positions_from_gateway(
114+
execution_gateway, retry_cnt: int = 0, max_retries: int = 3
115+
) -> Dict[str, PositionSnapshot]:
116+
"""Fetch positions from exchange."""
117+
logger.info("Fetching positions for LIVE trading mode")
118+
try:
119+
if not hasattr(execution_gateway, "fetch_positions"):
120+
return {}
121+
raw_positions = await execution_gateway.fetch_positions()
122+
except Exception as e:
123+
if retry_cnt < max_retries:
124+
logger.warning(
125+
f"Failed to fetch positions from exchange, retrying... ({retry_cnt + 1}/{max_retries})"
126+
)
127+
execution_gateway.new_exchange()
128+
return await fetch_positions_from_gateway(execution_gateway, retry_cnt + 1)
129+
logger.error(
130+
f"Failed to fetch positions from exchange after {retry_cnt} retries, returning no positions {e}"
131+
)
132+
return {}
133+
134+
logger.debug(f"Raw positions response: {raw_positions}")
135+
positions = {}
136+
for position in raw_positions:
137+
if "symbol" in position:
138+
symbol = position.get("symbol").split(":")[0]
139+
position = PositionSnapshot(
140+
instrument=InstrumentRef(
141+
exchange_id=execution_gateway.exchange_id,
142+
symbol=symbol,
143+
),
144+
quantity=abs(position.get("contracts")),
145+
avg_price=position.get("entryPrice"),
146+
mark_price=position.get("markPrice"),
147+
unrealized_pnl=position.get("unrealizedPnl"),
148+
notional=position.get("notional"),
149+
leverage=position.get("leverage") or 1,
150+
entry_ts=position.get("timestamp"),
151+
trade_type=(
152+
TradeType.LONG
153+
if position.get("side") == "long"
154+
else TradeType.SHORT
155+
),
156+
)
157+
if position.notional != 0:
158+
position.unrealized_pnl_pct = (
159+
position.unrealized_pnl / position.notional
160+
)
161+
positions[symbol] = position
162+
logger.info(f"Fetched positions: {positions}")
163+
164+
return positions
165+
166+
97167
def extract_market_snapshot_features(
98168
features: List[FeatureVector],
99169
) -> List[FeatureVector]:

0 commit comments

Comments
 (0)