diff --git a/services/trading/engine.py b/services/trading/engine.py index 63bfea2..7856b39 100644 --- a/services/trading/engine.py +++ b/services/trading/engine.py @@ -551,13 +551,41 @@ class TradingEngine: except Exception: logger.debug("Could not load circuit breaker state — using inactive") - # Build portfolio state with defaults - # Default initial capital for paper trading — overridden by broker sync + # Build portfolio state from broker account (real buying power) 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( reserve_pool=reserve_balance, - active_pool=max(0.0, initial_capital - reserve_balance), + active_pool=available, 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: @@ -672,6 +700,12 @@ class TradingEngine: # For "act" decisions: push order to broker queue 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 = { "trading_decision_id": decision.id, "ticker": decision.ticker, @@ -684,9 +718,11 @@ class TradingEngine: broker_queue = queue_key(QUEUE_BROKER) await self.redis.rpush(broker_queue, json.dumps(order_job)) 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.computed_share_quantity or 0, + order_cost, + self.portfolio_state.active_pool if self.portfolio_state else 0, ) # Persist decision @@ -816,6 +852,20 @@ class TradingEngine: if self.portfolio_state is None: 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 try: metrics = self.performance_computer.compute_metrics(