From fd862da29e26ce1e5a3ec58bb82d42a5245e040b Mon Sep 17 00:00:00 2001 From: Celes Renata Date: Fri, 17 Apr 2026 04:24:10 +0000 Subject: [PATCH] fix: remove broken capital controls, reset now queries broker for real balance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- frontend/src/api/hooks.ts | 9 -- frontend/src/api/tradingHooks.ts | 22 +---- frontend/src/pages/Trading.tsx | 163 +++++-------------------------- services/trading/app.py | 159 ++++++++---------------------- 4 files changed, 69 insertions(+), 284 deletions(-) diff --git a/frontend/src/api/hooks.ts b/frontend/src/api/hooks.ts index 1b5d91d..cae80c4 100644 --- a/frontend/src/api/hooks.ts +++ b/frontend/src/api/hooks.ts @@ -426,15 +426,6 @@ export function useSetTradingMode() { }); } -export function useSetTradingCapital() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (initial_capital: number) => - apiPut<{ initial_capital: number; active_pool: number; reserve_pool: number }>('trading', '/api/trading/capital', { initial_capital }), - onSuccess: () => qc.invalidateQueries({ queryKey: ['trading-config'] }), - }); -} - export function usePendingApprovals() { return useGet(['pending-approvals'], 'query', '/api/admin/trading/approvals'); } diff --git a/frontend/src/api/tradingHooks.ts b/frontend/src/api/tradingHooks.ts index 87efd2f..a35a905 100644 --- a/frontend/src/api/tradingHooks.ts +++ b/frontend/src/api/tradingHooks.ts @@ -309,12 +309,13 @@ export function useBacktestLaunch() { }); } -/** Full paper trading reset: clears all positions, orders, decisions, and resets capital. */ +/** Full paper trading reset: liquidates broker positions, cancels orders, + * clears all local trading state, and syncs capital from the broker. */ export function useResetPaperTrading() { const qc = useQueryClient(); return useMutation({ - mutationFn: (initial_capital: number) => - apiPost<{ reset: boolean; initial_capital: number; active_pool: number; reserve_pool: number }>( + mutationFn: (initial_capital: number = 0) => + apiPost<{ reset: boolean; initial_capital: number; active_pool: number; reserve_pool: number; broker: Record }>( 'trading', '/api/trading/reset', { initial_capital }, ), onSuccess: () => { @@ -326,21 +327,6 @@ export function useResetPaperTrading() { }); } -/** Add or subtract capital from the active pool without resetting anything else. */ -export function useAdjustCapital() { - const qc = useQueryClient(); - return useMutation({ - mutationFn: (amount: number) => - apiPost<{ adjusted: number; active_pool: number; reserve_pool: number; total_value: number }>( - 'trading', '/api/trading/capital/adjust', { amount }, - ), - onSuccess: () => { - qc.invalidateQueries({ queryKey: ['trading-status'] }); - qc.invalidateQueries({ queryKey: ['trading-metrics'] }); - }, - }); -} - /** Update notification preferences. */ export function useUpdateNotificationConfig() { const qc = useQueryClient(); diff --git a/frontend/src/pages/Trading.tsx b/frontend/src/pages/Trading.tsx index 93df24c..1641dbc 100644 --- a/frontend/src/pages/Trading.tsx +++ b/frontend/src/pages/Trading.tsx @@ -2,7 +2,6 @@ import { useState } from 'react'; import { useTradingConfig, useSetTradingMode, - useSetTradingCapital, usePendingApprovals, useReviewApproval, useActiveLockouts, @@ -11,7 +10,7 @@ import { useCompetitiveStatus, useToggleCompetitive, } from '../api/hooks'; -import { useResetPaperTrading, useAdjustCapital } from '../api/tradingHooks'; +import { useResetPaperTrading } from '../api/tradingHooks'; import { StatusBadge, LoadingSpinner, Card } from '../components/ui'; export function TradingPage() { @@ -21,9 +20,7 @@ export function TradingPage() { const { data: macroStatus } = useMacroStatus(); const { data: competitiveStatus } = useCompetitiveStatus(); const setMode = useSetTradingMode(); - const setCapital = useSetTradingCapital(); const resetTrading = useResetPaperTrading(); - const adjustCapital = useAdjustCapital(); const reviewApproval = useReviewApproval(); const toggleMacro = useToggleMacro(); const toggleCompetitive = useToggleCompetitive(); @@ -92,14 +89,10 @@ export function TradingPage() { )} - {/* Paper Trading Capital */} - setCapital.mutate(amount)} - onReset={(amount) => resetTrading.mutate(amount)} - onAdjust={(amount) => adjustCapital.mutate(amount)} - isPending={setCapital.isPending} + {/* Paper Trading Reset */} + resetTrading.mutate(0)} isResetting={resetTrading.isPending} - isAdjusting={adjustCapital.isPending} /> {/* Macro Signal Layer Toggle */} @@ -309,77 +302,45 @@ function ApprovalRow({ approval, onReview }: { } -function CapitalCard({ onSetCapital, onReset, onAdjust, isPending, isResetting, isAdjusting }: { - onSetCapital: (amount: number) => void; - onReset: (amount: number) => void; - onAdjust: (amount: number) => void; - isPending: boolean; +function ResetCard({ onReset, isResetting }: { + onReset: () => void; isResetting: boolean; - isAdjusting: boolean; }) { - const [amount, setAmount] = useState('100000'); - const [adjustAmount, setAdjustAmount] = useState(''); const [showConfirm, setShowConfirm] = useState(false); - const [showResetConfirm, setShowResetConfirm] = useState(false); - - const presets = [10000, 50000, 100000, 500000]; - const busy = isPending || isResetting || isAdjusting; return ( -

Paper Trading Capital

- - {/* Set Capital */} -
+

Paper Trading Account

+
- - setAmount(e.target.value)} - className="w-40 rounded-md border border-surface-700 bg-surface-900 px-3 py-1.5 text-sm text-gray-200 focus:border-brand-500 focus:outline-none" - /> -
-
- {presets.map((p) => ( - - ))} +

Full Reset

+

+ Liquidates all broker positions, cancels open orders, wipes local trading history, + and syncs capital from the broker account. +

- {showConfirm && ( -
-

- This will reset the paper trading pools to ${Number(amount).toLocaleString()}. - Any existing pool balances will be overwritten. +

+

+ This will permanently delete all positions, orders, + trading decisions, stop levels, portfolio snapshots, and backtest data. + All broker positions will be liquidated and capital will be set from the broker's account balance.

)} - - {/* Add / Subtract Capital */} -
-

Adjust Capital

-
-
- - setAdjustAmount(e.target.value)} - placeholder="5000" - className="w-36 rounded-md border border-surface-700 bg-surface-900 px-3 py-1.5 text-sm text-gray-200 placeholder-gray-600 focus:border-brand-500 focus:outline-none" - /> -
- - -
- {isAdjusting &&

Adjusting…

} -
- - {/* Full Reset */} -
-
-
-

Full Reset

-

Wipes all positions, orders, decisions, and history. Resets capital to the amount above.

-
- -
- {showResetConfirm && ( -
-

- This will permanently delete all positions, orders, trading decisions, stop levels, portfolio snapshots, and backtest data. - Capital will be reset to ${Number(amount).toLocaleString()}. -

-
- - -
-
- )} -
); } diff --git a/services/trading/app.py b/services/trading/app.py index d3417aa..f7714f5 100644 --- a/services/trading/app.py +++ b/services/trading/app.py @@ -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: