feat: enhanced paper trading reset with capital and reserve controls
ci/woodpecker/push/test Pipeline was successful
ci/woodpecker/push/build-1 Pipeline was successful
ci/woodpecker/push/build-2 Pipeline was successful
ci/woodpecker/push/build-3 Pipeline was successful
ci/woodpecker/push/finalize Pipeline was successful
Build and Push / lint-and-test (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.adapters.broker_adapter name:broker-adapter]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.aggregation.worker name:aggregation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.extractor.worker name:extractor]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.ingestion.worker name:ingestion]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.lake_publisher.worker name:lake-publisher]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.parser.worker name:parser]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.recommendation.worker name:recommendation]) (push) Has been cancelled
Build and Push / build-services (map[cmd:python -m services.scheduler.app name:scheduler]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.api.app:app --host 0.0.0.0 --port 8000 name:query-api]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.risk.app:app --host 0.0.0.0 --port 8000 name:risk]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.symbol_registry.app:app --host 0.0.0.0 --port 8000 name:symbol-registry]) (push) Has been cancelled
Build and Push / build-services (map[cmd:uvicorn services.trading.app:app --host 0.0.0.0 --port 8000 name:trading-engine]) (push) Has been cancelled
Build and Push / build-dashboard (push) Has been cancelled
Build and Push / build-superset (push) Has been cancelled
Build and Push / integration-test (push) Has been cancelled
Build and Push / beta-gate (push) Has been cancelled

