fix: critical — track capital properly: load invested positions on startup, deduct on act, sync every 5min

This commit is contained in:
Celes Renata
2026-04-16 15:29:28 +00:00
parent 9a8d36068a
commit 2e77cf32fd
+54 -4
View File
@@ -551,13 +551,41 @@ class TradingEngine:
except Exception: except Exception:
logger.debug("Could not load circuit breaker state — using inactive") logger.debug("Could not load circuit breaker state — using inactive")
# Build portfolio state with defaults # Build portfolio state from broker account (real buying power)
# Default initial capital for paper trading — overridden by broker sync
initial_capital = 100000.0 initial_capital = 100000.0
try:
acct_row = await self.pool.fetchrow(
"SELECT config FROM broker_accounts WHERE active = TRUE ORDER BY created_at DESC LIMIT 1"
)
if acct_row and acct_row["config"]:
cfg = acct_row["config"] if isinstance(acct_row["config"], dict) else {}
initial_capital = float(cfg.get("portfolio_value", cfg.get("cash", 100000.0)))
except Exception:
logger.debug("Could not load broker account — using default capital")
# Load actual positions to calculate invested amount
invested = 0.0
open_count = 0
try:
pos_rows = await self.pool.fetch(
"SELECT quantity, avg_entry_price FROM positions WHERE quantity > 0"
)
for pr in pos_rows:
invested += float(pr["quantity"]) * float(pr["avg_entry_price"])
open_count += 1
except Exception:
logger.debug("Could not load positions — assuming no invested capital")
available = max(0.0, initial_capital - reserve_balance - invested)
self.portfolio_state = PortfolioState( self.portfolio_state = PortfolioState(
reserve_pool=reserve_balance, reserve_pool=reserve_balance,
active_pool=max(0.0, initial_capital - reserve_balance), active_pool=available,
total_value=initial_capital, total_value=initial_capital,
open_position_count=open_count,
)
logger.info(
"Portfolio state: total=$%.2f invested=$%.2f available=$%.2f reserve=$%.2f positions=%d",
initial_capital, invested, available, reserve_balance, open_count,
) )
async def _decision_loop(self) -> None: async def _decision_loop(self) -> None:
@@ -672,6 +700,12 @@ class TradingEngine:
# For "act" decisions: push order to broker queue # For "act" decisions: push order to broker queue
if decision.decision == "act": if decision.decision == "act":
# Deduct from active pool immediately to prevent over-allocation
order_cost = (decision.computed_share_quantity or 0) * rec.get("current_price", 0)
if self.portfolio_state and order_cost > 0:
self.portfolio_state.active_pool = max(0.0, self.portfolio_state.active_pool - order_cost)
self.portfolio_state.open_position_count += 1
order_job = { order_job = {
"trading_decision_id": decision.id, "trading_decision_id": decision.id,
"ticker": decision.ticker, "ticker": decision.ticker,
@@ -684,9 +718,11 @@ class TradingEngine:
broker_queue = queue_key(QUEUE_BROKER) broker_queue = queue_key(QUEUE_BROKER)
await self.redis.rpush(broker_queue, json.dumps(order_job)) await self.redis.rpush(broker_queue, json.dumps(order_job))
logger.info( logger.info(
"Pushed order for %s (%d shares) to broker queue", "Pushed order for %s (%d shares, $%.2f) to broker queue — remaining pool: $%.2f",
decision.ticker, decision.ticker,
decision.computed_share_quantity or 0, decision.computed_share_quantity or 0,
order_cost,
self.portfolio_state.active_pool if self.portfolio_state else 0,
) )
# Persist decision # Persist decision
@@ -816,6 +852,20 @@ class TradingEngine:
if self.portfolio_state is None: if self.portfolio_state is None:
continue continue
# Sync active_pool with actual positions from DB
try:
if self.pool is not None:
pos_rows = await self.pool.fetch(
"SELECT quantity, avg_entry_price FROM positions WHERE quantity > 0"
)
invested = sum(float(r["quantity"]) * float(r["avg_entry_price"]) for r in pos_rows)
open_count = len(pos_rows)
available = max(0.0, self.portfolio_state.total_value - self.portfolio_state.reserve_pool - invested)
self.portfolio_state.active_pool = available
self.portfolio_state.open_position_count = open_count
except Exception:
logger.debug("Could not sync positions for portfolio state")
# Update portfolio heat and metrics from current positions # Update portfolio heat and metrics from current positions
try: try:
metrics = self.performance_computer.compute_metrics( metrics = self.performance_computer.compute_metrics(