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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user