diff --git a/frontend/src/api/tradingHooks.ts b/frontend/src/api/tradingHooks.ts index 0eaddc2..87efd2f 100644 --- a/frontend/src/api/tradingHooks.ts +++ b/frontend/src/api/tradingHooks.ts @@ -309,6 +309,38 @@ export function useBacktestLaunch() { }); } +/** Full paper trading reset: clears all positions, orders, decisions, and resets capital. */ +export function useResetPaperTrading() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (initial_capital: number) => + apiPost<{ reset: boolean; initial_capital: number; active_pool: number; reserve_pool: number }>( + 'trading', '/api/trading/reset', { initial_capital }, + ), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ['trading-status'] }); + qc.invalidateQueries({ queryKey: ['trading-decisions'] }); + qc.invalidateQueries({ queryKey: ['trading-metrics'] }); + qc.invalidateQueries({ queryKey: ['trading-metrics-history'] }); + }, + }); +} + +/** 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 00a9aa9..93df24c 100644 --- a/frontend/src/pages/Trading.tsx +++ b/frontend/src/pages/Trading.tsx @@ -11,6 +11,7 @@ import { useCompetitiveStatus, useToggleCompetitive, } from '../api/hooks'; +import { useResetPaperTrading, useAdjustCapital } from '../api/tradingHooks'; import { StatusBadge, LoadingSpinner, Card } from '../components/ui'; export function TradingPage() { @@ -21,6 +22,8 @@ export function TradingPage() { 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(); @@ -90,7 +93,14 @@ export function TradingPage() { {/* Paper Trading Capital */} - setCapital.mutate(amount)} isPending={setCapital.isPending} /> + setCapital.mutate(amount)} + onReset={(amount) => resetTrading.mutate(amount)} + onAdjust={(amount) => adjustCapital.mutate(amount)} + isPending={setCapital.isPending} + isResetting={resetTrading.isPending} + isAdjusting={adjustCapital.isPending} + /> {/* Macro Signal Layer Toggle */} @@ -299,15 +309,27 @@ function ApprovalRow({ approval, onReview }: { } -function CapitalCard({ onSetCapital, isPending }: { onSetCapital: (amount: number) => void; isPending: boolean }) { +function CapitalCard({ onSetCapital, onReset, onAdjust, isPending, isResetting, isAdjusting }: { + onSetCapital: (amount: number) => void; + onReset: (amount: number) => void; + onAdjust: (amount: number) => void; + isPending: boolean; + 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 */}
@@ -338,7 +360,7 @@ function CapitalCard({ onSetCapital, isPending }: { onSetCapital: (amount: numbe
)} + + {/* 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 c614ccb..a5044a7 100644 --- a/services/trading/app.py +++ b/services/trading/app.py @@ -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 # ---------------------------------------------------------------------------