- Add initial capital input (toggle between broker balance or custom amount)
- Add reserve/active pool split slider (0-50%, default 20%)
- Backend accepts reserve_pct in reset request body
- Note in UI that Alpaca balance reset requires Alpaca dashboard
- Confirmation dialog shows exact capital and split being applied
This commit is contained in:
Celes Renata
2026-04-30 05:49:38 +00:00
parent fa18b1a7c2
commit 5209cc522e
3 changed files with 98 additions and 9 deletions
+5 -2
View File
@@ -316,9 +316,12 @@ export function useBacktestLaunch() {
export function useResetPaperTrading() { export function useResetPaperTrading() {
const qc = useQueryClient(); const qc = useQueryClient();
return useMutation({ return useMutation({
mutationFn: (initial_capital: number = 0) => mutationFn: (params: { initial_capital?: number; reserve_pct?: number } = {}) =>
apiPost<{ reset: boolean; initial_capital: number; active_pool: number; reserve_pool: number; broker: Record<string, 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: params.initial_capital ?? 0,
reserve_pct: params.reserve_pct ?? undefined,
},
), ),
onSuccess: () => { onSuccess: () => {
qc.invalidateQueries({ queryKey: ['trading-status'] }); qc.invalidateQueries({ queryKey: ['trading-status'] });
+90 -6
View File
@@ -116,7 +116,7 @@ export function TradingPage() {
{/* Paper Trading Reset */} {/* Paper Trading Reset */}
<ResetCard <ResetCard
onReset={() => resetTrading.mutate(0)} onReset={(params) => resetTrading.mutate(params)}
isResetting={resetTrading.isPending} isResetting={resetTrading.isPending}
/> />
@@ -490,26 +490,101 @@ function ApprovalRow({ approval, onReview }: {
function ResetCard({ onReset, isResetting }: { function ResetCard({ onReset, isResetting }: {
onReset: () => void; onReset: (params: { initial_capital?: number; reserve_pct?: number }) => void;
isResetting: boolean; isResetting: boolean;
}) { }) {
const [showConfirm, setShowConfirm] = useState(false); const [showConfirm, setShowConfirm] = useState(false);
const [capitalInput, setCapitalInput] = useState('100000');
const [reservePct, setReservePct] = useState(20);
const [useCustomCapital, setUseCustomCapital] = useState(false);
const capital = parseFloat(capitalInput) || 0;
const reserveAmount = capital * (reservePct / 100);
const activeAmount = capital - reserveAmount;
return ( return (
<Card> <Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Paper Trading Account</h2> <h2 className="mb-3 text-sm font-medium text-gray-400">Paper Trading Account</h2>
{/* Capital & Reserve Configuration */}
<div className="mb-4 space-y-3 rounded-lg border border-surface-700 bg-surface-950 p-4">
<div className="flex items-center gap-3">
<button
onClick={() => setUseCustomCapital(!useCustomCapital)}
className={`relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-brand-500 focus:ring-offset-2 focus:ring-offset-surface-900 ${
useCustomCapital ? 'bg-brand-600' : 'bg-surface-700'
}`}
role="switch"
aria-checked={useCustomCapital}
aria-label="Set custom initial capital"
>
<span className={`pointer-events-none inline-block h-4 w-4 rounded-full bg-white shadow transition-transform ${
useCustomCapital ? 'translate-x-4' : 'translate-x-0'
}`} />
</button>
<span className="text-sm text-gray-300">Set initial capital</span>
<span className="text-[10px] text-gray-600">(otherwise uses broker account balance)</span>
</div>
{useCustomCapital && (
<div className="flex items-center gap-2">
<label htmlFor="reset-capital" className="text-xs text-gray-500">Capital $</label>
<input
id="reset-capital"
type="number"
min={0}
step={1000}
value={capitalInput}
onChange={(e) => setCapitalInput(e.target.value)}
className="w-36 rounded-md border border-surface-700 bg-surface-900 px-2 py-1 text-sm font-mono text-gray-200"
/>
</div>
)}
<div>
<div className="flex items-center justify-between">
<label htmlFor="reset-reserve" className="text-xs text-gray-500">
Reserve pool: {reservePct}%
</label>
<span className="text-xs text-gray-600">
Active: {100 - reservePct}%
</span>
</div>
<input
id="reset-reserve"
type="range"
min={0}
max={50}
step={5}
value={reservePct}
onChange={(e) => setReservePct(Number(e.target.value))}
className="mt-1 w-full accent-brand-600"
/>
{useCustomCapital && capital > 0 && (
<div className="mt-1 flex justify-between text-[10px] text-gray-600">
<span>Reserve: ${reserveAmount.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
<span>Active: ${activeAmount.toLocaleString(undefined, { maximumFractionDigits: 0 })}</span>
</div>
)}
</div>
</div>
{/* Reset Button */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<p className="text-sm text-gray-300">Full Reset</p> <p className="text-sm text-gray-300">Full Reset</p>
<p className="text-[10px] text-gray-600"> <p className="text-[10px] text-gray-600">
Liquidates all broker positions, cancels open orders, wipes local trading history, Liquidates all broker positions, cancels open orders, wipes local trading history,
and syncs capital from the broker account. and sets capital from {useCustomCapital ? 'the amount above' : 'the broker account balance'}.
</p>
<p className="mt-1 text-[10px] text-gray-600 italic">
Note: To reset the Alpaca paper account balance itself, use the Alpaca dashboard.
</p> </p>
</div> </div>
<button <button
onClick={() => setShowConfirm(true)} onClick={() => setShowConfirm(true)}
disabled={isResetting} 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" className="shrink-0 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 Reset Everything
</button> </button>
@@ -519,11 +594,20 @@ function ResetCard({ onReset, isResetting }: {
<p className="text-sm text-red-300"> <p className="text-sm text-red-300">
This will <span className="font-semibold">permanently delete</span> all positions, orders, This will <span className="font-semibold">permanently delete</span> all positions, orders,
trading decisions, stop levels, portfolio snapshots, and backtest data. 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. All broker positions will be liquidated.
{useCustomCapital
? ` Capital will be set to $${capital.toLocaleString()} (${reservePct}% reserve / ${100 - reservePct}% active).`
: ` Capital will be set from the broker's account balance (${reservePct}% reserve / ${100 - reservePct}% active).`}
</p> </p>
<div className="mt-3 flex gap-2"> <div className="mt-3 flex gap-2">
<button <button
onClick={() => { onReset(); setShowConfirm(false); }} onClick={() => {
onReset({
initial_capital: useCustomCapital ? capital : undefined,
reserve_pct: reservePct / 100,
});
setShowConfirm(false);
}}
disabled={isResetting} 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" className="rounded-md bg-red-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-red-700 disabled:opacity-50"
> >
+3 -1
View File
@@ -68,6 +68,7 @@ class CapitalRequest(BaseModel):
"""Body for POST /api/trading/reset.""" """Body for POST /api/trading/reset."""
initial_capital: float = 0.0 initial_capital: float = 0.0
reserve_pct: float | None = None
class BacktestRequest(BaseModel): class BacktestRequest(BaseModel):
@@ -430,7 +431,8 @@ async def reset_paper_trading(body: CapitalRequest) -> dict[str, Any]:
else: else:
capital = 100_000.0 capital = 100_000.0
reserve_pct = engine.config.reserve_siphon_pct reserve_pct = body.reserve_pct if body.reserve_pct is not None else engine.config.reserve_siphon_pct
reserve_pct = max(0.0, min(1.0, reserve_pct)) # clamp 0-100%
reserve = capital * reserve_pct reserve = capital * reserve_pct
active = capital - reserve active = capital - reserve