feat: paper trading capital controls — add, withdraw, and full reset

Three distinct capital operations on the Trading Controls page:

- Set Capital: overwrites pool balances to a new amount (existing)
- Add/Withdraw: adjusts active pool by a delta without touching
  positions, orders, or history. Validates sufficient balance for
  withdrawals. Logged to reserve_pool_ledger as manual_adjustment.
- Reset Everything: nuclear option — deletes all positions, orders,
  trading decisions, stop levels, snapshots, backtests, notifications,
  and circuit breaker events, then resets capital fresh. Red button
  with double-confirmation dialog.

Backend: POST /api/trading/capital/adjust and POST /api/trading/reset
Frontend: CapitalCard rebuilt with three sections and confirmation UIs
This commit is contained in:
Celes Renata
2026-04-17 02:23:26 +00:00
parent 45752b9a29
commit 90614dd7bb
3 changed files with 245 additions and 4 deletions
+113
View File
@@ -354,6 +354,119 @@ async def set_capital(body: CapitalRequest) -> dict[str, Any]:
}
@app.post("/api/trading/capital/adjust")
async def adjust_capital(body: dict[str, Any]) -> dict[str, Any]:
"""Add or subtract capital from the active pool without resetting anything.
Body: { "amount": 5000 } to add, { "amount": -2000 } to subtract.
Positions, orders, decisions, and history are untouched.
"""
if engine is None:
raise HTTPException(503, "Engine not initialised")
amount = body.get("amount", 0)
if not isinstance(amount, (int, float)) or amount == 0:
raise HTTPException(400, "amount must be a non-zero number")
ps = engine.portfolio_state
if ps is None:
raise HTTPException(400, "No portfolio state — set capital first")
new_active = ps.active_pool + amount
if new_active < 0:
raise HTTPException(400, f"Cannot subtract ${abs(amount):,.2f} — only ${ps.active_pool:,.2f} available in active pool")
previous_active = ps.active_pool
ps.active_pool = new_active
ps.cash = ps.cash + amount
ps.total_value = ps.total_value + amount
# Record in reserve pool ledger for audit
if engine.pool:
try:
trigger = "manual_adjustment"
note = f"{'Added' if amount > 0 else 'Withdrew'} ${abs(amount):,.2f} (active pool: ${previous_active:,.2f} → ${new_active:,.2f})"
await engine.pool.execute(
"INSERT INTO reserve_pool_ledger (amount, balance_after, trigger_type, notes) "
"VALUES ($1, $2, $3, $4)",
amount, new_active, trigger, note,
)
except Exception:
pass
return {
"adjusted": amount,
"active_pool": ps.active_pool,
"reserve_pool": ps.reserve_pool,
"total_value": ps.total_value,
}
@app.post("/api/trading/reset")
async def reset_paper_trading(body: CapitalRequest) -> dict[str, Any]:
"""Full paper trading reset: clear all positions, orders, decisions,
stop levels, and snapshots, then reset capital to the specified amount.
This is a destructive operation — all trading history is wiped.
"""
if engine is None:
raise HTTPException(503, "Engine not initialised")
capital = body.initial_capital
if capital <= 0:
raise HTTPException(400, "initial_capital must be positive")
reserve_pct = engine.config.reserve_siphon_pct
reserve = capital * reserve_pct
active = capital - reserve
# Clear trading state in the database
if engine.pool:
try:
async with engine.pool.acquire() as conn:
async with conn.transaction():
# Order matters due to FK constraints
await conn.execute("DELETE FROM backtest_trades")
await conn.execute("DELETE FROM backtest_runs")
await conn.execute("DELETE FROM position_stop_levels")
await conn.execute("DELETE FROM trading_decisions")
await conn.execute("DELETE FROM portfolio_snapshots")
await conn.execute("DELETE FROM reserve_pool_ledger")
await conn.execute("DELETE FROM risk_tier_history")
await conn.execute("DELETE FROM circuit_breaker_events")
await conn.execute("DELETE FROM notifications")
# Clear orders and their events
await conn.execute("DELETE FROM order_events")
await conn.execute("DELETE FROM orders")
# Re-seed reserve pool ledger
await conn.execute(
"INSERT INTO reserve_pool_ledger (amount, balance_after, trigger_type, notes) "
"VALUES ($1, $2, 'initial', $3)",
reserve, reserve,
f"Paper trading reset to ${capital:,.2f}",
)
except Exception:
logger.exception("Failed to clear trading tables during reset")
raise HTTPException(500, "Database reset failed")
# Reset in-memory engine state
from services.trading.models import CircuitBreakerState, PortfolioState
engine.portfolio_state = PortfolioState(
total_value=capital,
cash=capital,
active_pool=active,
reserve_pool=reserve,
)
engine.circuit_breaker_state = CircuitBreakerState()
return {
"reset": True,
"initial_capital": capital,
"active_pool": active,
"reserve_pool": reserve,
}
# ---------------------------------------------------------------------------
# Decision Audit Trail
# ---------------------------------------------------------------------------