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
-9
View File
@@ -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() { export function usePendingApprovals() {
return useGet<Approval[]>(['pending-approvals'], 'query', '/api/admin/trading/approvals'); return useGet<Approval[]>(['pending-approvals'], 'query', '/api/admin/trading/approvals');
} }
+4 -18
View File
@@ -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() { export function useResetPaperTrading() {
const qc = useQueryClient(); const qc = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (initial_capital: number) => mutationFn: (initial_capital: number = 0) =>
apiPost<{ reset: boolean; initial_capital: number; active_pool: number; reserve_pool: number }>( apiPost<{ reset: boolean; initial_capital: number; active_pool: number; reserve_pool: number; broker: Record<string, number> }>(
'trading', '/api/trading/reset', { initial_capital }, 'trading', '/api/trading/reset', { initial_capital },
), ),
onSuccess: () => { 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. */ /** Update notification preferences. */
export function useUpdateNotificationConfig() { export function useUpdateNotificationConfig() {
const qc = useQueryClient(); const qc = useQueryClient();
+25 -138
View File
@@ -2,7 +2,6 @@ import { useState } from 'react';
import { import {
useTradingConfig, useTradingConfig,
useSetTradingMode, useSetTradingMode,
useSetTradingCapital,
usePendingApprovals, usePendingApprovals,
useReviewApproval, useReviewApproval,
useActiveLockouts, useActiveLockouts,
@@ -11,7 +10,7 @@ import {
useCompetitiveStatus, useCompetitiveStatus,
useToggleCompetitive, useToggleCompetitive,
} from '../api/hooks'; } from '../api/hooks';
import { useResetPaperTrading, useAdjustCapital } from '../api/tradingHooks'; import { useResetPaperTrading } from '../api/tradingHooks';
import { StatusBadge, LoadingSpinner, Card } from '../components/ui'; import { StatusBadge, LoadingSpinner, Card } from '../components/ui';
export function TradingPage() { export function TradingPage() {
@@ -21,9 +20,7 @@ export function TradingPage() {
const { data: macroStatus } = useMacroStatus(); const { data: macroStatus } = useMacroStatus();
const { data: competitiveStatus } = useCompetitiveStatus(); const { data: competitiveStatus } = useCompetitiveStatus();
const setMode = useSetTradingMode(); const setMode = useSetTradingMode();
const setCapital = useSetTradingCapital();
const resetTrading = useResetPaperTrading(); const resetTrading = useResetPaperTrading();
const adjustCapital = useAdjustCapital();
const reviewApproval = useReviewApproval(); const reviewApproval = useReviewApproval();
const toggleMacro = useToggleMacro(); const toggleMacro = useToggleMacro();
const toggleCompetitive = useToggleCompetitive(); const toggleCompetitive = useToggleCompetitive();
@@ -92,14 +89,10 @@ export function TradingPage() {
)} )}
</Card> </Card>
{/* Paper Trading Capital */} {/* Paper Trading Reset */}
<CapitalCard <ResetCard
onSetCapital={(amount) => setCapital.mutate(amount)} onReset={() => resetTrading.mutate(0)}
onReset={(amount) => resetTrading.mutate(amount)}
onAdjust={(amount) => adjustCapital.mutate(amount)}
isPending={setCapital.isPending}
isResetting={resetTrading.isPending} isResetting={resetTrading.isPending}
isAdjusting={adjustCapital.isPending}
/> />
{/* Macro Signal Layer Toggle */} {/* Macro Signal Layer Toggle */}
@@ -309,77 +302,45 @@ function ApprovalRow({ approval, onReview }: {
} }
function CapitalCard({ onSetCapital, onReset, onAdjust, isPending, isResetting, isAdjusting }: { function ResetCard({ onReset, isResetting }: {
onSetCapital: (amount: number) => void; onReset: () => void;
onReset: (amount: number) => void;
onAdjust: (amount: number) => void;
isPending: boolean;
isResetting: boolean; isResetting: boolean;
isAdjusting: boolean;
}) { }) {
const [amount, setAmount] = useState('100000');
const [adjustAmount, setAdjustAmount] = useState('');
const [showConfirm, setShowConfirm] = useState(false); const [showConfirm, setShowConfirm] = useState(false);
const [showResetConfirm, setShowResetConfirm] = useState(false);
const presets = [10000, 50000, 100000, 500000];
const busy = isPending || isResetting || isAdjusting;
return ( return (
<Card> <Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Paper Trading Capital</h2> <h2 className="mb-3 text-sm font-medium text-gray-400">Paper Trading Account</h2>
<div className="flex items-center justify-between">
{/* Set Capital */}
<div className="flex flex-wrap items-end gap-3">
<div> <div>
<label htmlFor="capital-input" className="mb-1 block text-xs text-gray-500">Initial Capital ($)</label> <p className="text-sm text-gray-300">Full Reset</p>
<input <p className="text-[10px] text-gray-600">
id="capital-input" Liquidates all broker positions, cancels open orders, wipes local trading history,
type="number" and syncs capital from the broker account.
min="100" </p>
step="1000"
value={amount}
onChange={(e) => 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"
/>
</div>
<div className="flex gap-2">
{presets.map((p) => (
<button
key={p}
onClick={() => setAmount(String(p))}
className={`rounded-md border px-2 py-1.5 text-xs font-medium ${
amount === String(p)
? 'border-brand-500 bg-brand-600/20 text-brand-300'
: 'border-surface-700 bg-surface-900 text-gray-400 hover:bg-surface-800'
}`}
>
${(p / 1000).toFixed(0)}k
</button>
))}
</div> </div>
<button <button
onClick={() => setShowConfirm(true)} onClick={() => setShowConfirm(true)}
disabled={busy || !amount || Number(amount) <= 0} disabled={isResetting}
className="rounded-md bg-brand-600 px-4 py-1.5 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50" className="rounded-md border border-red-700/50 bg-red-900/20 px-3 py-1.5 text-sm font-medium text-red-400 hover:bg-red-900/40 disabled:opacity-50"
> >
Set Capital Reset Everything
</button> </button>
</div> </div>
{showConfirm && ( {showConfirm && (
<div className="mt-4 rounded-lg border border-orange-700/50 bg-orange-900/20 p-4"> <div className="mt-3 rounded-lg border border-red-700/50 bg-red-900/20 p-4">
<p className="text-sm text-orange-300"> <p className="text-sm text-red-300">
This will reset the paper trading pools to <span className="font-semibold">${Number(amount).toLocaleString()}</span>. This will <span className="font-semibold">permanently delete</span> all positions, orders,
Any existing pool balances will be overwritten. trading decisions, stop levels, portfolio snapshots, and backtest data.
All broker positions will be liquidated and capital will be set from the broker&apos;s account balance.
</p> </p>
<div className="mt-3 flex gap-2"> <div className="mt-3 flex gap-2">
<button <button
onClick={() => { onSetCapital(Number(amount)); setShowConfirm(false); }} onClick={() => { onReset(); setShowConfirm(false); }}
disabled={busy} disabled={isResetting}
className="rounded-md bg-orange-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-orange-700 disabled:opacity-50" className="rounded-md bg-red-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-50"
> >
{isPending ? 'Setting…' : 'Confirm'} {isResetting ? 'Resetting…' : 'Yes, Reset Everything'}
</button> </button>
<button <button
onClick={() => setShowConfirm(false)} onClick={() => setShowConfirm(false)}
@@ -390,80 +351,6 @@ function CapitalCard({ onSetCapital, onReset, onAdjust, isPending, isResetting,
</div> </div>
</div> </div>
)} )}
{/* Add / Subtract Capital */}
<div className="mt-4 border-t border-surface-700 pt-4">
<h3 className="mb-2 text-xs font-medium text-gray-500">Adjust Capital</h3>
<div className="flex items-end gap-2">
<div>
<label htmlFor="adjust-input" className="mb-1 block text-xs text-gray-500">Amount ($)</label>
<input
id="adjust-input"
type="number"
step="100"
value={adjustAmount}
onChange={(e) => 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"
/>
</div>
<button
onClick={() => { onAdjust(Math.abs(Number(adjustAmount))); setAdjustAmount(''); }}
disabled={busy || !adjustAmount || Number(adjustAmount) <= 0}
className="rounded-md bg-green-700 px-3 py-1.5 text-sm font-medium text-white hover:bg-green-600 disabled:opacity-50"
>
+ Add
</button>
<button
onClick={() => { onAdjust(-Math.abs(Number(adjustAmount))); setAdjustAmount(''); }}
disabled={busy || !adjustAmount || Number(adjustAmount) <= 0}
className="rounded-md bg-red-700 px-3 py-1.5 text-sm font-medium text-white hover:bg-red-600 disabled:opacity-50"
>
Withdraw
</button>
</div>
{isAdjusting && <p className="mt-1 text-xs text-gray-500">Adjusting</p>}
</div>
{/* Full Reset */}
<div className="mt-4 border-t border-surface-700 pt-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-xs font-medium text-gray-500">Full Reset</h3>
<p className="text-[10px] text-gray-600">Wipes all positions, orders, decisions, and history. Resets capital to the amount above.</p>
</div>
<button
onClick={() => setShowResetConfirm(true)}
disabled={busy || !amount || Number(amount) <= 0}
className="rounded-md border border-red-700/50 bg-red-900/20 px-3 py-1.5 text-sm font-medium text-red-400 hover:bg-red-900/40 disabled:opacity-50"
>
Reset Everything
</button>
</div>
{showResetConfirm && (
<div className="mt-3 rounded-lg border border-red-700/50 bg-red-900/20 p-4">
<p className="text-sm text-red-300">
This will <span className="font-semibold">permanently delete</span> all positions, orders, trading decisions, stop levels, portfolio snapshots, and backtest data.
Capital will be reset to <span className="font-semibold">${Number(amount).toLocaleString()}</span>.
</p>
<div className="mt-3 flex gap-2">
<button
onClick={() => { onReset(Number(amount)); setShowResetConfirm(false); }}
disabled={busy}
className="rounded-md bg-red-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-50"
>
{isResetting ? 'Resetting…' : 'Yes, Reset Everything'}
</button>
<button
onClick={() => setShowResetConfirm(false)}
className="rounded-md border border-surface-700 px-3 py-1.5 text-sm text-gray-400 hover:bg-surface-800"
>
Cancel
</button>
</div>
</div>
)}
</div>
</Card> </Card>
); );
} }
+40 -119
View File
@@ -60,9 +60,9 @@ class ConfigUpdateRequest(BaseModel):
class CapitalRequest(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): class BacktestRequest(BaseModel):
@@ -300,131 +300,29 @@ async def resume_engine() -> dict[str, bool]:
return {"paused": False} 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") @app.post("/api/trading/reset")
async def reset_paper_trading(body: CapitalRequest) -> dict[str, Any]: async def reset_paper_trading(body: CapitalRequest) -> dict[str, Any]:
"""Full paper trading reset: clear all positions, orders, decisions, """Full paper trading reset: liquidate all broker positions, cancel open
stop levels, and snapshots, then reset capital to the specified amount. 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 If initial_capital is provided and > 0, it overrides the broker balance
broker (Alpaca) so the paper account starts clean. (useful when the broker is unavailable or you want a specific amount).
This is a destructive operation — all trading history is wiped. This is a destructive operation — all trading history is wiped.
""" """
if engine is None: if engine is None:
raise HTTPException(503, "Engine not initialised") raise HTTPException(503, "Engine not initialised")
capital = body.initial_capital # --- Reset broker (Alpaca) state and query real balance ---
if capital <= 0: broker_result: dict[str, Any] = {
raise HTTPException(400, "initial_capital must be positive") "orders_cancelled": 0,
"positions_closed": 0,
reserve_pct = engine.config.reserve_siphon_pct "portfolio_value": 0.0,
reserve = capital * reserve_pct "cash": 0.0,
active = capital - reserve "buying_power": 0.0,
}
# --- Reset broker (Alpaca) state --- broker_capital: float | None = None
broker_result: dict[str, Any] = {"orders_cancelled": 0, "positions_closed": 0}
try: try:
from services.adapters.broker_adapter import AlpacaBrokerAdapter from services.adapters.broker_adapter import AlpacaBrokerAdapter
from services.shared.config import load_config as _load_config 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["orders_cancelled"] = await adapter.cancel_all_orders()
broker_result["positions_closed"] = await adapter.close_all_positions() 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( 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["orders_cancelled"],
broker_result["positions_closed"], broker_result["positions_closed"],
acct.portfolio_value,
acct.cash,
) )
else: else:
logger.info("No broker API key configured — skipping broker reset") logger.info("No broker API key configured — skipping broker reset")
except Exception: except Exception:
logger.exception("Broker reset failed — continuing with DB/engine reset") 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 --- # --- Clear trading state in the database ---
if engine.pool: if engine.pool:
try: try: