// app.jsx — DreamyStacks Live Trading Room // Bento Soft layout (per Marketwise design handoff), wired to /api endpoints. const { useState, useEffect, useMemo } = React; // ───────────────────────────── helpers ───────────────────────────── const fmt$ = (v) => '$' + Number(v || 0).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2, }); const fmt$0 = (v) => '$' + Number(v || 0).toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0, }); const fmtPct = (v) => Number(v || 0).toFixed(2) + '%'; const fmtSigned = (v, d = 2) => (v >= 0 ? '+' : '−') + fmt$0(Math.abs(v)).slice(1); const titleCase = (s) => (s || '').charAt(0).toUpperCase() + (s || '').slice(1).toLowerCase(); async function api(path) { try { const r = await fetch(path, { credentials: 'same-origin' }); if (!r.ok) throw new Error(r.statusText); return await r.json(); } catch (e) { console.warn('api', path, e); return null; } } function useApi(path, intervalMs = 15000) { const [data, setData] = useState(null); useEffect(() => { let alive = true; const tick = async () => { const d = await api(path); if (alive && d !== null) setData(d); }; tick(); const id = setInterval(tick, intervalMs); return () => { alive = false; clearInterval(id); }; }, [path, intervalMs]); return data; } // ───────────────────────────── icons ───────────────────────────── const Icon = { Dashboard: ({ s = 16 }) => ( ), Award: ({ s = 16 }) => ( ), Bank: ({ s = 16 }) => ( ), ChevronRight: ({ s = 12 }) => ( ), Settings: ({ s = 16 }) => ( ), Sun: ({ s = 16 }) => ( ), Moon: ({ s = 16 }) => ( ), Plus: ({ s = 13 }) => ( ), Search: ({ s = 14 }) => ( ), Bell: ({ s = 14 }) => ( ), More: ({ s = 14 }) => ( ), Menu: ({ s = 20 }) => ( ), Dot: ({ color = 'currentColor' }) => , }; // ───────────────────────────── sidebar ───────────────────────────── function Sidebar({ active, setActive, collapsed, setCollapsed, closeMobNav }) { const [open, setOpen] = useState({ ftmo: true, ibkr: false }); const toggle = (gid) => setOpen(o => ({ ...o, [gid]: !o[gid] })); const groups = [ { id: 'ftmo', label: 'FTMO Demo', icon: , children: [ { id: 'dashboard', label: 'Dashboard' }, { id: 'portfolio', label: 'Portfolio' }, { id: 'strategy', label: 'Strategy' }, { id: 'journal', label: 'Journal' }, { id: 'positions', label: 'Positions' }, ]}, { id: 'ibkr', label: 'IBKR', icon: , soon: true, children: [ { id: 'tfsa', label: 'TFSA' }, { id: 'margin', label: 'Margin' }, ]}, { id: 'signal', label: 'Signal Trader', icon: , href: '/signal-trader', children: [] }, ]; const onGroupClick = (gid) => { const g = groups.find(x => x.id === gid); if (g && g.href) { window.location.href = g.href; return; } if (collapsed) { setCollapsed(false); setOpen(o => ({ ...o, [gid]: true })); } else toggle(gid); }; return ( ); } // ───────────────────────────── theme toggle ───────────────────────────── function ThemeToggle({ mode, onChange }) { return (
{['light', 'dark'].map(m => ( ))}
); } // ───────────────────────────── equity sparkline ───────────────────────────── function EquityLine({ dates, equity, h = 200, accent }) { if (!dates || dates.length < 2) { return
Waiting for first closed trade…
; } const min = Math.min(...equity), max = Math.max(...equity); const r = max - min || 1; const w = 600; const pts = equity.map((v, i) => [(i / (equity.length - 1)) * w, h - 18 - ((v - min) / r) * (h - 40)]); let p = `M ${pts[0][0]} ${pts[0][1]}`; for (let i = 1; i < pts.length; i++) { const [x1, y1] = pts[i - 1], [x2, y2] = pts[i], cx = (x1 + x2) / 2; p += ` C ${cx} ${y1}, ${cx} ${y2}, ${x2} ${y2}`; } const grad = `url(#eqg)`; return ( ); } // ───────────────────────────── dashboard page ───────────────────────────── const STATUS_PILL = { LIVE: { bg: 'rgba(16,167,114,0.14)', col: 'var(--pos)', label: 'LIVE' }, SHADOW: { bg: 'rgba(245,158,11,0.16)', col: '#a16207', label: 'SHADOW' }, OFF: { bg: 'var(--bg-3)', col: 'var(--ink-3)', label: 'OFF' }, }; function StatusPill({ status }) { const s = STATUS_PILL[status] || STATUS_PILL.OFF; return ( {s.label} ); } function DashboardPage({ accent }) { const account = useApi('/api/account', 5000); const ftmo = useApi('/api/ftmo', 5000); const equity = useApi('/api/equity', 30000); const positions = useApi('/api/positions', 5000); const live = useApi('/api/live_state', 30000); const pairs = useApi('/api/pair_positions', 10000); const balance = account?.balance ?? 50000; const equityNow = account?.equity ?? 50000; const floating = account?.floating_pnl ?? 0; const freeMargin = account?.free_margin ?? 50000; const connected = account?.connected ?? false; const initial = ftmo?.initial_balance ?? 50000; const profitProgress = ftmo?.profit_progress_pct ?? 0; const profitTarget = ftmo?.profit_target_usd ?? 5000; const profitEarned = ftmo?.current_profit_usd ?? 0; const profitRemain = Math.max(0, profitTarget - profitEarned); const dailyUsedPct = ftmo?.daily_loss_used_pct ?? 0; const dailyUsed = ftmo?.daily_loss_used_usd ?? 0; const dailyLimit = ftmo?.daily_loss_limit_usd ?? 2250; const totalDdPct = ftmo?.total_dd_used_pct ?? 0; const totalDd = ftmo?.total_dd_used_usd ?? 0; const totalDdLimit = ftmo?.total_dd_limit_usd ?? 4500; const tradingDays = ftmo?.trading_days ?? 0; const strats = live?.strategies ?? []; const openPairs = pairs?.open ?? []; const closedPairs = pairs?.closed_recent ?? []; return ( <>

Live Trading Room

FTMO-Demo · account {(live?.ftmo_demo?.account_id || '').slice(0, 8)}… {' · '} {connected ? '● connected' : '● disconnected'} {' · bot '} {live?.bot_active ? 'running' : 'stopped'}
{/* Hero — total equity */}
Total equity
{fmt$(equityNow)}
{fmtSigned(equityNow - initial)}  ·  {((equityNow - initial) / initial * 100).toFixed(2)}%  since account start
Balance
{fmt$0(balance)}
Free margin
${(freeMargin / 1000).toFixed(1)}k
Floating
{fmtSigned(floating)}
Trading days
{tradingDays}
{/* FTMO progress */}

FTMO progress

Phase 1 demo · no real risk
{[ { name: 'Profit target', stat: fmtPct(profitProgress / 10), pct: profitProgress, sub: fmt$0(profitEarned) + ' earned', sub2: fmt$0(profitRemain) + ' to go', kind: 'pos' }, { name: 'Daily loss', stat: fmtPct(dailyUsedPct), pct: (dailyUsedPct / (ftmo?.daily_loss_limit_pct || 4.5)) * 100, sub: fmt$0(dailyUsed) + ' used', sub2: fmt$0(dailyLimit - dailyUsed) + ' buffer', kind: 'warn' }, { name: 'Overall loss', stat: fmtPct(totalDdPct), pct: (totalDdPct / (ftmo?.total_dd_limit_pct || 9)) * 100, sub: fmt$0(totalDd) + ' used', sub2: fmt$0(totalDdLimit - totalDd) + ' buffer', kind: '' }, ].map((o, i) => (
{o.name}{o.stat}
{o.sub}{o.sub2}
))}
{/* Equity curve */}

Equity curve

Cumulative closed-trade P&L
{/* Active strategies */}

Active strategies

