Skip to content

Commit 705ac71

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 f13d662 commit 705ac71

File tree

16 files changed

+259
-103
lines changed

16 files changed

+259
-103
lines changed

frontend/src/app/agent/components/strategy-items/forms/trading-strategy-form.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export const TradingStrategyForm = withForm({
2525
defaultValues: {
2626
strategy_type: "" as Strategy["strategy_type"],
2727
strategy_name: "",
28-
initial_capital: 1000,
28+
initial_capital_input: 1000,
2929
max_leverage: 2,
3030
decide_interval: 60,
3131
symbols: TRADING_SYMBOLS,
@@ -73,7 +73,7 @@ export const TradingStrategyForm = withForm({
7373

7474
<FieldGroup className="flex flex-row gap-4">
7575
{tradingMode === "virtual" && (
76-
<form.AppField name="initial_capital">
76+
<form.AppField name="initial_capital_input">
7777
{(field) => (
7878
<field.NumberField
7979
className="flex-1"

frontend/src/app/agent/components/strategy-items/modals/create-strategy-modal.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ const step2Schema = z.union([
9393
const step3Schema = z.object({
9494
strategy_type: z.enum(["PromptBasedStrategy", "GridStrategy"]),
9595
strategy_name: z.string().min(1, "Strategy name is required"),
96-
initial_capital: z.number().min(1, "Initial capital must be at least 1"),
96+
initial_capital_input: z
97+
.number()
98+
.min(1, "Initial capital must be at least 1"),
9799
max_leverage: z
98100
.number()
99101
.min(1, "Leverage must be at least 1")
@@ -265,7 +267,7 @@ const CreateStrategyModal: FC<CreateStrategyModalProps> = ({ children }) => {
265267
defaultValues: {
266268
strategy_type: "PromptBasedStrategy" as Strategy["strategy_type"],
267269
strategy_name: "",
268-
initial_capital: 1000,
270+
initial_capital_input: 1000,
269271
max_leverage: 2,
270272
decide_interval: 60,
271273
symbols: TRADING_SYMBOLS,
@@ -275,10 +277,16 @@ const CreateStrategyModal: FC<CreateStrategyModalProps> = ({ children }) => {
275277
onSubmit: step3Schema,
276278
},
277279
onSubmit: async ({ value }) => {
280+
const { initial_capital_input, ...restOfTradingConfig } = value;
281+
const trading_config = {
282+
...restOfTradingConfig,
283+
initial_free_cash: initial_capital_input,
284+
initial_total_cash: initial_capital_input,
285+
};
278286
const payload = {
279287
llm_model_config: form1.state.values,
280288
exchange_config: form2.state.values,
281-
trading_config: value,
289+
trading_config: trading_config,
282290
};
283291

284292
await createStrategy(payload);

frontend/src/app/rank/board.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export default function RankBoard() {
208208
<span>{strategyDetail.llm_model_id}</span>
209209

210210
<p>Initial Capital</p>
211-
<span>{strategyDetail.initial_capital}</span>
211+
<span>{strategyDetail.initial_total_cash}</span>
212212

213213
<p>Max Leverage</p>
214214
<span>{strategyDetail.max_leverage}x</span>

frontend/src/types/strategy.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export interface Strategy {
1717
// Strategy Performance types
1818
export type StrategyPerformance = {
1919
strategy_id: string;
20-
initial_capital: number;
20+
initial_total_cash: number;
2121
return_rate_pct: number;
2222
llm_provider: string;
2323
llm_model_id: string;
@@ -96,7 +96,8 @@ export interface CreateStrategyRequest {
9696
// Trading Strategy Configuration
9797
trading_config: {
9898
strategy_name: string;
99-
initial_capital: number;
99+
initial_free_cash: number;
100+
initial_total_cash: number;
100101
max_leverage: number;
101102
symbols: string[]; // e.g. ['BTC', 'ETH', ...]
102103
template_id: string;

frontend/src/types/system.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export interface StrategyDetail {
3434
llm_provider: string;
3535
llm_model_id: string;
3636
max_leverage: number;
37-
initial_capital: number;
37+
initial_total_cash: number;
3838
prompt: string;
3939
}
4040

@@ -48,6 +48,6 @@ export interface StrategyReport {
4848
llm_provider: string;
4949
llm_model_id: string;
5050
max_leverage: number;
51-
initial_capital: number;
51+
initial_total_cash: number;
5252
prompt: string;
5353
}

python/valuecell/agents/common/trading/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,8 @@ UserRequest
120120
TradingConfig
121121

122122
- `strategy_name?: str` — Display name
123-
- `initial_capital: float` — Starting capital (USD)
123+
- `initial_free_cash: float` — Starting free cash in USD
124+
- `initial_total_cash: float` — Starting total cash in USD (same as initial_free_cash without initial positions)
124125
- `max_leverage: float` — Maximum leverage allowed
125126
- `max_positions: int` — Concurrent position limit
126127
- `symbols: List[str]` — Instruments to trade (e.g., `["BTC-USDT", "ETH-USDT"]`)
@@ -459,7 +460,8 @@ For live trading with real exchanges:
459460
**The runtime automatically:**
460461

461462
- Fetches real account balance
462-
- Sets `initial_capital` to available cash
463+
- Sets `initial_free_cash` to available cash
464+
- Sets `initial_total_cash` to account total cash
463465
- Uses `CCXTExecutionGateway` for order submission
464466

465467
**Always test on testnet first** before using real funds

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -466,16 +466,17 @@ def build_summary(
466466
# Fallback to internal tracking if portfolio service is unavailable
467467
unrealized = float(self._unrealized_pnl or 0.0)
468468
# Fallback equity uses initial capital when view is unavailable
469-
equity = float(self._request.trading_config.initial_capital or 0.0)
469+
equity = float(
470+
(self._request.trading_config.initial_total_cash + unrealized)
471+
if self._request.trading_config.initial_total_cash is not None
472+
else 0.0
473+
)
470474

471475
# Keep internal state in sync (allow negative unrealized PnL)
472476
self._unrealized_pnl = float(unrealized)
473477

474-
initial_capital = self._request.trading_config.initial_capital or 0.0
475478
pnl_pct = (
476-
(self._realized_pnl + self._unrealized_pnl) / initial_capital
477-
if initial_capital
478-
else None
479+
(self._realized_pnl + self._unrealized_pnl) / equity if equity else None
479480
)
480481

481482
# Strategy-level unrealized percent: percent of equity (if equity is available)

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

Lines changed: 35 additions & 21 deletions
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

@@ -28,22 +28,26 @@ async def _create_execution_gateway(request: UserRequest) -> BaseExecutionGatewa
2828
# In LIVE mode, fetch exchange balance and set initial capital from free cash
2929
try:
3030
if request.exchange_config.trading_mode == TradingMode.LIVE:
31-
free_cash, _ = await fetch_free_cash_from_gateway(
31+
free_cash, total_cash = await fetch_free_cash_from_gateway(
3232
execution_gateway, request.trading_config.symbols
3333
)
34-
request.trading_config.initial_capital = float(free_cash)
34+
request.trading_config.initial_free_cash = float(free_cash)
35+
request.trading_config.initial_total_cash = float(total_cash)
36+
request.trading_config.initial_positions = (
37+
await fetch_positions_from_gateway(execution_gateway)
38+
)
3539
except Exception:
36-
# Log the error but continue - user might have set initial_capital manually
40+
# Log the error but continue - user might have set initial portfolio manually
3741
logger.exception(
38-
"Failed to fetch exchange balance for LIVE mode. Will use configured initial_capital instead."
42+
"Failed to fetch exchange balance for LIVE mode. Will use configured initial portfolio instead."
3943
)
4044

4145
# Validate initial capital for LIVE mode
4246
if request.exchange_config.trading_mode == TradingMode.LIVE:
43-
initial_cap = request.trading_config.initial_capital or 0.0
44-
if initial_cap <= 0:
47+
initial_total_cash = request.trading_config.initial_total_cash or 0.0
48+
if initial_total_cash <= 0:
4549
logger.error(
46-
f"LIVE trading mode has initial_capital={initial_cap}. "
50+
f"LIVE trading mode has initial_total_cash={initial_total_cash}. "
4751
"This usually means balance fetch failed or account has no funds. "
4852
"Strategy will not be able to trade without capital."
4953
)
@@ -99,7 +103,8 @@ async def create_strategy_runtime(
99103
... ),
100104
... trading_config=TradingConfig(
101105
... symbols=['BTC-USDT', 'ETH-USDT'],
102-
... initial_capital=10000.0,
106+
... initial_free_cash=10000.0,
107+
... initial_total_cash=10000.0,
103108
... max_leverage=10.0,
104109
... max_positions=5,
105110
... )
@@ -112,36 +117,45 @@ async def create_strategy_runtime(
112117
# Create strategy runtime components
113118
strategy_id = strategy_id_override or generate_uuid("strategy")
114119

115-
# If an initial capital override wasn't provided, and this is a resume
116-
# of an existing strategy, attempt to initialize from the persisted
117-
# portfolio snapshot so the in-memory portfolio starts with the
118-
# previously recorded equity.
119-
initial_capital_override = None
120+
# If this is a resume of an existing strategy,
121+
# attempt to initialize from the persisted portfolio snapshot
122+
# so the in-memory portfolio starts with the previously recorded equity.
123+
free_cash_override = None
124+
total_cash_override = None
120125
if strategy_id_override:
121126
try:
122127
repo = get_strategy_repository()
123128
snap = repo.get_latest_portfolio_snapshot(strategy_id_override)
124129
if snap is not None:
125-
initial_capital_override = float(snap.total_value or snap.cash or 0.0)
130+
free_cash_override = float(snap.cash or 0.0)
131+
total_cash_override = float(
132+
snap.total_value - snap.total_unrealized_pnl
133+
if (
134+
snap.total_value is not None
135+
and snap.total_unrealized_pnl is not None
136+
)
137+
else 0.0
138+
)
126139
logger.info(
127-
"Initialized runtime initial_capital from persisted snapshot for strategy_id=%s",
140+
"Initialized runtime initial capital from persisted snapshot for strategy_id=%s",
128141
strategy_id_override,
129142
)
130143
except Exception:
131144
logger.exception(
132-
"Failed to initialize initial_capital from persisted snapshot for strategy_id=%s",
145+
"Failed to initialize initial capital from persisted snapshot for strategy_id=%s",
133146
strategy_id_override,
134147
)
135148

136-
initial_capital = (
137-
initial_capital_override or request.trading_config.initial_capital or 0.0
138-
)
149+
free_cash = free_cash_override or request.trading_config.initial_free_cash or 0.0
150+
total_cash = total_cash_override or request.trading_config.initial_total_cash or 0.0
139151
constraints = Constraints(
140152
max_positions=request.trading_config.max_positions,
141153
max_leverage=request.trading_config.max_leverage,
142154
)
143155
portfolio_service = InMemoryPortfolioService(
144-
initial_capital=initial_capital,
156+
free_cash=free_cash,
157+
total_cash=total_cash,
158+
initial_positions=request.trading_config.initial_positions,
145159
trading_mode=request.exchange_config.trading_mode,
146160
market_type=request.exchange_config.market_type,
147161
constraints=constraints,

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

Lines changed: 26 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ def persist_initial_state(self, runtime: StrategyRuntime) -> None:
123123
self.strategy_id,
124124
)
125125

126-
# When running in LIVE mode, update DB config.initial_capital to exchange available balance
126+
# When running in LIVE mode, update DB config.initial_free_cash to exchange available balance
127127
# and record initial capital into strategy metadata for fast access by APIs.
128128
# Only perform this on the first snapshot to avoid overwriting user edits or restarts.
129129
try:
@@ -132,51 +132,54 @@ def persist_initial_state(self, runtime: StrategyRuntime) -> None:
132132
)
133133
is_live = trading_mode == agent_models.TradingMode.LIVE
134134
if is_live and is_first_snapshot:
135-
initial_cash = getattr(initial_portfolio, "free_cash", None)
136-
if initial_cash is None:
137-
initial_cash = getattr(
138-
initial_portfolio, "account_balance", None
139-
)
140-
if initial_cash is None:
141-
initial_cash = getattr(
142-
runtime.request.trading_config, "initial_capital", None
135+
initial_free_cash = (
136+
initial_portfolio.free_cash
137+
or initial_portfolio.account_balance
138+
or runtime.request.trading_config.initial_free_cash
139+
)
140+
initial_total_cash = (
141+
initial_portfolio.total_value
142+
- initial_portfolio.total_unrealized_pnl
143+
if (
144+
initial_portfolio.total_value is not None
145+
and initial_portfolio.total_unrealized_pnl is not None
143146
)
144-
145-
if initial_cash is not None:
147+
else runtime.request.trading_config.initial_free_cash
148+
)
149+
if initial_free_cash is not None:
146150
if strategy_persistence.update_initial_capital(
147-
self.strategy_id, float(initial_cash)
151+
self.strategy_id,
152+
float(initial_free_cash),
153+
float(initial_total_cash),
148154
):
149-
logger.info(
150-
"Updated DB initial_capital to {} for strategy={} (LIVE mode)",
151-
initial_cash,
152-
self.strategy_id,
153-
)
154155
try:
155156
# Also persist metadata for initial capital to avoid repeated first-snapshot queries
156157
strategy_persistence.set_initial_capital_metadata(
157158
strategy_id=self.strategy_id,
158-
initial_capital=float(initial_cash),
159+
initial_free_cash=float(initial_free_cash),
160+
initial_total_cash=float(initial_total_cash),
159161
source="live_snapshot_cash",
160162
ts_ms=timestamp_ms,
161163
)
162164
logger.info(
163-
"Recorded initial_capital_live={} (source=live_snapshot_cash) in metadata for strategy={}",
164-
initial_cash,
165+
"Recorded initial_free_cash={} initial_total_cash={} (source=live_snapshot_cash) in metadata for strategy={}",
166+
initial_free_cash,
167+
initial_total_cash,
165168
self.strategy_id,
166169
)
167170
except Exception:
168171
logger.exception(
169-
"Failed to set initial_capital metadata for {}",
172+
"Failed to set initial capital metadata for {}",
170173
self.strategy_id,
171174
)
172175
else:
173176
logger.warning(
174-
"Failed to update DB initial_capital for strategy={} (LIVE mode)",
177+
"Failed to update DB initial capital for strategy={} (LIVE mode)",
175178
self.strategy_id,
176179
)
177180
except Exception:
178181
logger.exception(
179-
"Error while updating DB initial_capital from live balance for {}",
182+
"Error while updating DB initial capital from live balance for {}",
180183
self.strategy_id,
181184
)
182185
except Exception:

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,11 +208,20 @@ class TradingConfig(BaseModel):
208208
default=None,
209209
description="Reuse existing strategy id to continue execution (resume semantics without extra flags)",
210210
)
211-
initial_capital: Optional[float] = Field(
211+
initial_free_cash: Optional[float] = Field(
212212
default=DEFAULT_INITIAL_CAPITAL,
213-
description="Initial capital for trading in USD",
213+
description="Initial free cash for trading in USD",
214214
gt=0,
215215
)
216+
initial_total_cash: Optional[float] = Field(
217+
default=DEFAULT_INITIAL_CAPITAL,
218+
description="Initial total cash for trading in USD",
219+
gt=0,
220+
)
221+
initial_positions: Dict[str, "PositionSnapshot"] = Field(
222+
default={},
223+
description="Initial positions in portfolio",
224+
)
216225
max_leverage: float = Field(
217226
default=DEFAULT_MAX_LEVERAGE,
218227
description="Maximum leverage",

0 commit comments

Comments
 (0)