fix: remove broken capital controls, reset now queries broker for real balance

- Removed PUT /api/trading/capital (set capital) — only touched in-memory state
- Removed POST /api/trading/capital/adjust (add/withdraw) — same problem
- Reset endpoint now: liquidates Alpaca positions, cancels orders, clears DB,
  then queries Alpaca for real portfolio_value to set engine capital
- Frontend: replaced CapitalCard with simple ResetCard (one button)
- Removed useSetTradingCapital and useAdjustCapital hooks
This commit is contained in:
Celes Renata
2026-04-17 04:24:10 +00:00
parent 5fb59b379c
commit fd862da29e
4 changed files with 69 additions and 284 deletions
+40 -119
View File
@@ -60,9 +60,9 @@ class ConfigUpdateRequest(BaseModel):
class CapitalRequest(BaseModel):
"""Body for PUT /api/trading/capital."""
"""Body for POST /api/trading/reset."""
initial_capital: float
initial_capital: float = 0.0
class BacktestRequest(BaseModel):
@@ -300,131 +300,29 @@ async def resume_engine() -> dict[str, bool]:
return {"paused": False}
@app.put("/api/trading/capital")
async def set_capital(body: CapitalRequest) -> dict[str, Any]:
"""Set or reset the paper trading capital.
Allocates the given amount as initial capital, splitting between
active pool and reserve pool based on the current reserve_siphon_pct.
"""
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
ps = engine.portfolio_state
previous = {
"total_value": ps.total_value if ps else 0.0,
"active_pool": ps.active_pool if ps else 0.0,
"reserve_pool": ps.reserve_pool if ps else 0.0,
}
from services.trading.models import PortfolioState
engine.portfolio_state = PortfolioState(
total_value=capital,
cash=capital,
active_pool=active,
reserve_pool=reserve,
)
# Record in reserve pool ledger
if engine.pool:
try:
await engine.pool.execute(
"INSERT INTO reserve_pool_ledger (amount, balance_after, trigger_type, notes) "
"VALUES ($1, $2, 'capital_reset', $3)",
reserve, reserve,
f"Capital set to ${capital:,.2f} (active=${active:,.2f}, reserve=${reserve:,.2f})",
)
except Exception:
pass # Non-critical
return {
"initial_capital": capital,
"active_pool": active,
"reserve_pool": reserve,
"reserve_siphon_pct": reserve_pct,
"previous": previous,
}
@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.
"""Full paper trading reset: liquidate all broker positions, cancel open
orders, clear all local trading state, then query the broker for the
real account balance and set the engine's capital from that.
Also liquidates all positions and cancels all open orders on the
broker (Alpaca) so the paper account starts clean.
If initial_capital is provided and > 0, it overrides the broker balance
(useful when the broker is unavailable or you want a specific 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
# --- Reset broker (Alpaca) state ---
broker_result: dict[str, Any] = {"orders_cancelled": 0, "positions_closed": 0}
# --- Reset broker (Alpaca) state and query real balance ---
broker_result: dict[str, Any] = {
"orders_cancelled": 0,
"positions_closed": 0,
"portfolio_value": 0.0,
"cash": 0.0,
"buying_power": 0.0,
}
broker_capital: float | None = None
try:
from services.adapters.broker_adapter import AlpacaBrokerAdapter
from services.shared.config import load_config as _load_config
@@ -438,16 +336,39 @@ async def reset_paper_trading(body: CapitalRequest) -> dict[str, Any]:
)
broker_result["orders_cancelled"] = await adapter.cancel_all_orders()
broker_result["positions_closed"] = await adapter.close_all_positions()
# Query the real account balance after liquidation
acct = await adapter.get_account()
broker_result["portfolio_value"] = acct.portfolio_value
broker_result["cash"] = acct.cash
broker_result["buying_power"] = acct.buying_power
broker_capital = acct.portfolio_value
logger.info(
"Broker reset: cancelled %d orders, closed %d positions",
"Broker reset: cancelled %d orders, closed %d positions, "
"portfolio_value=%.2f, cash=%.2f",
broker_result["orders_cancelled"],
broker_result["positions_closed"],
acct.portfolio_value,
acct.cash,
)
else:
logger.info("No broker API key configured — skipping broker reset")
except Exception:
logger.exception("Broker reset failed — continuing with DB/engine reset")
# Determine capital: explicit override > broker balance > default 100k
if body.initial_capital > 0:
capital = body.initial_capital
elif broker_capital and broker_capital > 0:
capital = broker_capital
else:
capital = 100_000.0
reserve_pct = engine.config.reserve_siphon_pct
reserve = capital * reserve_pct
active = capital - reserve
# --- Clear trading state in the database ---
if engine.pool:
try: