add paper trading: mark a setup as taken, track open P&L, sell
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:
@@ -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 && (
|
||||
|
||||
Reference in New Issue
Block a user