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() {
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() {
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<string, number> }>(
'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();
+25 -138
View File
@@ -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() {
)}
</Card>
{/* Paper Trading Capital */}
<CapitalCard
onSetCapital={(amount) => setCapital.mutate(amount)}
onReset={(amount) => resetTrading.mutate(amount)}
onAdjust={(amount) => adjustCapital.mutate(amount)}
isPending={setCapital.isPending}
{/* Paper Trading Reset */}
<ResetCard
onReset={() => 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 (
<Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Paper Trading Capital</h2>
{/* Set Capital */}
<div className="flex flex-wrap items-end gap-3">
<h2 className="mb-3 text-sm font-medium text-gray-400">Paper Trading Account</h2>
<div className="flex items-center justify-between">
<div>
<label htmlFor="capital-input" className="mb-1 block text-xs text-gray-500">Initial Capital ($)</label>
<input
id="capital-input"
type="number"
min="100"
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>
))}
<p className="text-sm text-gray-300">Full Reset</p>
<p className="text-[10px] text-gray-600">
Liquidates all broker positions, cancels open orders, wipes local trading history,
and syncs capital from the broker account.
</p>
</div>
<button
onClick={() => setShowConfirm(true)}
disabled={busy || !amount || Number(amount) <= 0}
className="rounded-md bg-brand-600 px-4 py-1.5 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50"
disabled={isResetting}
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>
</div>
{showConfirm && (
<div className="mt-4 rounded-lg border border-orange-700/50 bg-orange-900/20 p-4">
<p className="text-sm text-orange-300">
This will reset the paper trading pools to <span className="font-semibold">${Number(amount).toLocaleString()}</span>.
Any existing pool balances will be overwritten.
<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.
All broker positions will be liquidated and capital will be set from the broker&apos;s account balance.
</p>
<div className="mt-3 flex gap-2">
<button
onClick={() => { onSetCapital(Number(amount)); setShowConfirm(false); }}
disabled={busy}
className="rounded-md bg-orange-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-orange-700 disabled:opacity-50"
onClick={() => { onReset(); setShowConfirm(false); }}
disabled={isResetting}
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
onClick={() => setShowConfirm(false)}
@@ -390,80 +351,6 @@ function CapitalCard({ onSetCapital, onReset, onAdjust, isPending, isResetting,
</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>
);
}
+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: