feat: paper trading capital controls — add, withdraw, and full reset

Three distinct capital operations on the Trading Controls page:

- Set Capital: overwrites pool balances to a new amount (existing)
- Add/Withdraw: adjusts active pool by a delta without touching
  positions, orders, or history. Validates sufficient balance for
  withdrawals. Logged to reserve_pool_ledger as manual_adjustment.
- Reset Everything: nuclear option — deletes all positions, orders,
  trading decisions, stop levels, snapshots, backtests, notifications,
  and circuit breaker events, then resets capital fresh. Red button
  with double-confirmation dialog.

Backend: POST /api/trading/capital/adjust and POST /api/trading/reset
Frontend: CapitalCard rebuilt with three sections and confirmation UIs
This commit is contained in:
Celes Renata
2026-04-17 02:23:26 +00:00
parent 45752b9a29
commit 90614dd7bb
3 changed files with 245 additions and 4 deletions
+32
View File
@@ -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();
+100 -4
View File
@@ -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() {
</Card>
{/* Paper Trading Capital */}
<CapitalCard onSetCapital={(amount) => setCapital.mutate(amount)} isPending={setCapital.isPending} />
<CapitalCard
onSetCapital={(amount) => 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 */}
<Card>
@@ -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 (
<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">
<div>
<label htmlFor="capital-input" className="mb-1 block text-xs text-gray-500">Initial Capital ($)</label>
@@ -338,7 +360,7 @@ function CapitalCard({ onSetCapital, isPending }: { onSetCapital: (amount: numbe
</div>
<button
onClick={() => setShowConfirm(true)}
disabled={isPending || !amount || Number(amount) <= 0}
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"
>
Set Capital
@@ -354,7 +376,7 @@ function CapitalCard({ onSetCapital, isPending }: { onSetCapital: (amount: numbe
<div className="mt-3 flex gap-2">
<button
onClick={() => { onSetCapital(Number(amount)); setShowConfirm(false); }}
disabled={isPending}
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"
>
{isPending ? 'Setting…' : 'Confirm'}
@@ -368,6 +390,80 @@ function CapitalCard({ onSetCapital, isPending }: { onSetCapital: (amount: numbe
</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>
);
}