fix: critical — track capital properly: load invested positions on startup, deduct on act, sync every 5min
This commit is contained in:
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user