add paper trading: mark a setup as taken, track open P&L, sell
Deploy / lint (push) Successful in 5s
Deploy / test (push) Successful in 35s
Deploy / deploy (push) Successful in 24s

New paper_trades table (migration 007) + service/router. "Mark as taken" on each
setup card (shares prefilled from position sizing, entry from current price, both
editable) records a simulated trade. Overview gains an Open Trades table that
marks each position to the latest close — P&L in $, %, and R-multiples — with a
total unrealized P&L footer and a Sell button to close at the current price.
Closed trades are retained for future realized-P&L reporting.

Deploy: alembic upgrade (new paper_trades table).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-16 06:33:56 +02:00
parent 050abc6f71
commit a69557f5d8
16 changed files with 736 additions and 1 deletions
@@ -1,5 +1,7 @@
import { useState } from 'react';
import type { TradeSetup } from '../../lib/types';
import { formatPrice, formatPercent } from '../../lib/format';
import { useCreatePaperTrade } from '../../hooks/usePaperTrades';
import { recommendationActionDirection, recommendationActionLabel } from '../../lib/recommendation';
import { useRiskSettings, type RiskSettings } from '../../hooks/useRiskSettings';
import { positionSize } from '../../lib/position';
@@ -113,6 +115,25 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
const sizing = positionSize(risk.accountSize, risk.riskPct, setup.entry_price, setup.stop_loss);
const counterTrend = regime ? isCounterTrend(setup.direction, regime.label) : false;
const createTrade = useCreatePaperTrade();
const [taking, setTaking] = useState(false);
const [takeShares, setTakeShares] = useState<number>(sizing?.shares ?? 0);
const [takeEntry, setTakeEntry] = useState<number>(currentPrice ?? setup.entry_price);
const confirmTake = () => {
createTrade.mutate(
{
symbol: setup.symbol,
direction: setup.direction as 'long' | 'short',
entry_price: takeEntry,
shares: takeShares,
stop_loss: setup.stop_loss,
target: setup.target,
},
{ onSuccess: () => setTaking(false) },
);
};
return (
<div
data-direction={setup.direction}
@@ -184,6 +205,63 @@ function SetupCard({ setup, action, currentPrice, risk, regime }: { setup?: Trad
<p className="text-[11px] text-gray-600">Set account size below to size this trade.</p>
)}
{!taking ? (
<button
onClick={() => {
setTakeShares(sizing?.shares ?? 0);
setTakeEntry(currentPrice ?? setup.entry_price);
setTaking(true);
}}
className="w-full rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-1.5 text-xs font-medium text-emerald-300 transition-colors hover:bg-emerald-500/20"
>
+ Mark as taken (paper trade)
</button>
) : (
<div className="rounded-md border border-white/[0.08] bg-white/[0.02] p-2.5 space-y-2">
<div className="grid grid-cols-2 gap-2">
<label className="block space-y-1">
<span className="text-[10px] uppercase tracking-wider text-gray-500">Shares</span>
<input
type="number"
min={0}
value={takeShares}
onChange={(e) => setTakeShares(Number(e.target.value))}
className="w-full input-glass px-2 py-1 text-sm num"
/>
</label>
<label className="block space-y-1">
<span className="text-[10px] uppercase tracking-wider text-gray-500">Entry</span>
<input
type="number"
min={0}
step="0.01"
value={takeEntry}
onChange={(e) => setTakeEntry(Number(e.target.value))}
className="w-full input-glass px-2 py-1 text-sm num"
/>
</label>
</div>
<p className="text-[10px] text-gray-500">
Stop {formatPrice(setup.stop_loss)} · Target {formatPrice(setup.target)} · {setup.direction.toUpperCase()}
</p>
<div className="flex gap-2">
<button
onClick={confirmTake}
disabled={createTrade.isPending || !(takeShares > 0) || !(takeEntry > 0)}
className="flex-1 rounded-md bg-emerald-500/20 px-3 py-1.5 text-xs font-medium text-emerald-300 hover:bg-emerald-500/30 disabled:opacity-50"
>
{createTrade.isPending ? 'Taking…' : 'Confirm'}
</button>
<button
onClick={() => setTaking(false)}
className="rounded-md border border-white/[0.08] px-3 py-1.5 text-xs text-gray-400 hover:text-gray-200"
>
Cancel
</button>
</div>
</div>
)}
<TargetTable setup={setup} />
{setup.conflict_flags.length > 0 && (