diff --git a/frontend/index.html b/frontend/index.html index e86f721..05e4897 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -7,11 +7,11 @@ - +
diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bc56c16..362d374 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,12 +9,18 @@ import SignalsPage from './pages/SignalsPage'; import RegimePage from './pages/RegimePage'; import TickerDetailPage from './pages/TickerDetailPage'; import AdminPage from './pages/AdminPage'; +import DesignMockupsPage from './pages/DesignMockupsPage'; +import DesignHorizonPage from './pages/DesignHorizonPage'; +import DesignOrbitPage from './pages/DesignOrbitPage'; export default function App() { return ( } /> } /> + } /> + } /> + } /> }> }> } /> diff --git a/frontend/src/components/charts/CandlestickChart.tsx b/frontend/src/components/charts/CandlestickChart.tsx index c984c30..87847c9 100644 --- a/frontend/src/components/charts/CandlestickChart.tsx +++ b/frontend/src/components/charts/CandlestickChart.tsx @@ -160,7 +160,7 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, const nTicks = 6; ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.lineWidth = 1; - ctx.fillStyle = '#6b7280'; + ctx.fillStyle = '#6e7484'; ctx.font = '11px "IBM Plex Mono", ui-monospace, monospace'; ctx.textAlign = 'right'; for (let i = 0; i <= nTicks; i++) { @@ -178,7 +178,7 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, const labelInterval = Math.max(Math.floor(visibleData.length / 8), 1); for (let i = 0; i < visibleData.length; i += labelInterval) { const x = ml + i * barW + barW / 2; - ctx.fillStyle = '#6b7280'; + ctx.fillStyle = '#6e7484'; ctx.fillText(formatDate(visibleData[i].date), x, H - 6); } @@ -195,7 +195,7 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, ctx.stroke(); ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace'; - ctx.fillStyle = '#6b7280'; + ctx.fillStyle = '#6e7484'; ctx.textAlign = 'left'; ctx.fillText('Volume', ml, volumeTop - 13); ctx.textAlign = 'right'; @@ -206,20 +206,22 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, const bullish = bar.close >= bar.open; const yVolume = volumeScale(bar.volume); const hVolume = Math.max(volumeBottom - yVolume, bar.volume > 0 ? 1 : 0); - ctx.fillStyle = bullish ? 'rgba(16, 185, 129, 0.32)' : 'rgba(239, 68, 68, 0.28)'; + ctx.fillStyle = bullish ? 'rgba(47, 157, 178, 0.32)' : 'rgba(227, 106, 88, 0.28)'; ctx.fillRect(x - volumeW / 2, yVolume, volumeW, hVolume); }); // Nearest support/resistance only (band if it came from a zone) markers.forEach((m) => { const isSupport = m.role === 'support'; - const color = isSupport ? '#10b981' : '#ef4444'; + // Support = rim-cyan, resistance = neutral ink — rose stays reserved for + // the stop level so S/R never impersonates trade levels. + const color = isSupport ? '#2f9db2' : '#9aa0b0'; const yMid = yScale(m.price); if (m.high > m.low) { const yTop = yScale(m.high); const rectHeight = Math.max(yScale(m.low) - yTop, 2); - ctx.fillStyle = isSupport ? 'rgba(16, 185, 129, 0.12)' : 'rgba(239, 68, 68, 0.12)'; + ctx.fillStyle = isSupport ? 'rgba(47, 157, 178, 0.12)' : 'rgba(154, 160, 176, 0.10)'; ctx.fillRect(ml, yTop, cw, rectHeight); } @@ -253,10 +255,10 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, // Stop-loss zone: red semi-transparent rectangle between entry and stop-loss const slTop = Math.min(entryY, stopY); const slHeight = Math.max(Math.abs(stopY - entryY), 1); - ctx.fillStyle = 'rgba(239, 68, 68, 0.13)'; + ctx.fillStyle = 'rgba(227, 106, 88, 0.13)'; ctx.fillRect(ml, slTop, cw, slHeight); // Stop-loss border - ctx.strokeStyle = 'rgba(239, 68, 68, 0.4)'; + ctx.strokeStyle = 'rgba(227, 106, 88, 0.45)'; ctx.lineWidth = 1; ctx.setLineDash([4, 3]); ctx.beginPath(); @@ -268,10 +270,10 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, // Take-profit zone: green semi-transparent rectangle between entry and target const tpTop = Math.min(entryY, targetY); const tpHeight = Math.max(Math.abs(targetY - entryY), 1); - ctx.fillStyle = 'rgba(16, 185, 129, 0.13)'; + ctx.fillStyle = 'rgba(47, 157, 178, 0.13)'; ctx.fillRect(ml, tpTop, cw, tpHeight); // Target border - ctx.strokeStyle = 'rgba(16, 185, 129, 0.4)'; + ctx.strokeStyle = 'rgba(47, 157, 178, 0.45)'; ctx.lineWidth = 1; ctx.setLineDash([4, 3]); ctx.beginPath(); @@ -280,8 +282,8 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, ctx.stroke(); ctx.setLineDash([]); - // Entry price: dashed horizontal line (blue/white) - ctx.strokeStyle = 'rgba(96, 165, 250, 0.9)'; + // Entry price: dashed horizontal line (neutral ink) + ctx.strokeStyle = 'rgba(154, 160, 176, 0.9)'; ctx.lineWidth = 1.5; ctx.setLineDash([6, 4]); ctx.beginPath(); @@ -293,11 +295,11 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, // Labels on right side ctx.font = '10px "IBM Plex Mono", ui-monospace, monospace'; ctx.textAlign = 'left'; - ctx.fillStyle = 'rgba(96, 165, 250, 0.9)'; + ctx.fillStyle = 'rgba(154, 160, 176, 0.95)'; ctx.fillText(`Entry ${formatPrice(tradeSetup.entry_price)}`, ml + cw + 4, entryY + 3); - ctx.fillStyle = 'rgba(239, 68, 68, 0.8)'; + ctx.fillStyle = 'rgba(239, 145, 130, 0.9)'; ctx.fillText(`SL ${formatPrice(tradeSetup.stop_loss)}`, ml + cw + 4, stopY + 3); - ctx.fillStyle = 'rgba(16, 185, 129, 0.8)'; + ctx.fillStyle = 'rgba(110, 201, 219, 0.9)'; ctx.fillText(`TP ${formatPrice(tradeSetup.target)}`, ml + cw + 4, targetY + 3); } @@ -316,7 +318,7 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, const tw = ctx.measureText(label).width; ctx.fillStyle = 'rgba(226, 232, 240, 0.95)'; ctx.fillRect(ml + 2, py - 8, tw + 8, 16); - ctx.fillStyle = '#0e120f'; + ctx.fillStyle = '#0a0b11'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(label, ml + 6, py); @@ -327,7 +329,7 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, visibleData.forEach((bar, i) => { const x = ml + i * barW + barW / 2; const bullish = bar.close >= bar.open; - const color = bullish ? '#10b981' : '#ef4444'; + const color = bullish ? '#2f9db2' : '#e36a58'; const yHigh = yScale(bar.high); const yLow = yScale(bar.low); @@ -441,11 +443,11 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, const labelX = ml + cw + 2; const labelY = cy - labelH / 2; - ctx.fillStyle = 'rgba(55, 65, 81, 0.9)'; + ctx.fillStyle = 'rgba(35, 39, 51, 0.95)'; ctx.beginPath(); ctx.roundRect(labelX, labelY, labelW, labelH, 3); ctx.fill(); - ctx.fillStyle = '#e5e7eb'; + ctx.fillStyle = '#edeef3'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(priceText, labelX + labelPadX, cy); @@ -462,11 +464,11 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup, const dateLabelX = cx - dateLabelW / 2; const dateLabelY = H - mb + 2; - ctx.fillStyle = 'rgba(55, 65, 81, 0.9)'; + ctx.fillStyle = 'rgba(35, 39, 51, 0.95)'; ctx.beginPath(); ctx.roundRect(dateLabelX, dateLabelY, dateLabelW, dateLabelH, 3); ctx.fill(); - ctx.fillStyle = '#e5e7eb'; + ctx.fillStyle = '#edeef3'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(dateText, cx, dateLabelY + dateLabelH / 2); @@ -667,8 +669,8 @@ export function CandlestickChart({ data, srLevels = [], zones = [], tradeSetup,
Direction${tradeSetup.direction} Entry${formatPrice(tradeSetup.entry_price)} - Stop${formatPrice(tradeSetup.stop_loss)} - Target${formatPrice(tradeSetup.target)} + Stop${formatPrice(tradeSetup.stop_loss)} + Target${formatPrice(tradeSetup.target)} R:R${tradeSetup.rr_ratio.toFixed(2)}
`; } diff --git a/frontend/src/components/charts/horizon.tsx b/frontend/src/components/charts/horizon.tsx new file mode 100644 index 0000000..214a03b --- /dev/null +++ b/frontend/src/components/charts/horizon.tsx @@ -0,0 +1,295 @@ +/** + * Horizon chart primitives shared by the dashboard and ticker page. + * All colors come from the :root tokens in globals.css; the data marks + * (--up / --down) are validated against the dark surface. + */ +import { useState } from 'react'; +import type { OHLCVBar } from '../../lib/types'; + +function fmt(n: number, digits = 2): string { + return n.toLocaleString('en-US', { minimumFractionDigits: digits, maximumFractionDigits: digits }); +} + +function signedR(r: number): string { + return `${r > 0 ? '+' : r < 0 ? '−' : ''}${fmt(Math.abs(r))}`; +} + +/* ------------------------------------------------------------------ */ +/* RBar — diverging R-multiple bar growing from a centered zero line */ +/* ------------------------------------------------------------------ */ + +export function RBar({ r, max = 1.6 }: { r: number | null; max?: number }) { + const pct = r == null ? 0 : Math.min(Math.abs(r) / Math.max(max, 0.01), 1) * 50; + return ( +
+ + {r != null && ( + = 0 ? '50%' : `${50 - pct}%`, + width: `${pct}%`, + background: r >= 0 ? 'var(--up)' : 'var(--down)', + borderRadius: r >= 0 ? '0 4px 4px 0' : '4px 0 0 4px', + }} + /> + )} +
+ ); +} + +/* ------------------------------------------------------------------ */ +/* PriceRail — stop → entry → now → target laid out spatially */ +/* ------------------------------------------------------------------ */ + +export function PriceRail({ + direction, entry, stop, target, current, +}: { + direction: string; + entry: number; + stop: number; + target: number; + current: number | null; +}) { + const points = [entry, stop, target, ...(current != null ? [current] : [])]; + const span = Math.max(...points) - Math.min(...points) || 1; + const lo = Math.min(...points) - span * 0.06; + const hi = Math.max(...points) + span * 0.06; + const pct = (v: number) => Math.min(100, Math.max(0, ((v - lo) / (hi - lo)) * 100)); + const risk = Math.abs(entry - stop); + const rNow = current != null && risk > 0 + ? (direction === 'long' ? current - entry : entry - current) / risk + : null; + const rTarget = risk > 0 ? Math.abs(target - entry) / risk : null; + const inProfit = rNow != null && rNow >= 0; + const progressLeft = current != null ? Math.min(pct(entry), pct(current)) : 0; + const progressWidth = current != null ? Math.abs(pct(current) - pct(entry)) : 0; + return ( +
+
+
+ {current != null && ( +
+ )} +
+ + + stop + {fmt(stop)} + −1R + +
+
+ + + entry + {fmt(entry)} + +
+ {current != null && ( +
+ + + now + {fmt(current)} + {rNow != null && {signedR(rNow)}R} + +
+ )} +
+ + + target + {fmt(target)} + {rTarget != null && +{fmt(rTarget, 1)}R} + +
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* RadarChart — score fingerprint over n axes (0–100), hover for values */ +/* ------------------------------------------------------------------ */ + +export interface RadarAxis { + label: string; + value: number; + /** Longer name for the tooltip / screen reader (falls back to label). */ + full?: string; +} + +export function RadarChart({ + axes, size = 120, labels = true, +}: { axes: RadarAxis[]; size?: number; labels?: boolean }) { + const [hover, setHover] = useState(null); + const n = axes.length; + if (n < 3) return null; + const pad = labels ? 74 : 10; + const vb = size + pad * 2; + const c = vb / 2; + const r = size / 2; + const angle = (i: number) => ((-90 + (i * 360) / n) * Math.PI) / 180; + const pt = (v: number, i: number): [number, number] => { + const a = angle(i); + return [c + (Math.cos(a) * r * Math.max(0, Math.min(100, v))) / 100, c + (Math.sin(a) * r * Math.max(0, Math.min(100, v))) / 100]; + }; + const poly = (vals: number[]) => + vals.map((v, i) => pt(v, i).map((x) => x.toFixed(1)).join(',')).join(' '); + const tip = hover !== null ? pt(axes[hover].value, hover) : null; + return ( +
+ `${a.full ?? a.label} ${Math.round(a.value)}`).join(', ')} (0–100)`} + > + {[50, 100].map((ring) => ( + + ))} + {axes.map((a, i) => { + const [x, y] = pt(100, i); + return ; + })} + a.value))} + fill="var(--up)" fillOpacity="0.13" + stroke="var(--up)" strokeWidth="2" strokeLinejoin="round" + /> + {axes.map((a, i) => { + const [x, y] = pt(a.value, i); + return ( + + ); + })} + {labels && axes.map((a, i) => { + const ang = angle(i); + const x = c + Math.cos(ang) * (r + 13); + const y = c + Math.sin(ang) * (r + 13); + const anchor = Math.cos(ang) > 0.3 ? 'start' : Math.cos(ang) < -0.3 ? 'end' : 'middle'; + const dy = Math.sin(ang) < -0.3 ? -2 : Math.sin(ang) > 0.3 ? 9 : 4; + return ( + + {a.label} {Math.round(a.value)} + + ); + })} + {/* hit targets on top — comfortably larger than the dots, keyboard-reachable */} + {axes.map((a, i) => { + const [x, y] = pt(a.value, i); + return ( + setHover(i)} + onMouseLeave={() => setHover(null)} + onFocus={() => setHover(i)} + onBlur={() => setHover(null)} + /> + ); + })} + + {hover !== null && tip && ( +
+ {axes[hover].full ?? axes[hover].label} + {Math.round(axes[hover].value)} / 100 +
+ )} +
+ ); +} + +/* ------------------------------------------------------------------ */ +/* TradeChart — close path since entry + entry / stop / target levels */ +/* ------------------------------------------------------------------ */ + +export function TradeChart({ + direction, entry, stop, target, bars, openedAt, +}: { + direction: string; + entry: number; + /** Current stop level (trailing stop if active, else the initial stop). */ + stop: number; + target: number; + bars: OHLCVBar[]; + openedAt: string; +}) { + const openedDate = openedAt.slice(0, 10); + const closes = bars + .filter((b) => b.date.slice(0, 10) >= openedDate) + .map((b) => b.close); + const series = [entry, ...closes]; + if (series.length < 3) return null; + + const w = 560; const h = 150; + const padL = 8; const padR = 96; const padT = 10; const padB = 12; + const isShort = direction === 'short'; + const vals = [...series, stop, entry]; + let lo = Math.min(...vals); + let hi = Math.max(...vals); + const range = hi - lo || 1; + // Only stretch the scale to the target when it doesn't flatten the price action. + const targetFits = isShort ? lo - target <= range * 0.9 : target - hi <= range * 0.9; + if (targetFits) { lo = Math.min(lo, target); hi = Math.max(hi, target); } + const pad = (hi - lo) * 0.07; + lo -= pad; hi += pad; + const plotW = w - padL - padR; + const plotH = h - padT - padB; + const px = (i: number) => padL + (i / (series.length - 1)) * plotW; + const py = (v: number) => padT + plotH - ((v - lo) / (hi - lo)) * plotH; + const path = series.map((v, i) => `${i === 0 ? 'M' : 'L'}${px(i).toFixed(1)},${py(v).toFixed(1)}`).join(' '); + const last = series[series.length - 1]; + const perShare = isShort ? entry - last : last - entry; + const col = perShare >= 0 ? 'var(--up)' : 'var(--down)'; + const labelX = w - padR + 10; + const entryY = py(entry); + let stopY = py(stop); + if (Math.abs(stopY - entryY) < 11) stopY = entryY + (stopY >= entryY ? 11 : -11); + return ( + + + entry {fmt(entry)} + + stop {fmt(stop)} + {targetFits ? ( + <> + + target {fmt(target)} + + ) : ( + + target {fmt(target)} {isShort ? '↓' : '↑'} + + )} + + + + ); +} diff --git a/frontend/src/components/dashboard/OpenTradesPanel.tsx b/frontend/src/components/dashboard/OpenTradesPanel.tsx index 5fbf62f..3984953 100644 --- a/frontend/src/components/dashboard/OpenTradesPanel.tsx +++ b/frontend/src/components/dashboard/OpenTradesPanel.tsx @@ -1,170 +1,249 @@ -import { useMemo } from 'react'; +import { useMemo, useState } from 'react'; +import type { KeyboardEvent, ReactNode } from 'react'; import { Link } from 'react-router-dom'; +import { useQuery } from '@tanstack/react-query'; import { usePaperTrades, useClosePaperTrade, useExitPolicy } from '../../hooks/usePaperTrades'; import { useTickerNames } from '../../hooks/useTickers'; +import { getOHLCV } from '../../api/ohlcv'; import { tradePnl } from '../../lib/paperTrade'; import { formatPrice } from '../../lib/format'; +import { RBar, TradeChart } from '../charts/horizon'; import { Section } from '../ui/Section'; import { Callout } from '../ui/Callout'; +import type { PaperTrade } from '../../lib/types'; function money(v: number): string { const sign = v >= 0 ? '+' : '−'; return `${sign}$${Math.abs(v).toFixed(2)}`; } function pnlColor(v: number): string { - if (v > 0) return 'text-emerald-400'; - if (v < 0) return 'text-red-400'; + if (v > 0) return 'text-emerald-300'; + if (v < 0) return 'text-red-300'; return 'text-gray-300'; } +function DirTag({ direction }: { direction: string }) { + const isLong = direction === 'long'; + return ( + + {direction} + + ); +} + +function Detail({ label, value, valueClass = 'text-gray-100' }: { + label: string; + value: ReactNode; + valueClass?: string; +}) { + return ( +
+
{label}
+
{value}
+
+ ); +} + +/** Expanded row: full trade detail + price chart with entry/stop/target levels. */ +function TradeDetail({ trade, exitLabel, onClose, closing }: { + trade: PaperTrade; + exitLabel: string | null; + onClose: () => void; + closing: boolean; +}) { + const p = tradePnl(trade); + const stopLevel = trade.trailing_stop ?? trade.stop_loss; + const ohlcv = useQuery({ + queryKey: ['ohlcv', trade.symbol], + queryFn: () => getOHLCV(trade.symbol), + staleTime: 5 * 60_000, + }); + const opened = new Date(trade.opened_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); + return ( +
+
+ + + = 0 ? '+' : ''}${p.pct.toFixed(1)}%` : '—'} + valueClass={p ? pnlColor(p.pnl) : 'text-gray-500'} + /> + = 0 ? '+' : ''}${trade.alpha_pct.toFixed(1)}%${trade.alpha_usd != null ? ` · ${money(trade.alpha_usd)}` : ''}` + : '—' + } + valueClass={trade.alpha_pct != null ? pnlColor(trade.alpha_pct) : 'text-gray-500'} + /> + + {formatPrice(stopLevel)} + {trade.trailing_stop != null && ( + + trailing{trade.trailing_distance_pct != null ? ` · ${Math.abs(trade.trailing_distance_pct).toFixed(1)}% away` : ''} + + )} + + } + /> + + +
+ +
+
+ {ohlcv.data && ( +
+

+ since entry · entry / stop / target +

+
+ +
+
+ )} +
+ ); +} + export function OpenTradesPanel() { const { data: trades, isLoading } = usePaperTrades('open'); const { data: policy } = useExitPolicy(); const tickerNames = useTickerNames(); const close = useClosePaperTrade(); + const [expandedId, setExpandedId] = useState(null); const exitLabel = policy ? policy.mode === 'atr_trailing' - ? `auto-exit: ${(policy.atr_multiplier ?? 3).toFixed(1)}x ATR trail / ${policy.hold_days}d max` + ? `${(policy.atr_multiplier ?? 3).toFixed(1)}x ATR trailing stop / ${policy.hold_days}d max` : policy.mode === 'trailing' - ? `auto-exit: trailing ${Math.round(policy.trailing_pct)}%` + ? `trailing ${Math.round(policy.trailing_pct)}%` : policy.mode === 'time' - ? `auto-exit: ${policy.hold_days}d hold` - : 'auto-exit: target/stop' + ? `${policy.hold_days}d hold` + : 'target/stop' : null; - const totals = useMemo(() => { - let pnl = 0, winners = 0, losers = 0, priced = 0, alphaUsd = 0, alphaPriced = 0; - for (const t of trades ?? []) { + const rows = trades ?? []; + + const { totals, rMax } = useMemo(() => { + let pnl = 0, winners = 0, losers = 0, alphaUsd = 0, alphaPriced = 0, rMax = 1; + for (const t of rows) { const p = tradePnl(t); if (p) { - priced += 1; pnl += p.pnl; if (p.pnl > 0) winners += 1; else if (p.pnl < 0) losers += 1; + if (p.r != null) rMax = Math.max(rMax, Math.abs(p.r)); } - if (t.alpha_usd != null) { - alphaUsd += t.alpha_usd; - alphaPriced += 1; - } + if (t.alpha_usd != null) { alphaUsd += t.alpha_usd; alphaPriced += 1; } } - return { pnl, winners, losers, priced, alphaUsd, alphaPriced }; - }, [trades]); + return { totals: { pnl, winners, losers, alphaUsd, alphaPriced }, rMax }; + }, [rows]); if (isLoading) return null; - const rows = trades ?? []; return (
0 ? `${rows.length} open · ${totals.winners}▲ ${totals.losers}▼${exitLabel ? ` · ${exitLabel}` : ''}` : 'paper trading'} + title="Open Positions" + hint={rows.length > 0 + ? `${rows.length} open · ${totals.winners}▲ ${totals.losers}▼ · R-multiple to entry · select a row for detail` + : 'paper trading'} > {rows.length === 0 ? ( No open paper trades. Open a ticker and tap “Mark as taken” on a setup to start. ) : ( -
- - - - - - - - - - - - - - - - - - {rows.map((t) => { - const p = tradePnl(t); - return ( - - - - - - - - - - - - - - ); - })} - - - - - - - - -
TickerDirSharesEntryNowP&L%RAlphaTrail Stop
- +
+
    + {rows.map((t) => { + const p = tradePnl(t); + const open = expandedId === t.id; + return ( +
  • +
    setExpandedId(open ? null : t.id)} + onKeyDown={(e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setExpandedId(open ? null : t.id); + } + }} + aria-expanded={open} + className="grid w-full cursor-pointer grid-cols-[110px_1fr_60px_16px] items-center gap-3 rounded-lg px-2 py-2.5 text-left transition-colors hover:bg-white/[0.03] sm:grid-cols-[130px_150px_1fr_70px_16px]" + > + + e.stopPropagation()} + className="font-medium text-blue-300 transition-colors hover:text-blue-200" + > {t.symbol} - {tickerNames.get(t.symbol.toUpperCase()) && ( -
    - {tickerNames.get(t.symbol.toUpperCase())} -
    - )} -
- - {t.direction} - - {t.shares}{formatPrice(t.entry_price)} - {t.current_price != null ? formatPrice(t.current_price) : '—'} - - {p ? money(p.pnl) : '—'} - - {p ? `${p.pct >= 0 ? '+' : ''}${p.pct.toFixed(1)}%` : '—'} - + + + + {formatPrice(t.entry_price)} → {t.current_price != null ? formatPrice(t.current_price) : '—'} + + + {p?.r != null ? `${p.r >= 0 ? '+' : ''}${p.r.toFixed(2)}R` : '—'} - - {t.alpha_pct != null ? `${t.alpha_pct >= 0 ? '+' : ''}${t.alpha_pct.toFixed(1)}%` : '—'} - - {t.trailing_stop != null ? ( - <> - {formatPrice(t.trailing_stop)} - {t.trailing_distance_pct != null && ( - - {Math.abs(t.trailing_distance_pct).toFixed(1)}% - - )} - - ) : ( - - )} - - -
- Total unrealized P&L · alpha vs S&P 500 - - {money(totals.pnl)} - - 0 ? pnlColor(totals.alphaUsd) : 'text-gray-500'}`}> - {totals.alphaPriced > 0 ? money(totals.alphaUsd) : '—'} - -
+ + +
+ {open && ( + { + if (window.confirm(`Close ${t.shares} ${t.symbol} at the current price?`)) { + close.mutate({ id: t.id }); + } + }} + /> + )} + {/* company name for screen readers / row context */} + {tickerNames.get(t.symbol.toUpperCase()) ?? ''} + + ); + })} + +
+ total unrealized · alpha vs S&P 500{exitLabel ? ` · auto-exit: ${exitLabel}` : ''} + + {money(totals.pnl)} + 0 ? pnlColor(totals.alphaUsd) : 'text-gray-500'}`}> + {totals.alphaPriced > 0 ? money(totals.alphaUsd) : '—'} + + +
)} diff --git a/frontend/src/components/layout/AppShell.tsx b/frontend/src/components/layout/AppShell.tsx index 7bf8e59..1516d6b 100644 --- a/frontend/src/components/layout/AppShell.tsx +++ b/frontend/src/components/layout/AppShell.tsx @@ -5,6 +5,10 @@ import MobileNav from './MobileNav'; export default function AppShell() { return (
+