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:
+40
-119
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user