{strats.filter(s => s.status === 'LIVE').length} live · {strats.filter(s => s.status === 'SHADOW').length} shadow
{strats.map(s => (
{s.label}
{s.symbols.length ? s.symbols.join(', ') : '—'}
${s.risk_usd.toFixed(0)}
))}
{/* Open positions (single-leg) */}

Open positions

{(positions || []).length} active · floating {fmtSigned(floating)}
{(positions || []).length === 0 && (
No open single-leg positions
)} {(positions || []).map((p, i) => (
{p.side === 'LONG' ? 'BUY' : 'SELL'} {p.symbol}
{p.lots} lots · @ {p.entry_price?.toFixed(5)} · {p.duration_min}m
SL {p.stop_loss?.toFixed(5) || '—'} · TP {p.take_profit?.toFixed(5) || '—'}
= 0 ? 'pos' : 'neg'}`}>{fmtSigned(p.unrealized_pnl)}
))}
{/* Open pairs */}

Open pair positions

{openPairs.length} active · max {live?.max_concurrent_pairs ?? 3} concurrent
{openPairs.length === 0 && (
No open pair positions
)} {openPairs.map((p, i) => (
{p.spread_name} id {p.pair_id} · {p.bars_held} bars
{p.direction_a.toUpperCase()} {' '}{p.symbol_a} · {p.lot_size_a.toFixed(2)} lots
{p.direction_b.toUpperCase()} {' '}{p.symbol_b} · {p.lot_size_b.toFixed(2)} lots
z entry {p.entry_z?.toFixed(2)} · stop {p.stop_z?.toFixed(2)} · exit {p.exit_z?.toFixed(2)} · β {p.hedge_ratio?.toFixed(3)}
))}
{/* Recent closed pairs */}

Recent pair activity

Last 20 records · open / closed / partial-fill forensics
{closedPairs.length === 0 && ( )} {closedPairs.map((p, i) => { const opened = p.opened_at ? new Date(p.opened_at) : null; const r = p.r_realized; const pnl = p.pnl_usd; const isForensic = (p.exit_reason || '').startsWith('leg_a_failed') || (p.exit_reason || '').startsWith('partial_fill'); return ( ); })}
WhenSpreadLegs R P&L Reason
No pair activity yet
{opened ? opened.toISOString().slice(5, 16).replace('T', ' ') : '—'} {p.spread_name} {(p.direction_a || '').toUpperCase()} {p.symbol_a} {' / '} {(p.direction_b || '').toUpperCase()} {p.symbol_b} 0 ? 'pos' : r < 0 ? 'neg' : ''}> {r != null ? (r >= 0 ? '+' : '') + r.toFixed(2) + 'R' : '—'} 0 ? 'pos' : pnl < 0 ? 'neg' : ''}> {pnl != null ? fmtSigned(pnl) : '—'} {p.exit_reason || '—'}
); } // ───────────────────────────── strategy page ───────────────────────────── const STRATEGY_DESC = { 'BOS+FVG': '4H BOS + 15M FVG triple-confirmation reversal — only AUDJPY survived the 2026-05 audit.', 'KL_S5': 'Koroush "S5" level breakout on 4H — halted 2026-05-26 after lookahead fix (all 3 pairs Tier 3/0).', 'ARB': 'Asian range → London breakout with EMA50 trend filter. Halted 2026-05-26 after EMA lookahead fix.', 'LNYR': 'London-NY reversal — fades the London move with z-thresholded entry. GBPUSD-only.', 'PAIRS': 'Statsmodels-coint mean-reversion on cointegrated spreads. AUDUSD/NZDUSD, EURJPY/GBPJPY, EURUSD/USDCHF.', }; function StrategyPage({ accent }) { const live = useApi('/api/live_state', 15000); const kpis = useApi('/api/strategies_live', 15000); const pairs = useApi('/api/pair_positions', 15000); const [report, setReport] = useState(null); if (!live) { return

Strategy

; } const strats = live.strategies || []; const kpiMap = new Map((kpis?.strategies || []).map(s => [s.key, s])); return ( <>

Strategy

Active playbooks · live state read from .env + signals.db
{report && (
{report === 'trust-report' ? 'Backtest Trust Report' : 'Trade Explorer'} · preview (illustrative data)