// 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
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
| When | Spread | Legs |
R |
P&L |
Reason |
{closedPairs.length === 0 && (
|
No pair activity yet
|
)}
{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 (
| {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)
)}
{strats.map(s => {
const kpi = kpiMap.get(s.key) || { n_closed: 0, win_rate_pct: null, pnl_usd: 0, last_close: null };
const desc = STRATEGY_DESC[s.key] || '';
const lastClose = kpi.last_close ? new Date(kpi.last_close).toISOString().slice(0, 10) : '—';
return (
{/* Per-strategy KPI row */}
Closed trades
{kpi.n_closed}
Win rate
{kpi.win_rate_pct != null ? kpi.win_rate_pct.toFixed(1) + '%' : '—'}
Risk/trade
${s.risk_usd.toFixed(0)}
{/* Symbol/spread list */}
Targets
{s.symbols.length === 0 ? (
— none configured —
) : (
{s.symbols.map(sym => (
{sym}
))}
)}
{/* Total P&L */}
Total P&L (closed)
Last close · {lastClose}
= 0 ? 'var(--pos)' : 'var(--neg)',
}}>
{fmtSigned(kpi.pnl_usd)}
);
})}
{/* Pair sub-cards */}
{(kpis?.strategies || []).filter(s => s.kind === 'pair').map(s => (
{s.label}
Pair sub-stream
WR
{s.win_rate_pct != null ? s.win_rate_pct.toFixed(0) + '%' : '—'}
P&L
= 0 ? 'pos' : 'neg'}`}>{fmtSigned(s.pnl_usd)}
))}
>
);
}
// ───────────────────────────── stub pages ─────────────────────────────
function ComingSoonPage({ title, sub, hint }) {
return (
<>
Coming soon
{title}
{hint}
>
);
}
function PositionsPage({ accent }) {
const positions = useApi('/api/positions', 5000);
const pairs = useApi('/api/pair_positions', 5000);
const open = positions || [];
const openPairs = pairs?.open || [];
return (
<>
Positions
{open.length} single-leg + {openPairs.length} pair
Single-leg ({open.length})
{open.length === 0 &&
None open
}
{open.map((p, i) => (
{p.side === 'LONG' ? 'BUY' : 'SELL'}{p.symbol}
{p.lots} lots · @ {p.entry_price?.toFixed(5)} · {p.duration_min}m
= 0 ? 'pos' : 'neg'}`}>{fmtSigned(p.unrealized_pnl)}
))}
Pairs ({openPairs.length})
{openPairs.length === 0 &&
None open
}
{openPairs.map((p, i) => (
{p.spread_name}
{p.direction_a === 'long' ? 'BUY' : 'SELL'} {p.symbol_a} {p.lot_size_a} · {p.direction_b === 'long' ? 'BUY' : 'SELL'} {p.symbol_b} {p.lot_size_b}
entry z {p.entry_z?.toFixed(2)} · {p.bars_held} bars · #{p.pair_id}
spread
))}
>
);
}
function JournalPage() {
const recent = useApi('/api/signals/history', 30000);
const rows = recent || [];
return (
<>
Journal
Recent signal records · {rows.length} entries
| Time | Strategy | Symbol | Side |
Entry |
Decision |
P&L |
Close reason |
{rows.length === 0 && | No signal records yet |
}
{rows.map((r, i) => (
| {r.signal_time ? r.signal_time.slice(5, 16).replace('T', ' ') : ''} |
{r.strategy} |
{r.symbol} |
{r.side} |
{r.actual_entry_price?.toFixed(5) || r.entry_price?.toFixed(5)} |
{r.hitl_decision || '—'} |
0 ? 'pos' : r.pnl_usd < 0 ? 'neg' : ''}>{r.pnl_usd != null ? fmtSigned(r.pnl_usd) : '—'} |
{r.close_reason || '—'} |
))}
>
);
}
// ───────────────────────────── app shell ─────────────────────────────
const ACCENT = '#3b82f6';
function _initialTab() {
// Derive initial tab from URL path so /strategy, /journal, /positions work.
const path = (typeof window !== 'undefined' ? window.location.pathname : '/').replace(/\/$/, '');
if (path === '/portfolio') return 'portfolio';
if (path === '/strategy') return 'strategy';
if (path === '/journal') return 'journal';
if (path === '/positions') return 'positions';
if (path === '/tfsa') return 'tfsa';
if (path === '/margin') return 'margin';
return 'dashboard';
}
function App() {
const [active, setActive] = useState(_initialTab());
const [collapsed, setCollapsed] = useState(false);
const [theme, setTheme] = useState('light');
const [mobNavOpen, setMobNavOpen] = useState(false);
// Keep the URL in sync with tab changes (no full reload).
useEffect(() => {
const path = active === 'dashboard' ? '/' : '/' + active;
if (window.location.pathname !== path) {
window.history.replaceState({}, '', path);
}
}, [active]);
// Close the mobile drawer whenever the active tab changes.
useEffect(() => { setMobNavOpen(false); }, [active]);
let Page;
switch (active) {
case 'portfolio': Page = ; break;
case 'strategy': Page = ; break;
case 'journal': Page = ; break;
case 'positions': Page = ; break;
case 'tfsa': Page = ; break;
case 'margin': Page = ; break;
default: Page = ;
}
return (
setMobNavOpen(false)} aria-hidden="true" />
setMobNavOpen(false)} />
);
}
ReactDOM.createRoot(document.getElementById('root')).render(
);
// ───────────────────────────── portfolio page ─────────────────────────────
function ParamGrid({ params }) {
return (
{Object.entries(params || {}).map(([k, v]) => (
{k.replace(/_/g, " ")}{" "}
{String(v)}
))}
);
}
function ConfigRow({ label, value, sub }) {
return (
{label}
{value}
{sub && {sub}
}
);
}
function SectionLabel({ text }) {
return (
);
}
function PortfolioPage({ accent }) {
const portfolio = useApi("/api/portfolio", 15000);
const ftmo = useApi("/api/ftmo", 5000);
if (!portfolio) {
return
;
}
const cfg = portfolio.config || {};
const m = portfolio.metrics || {};
const insts = portfolio.instruments || [];
const pairs = insts.filter(i => i.kind === "pair");
const single = insts.filter(i => i.kind === "single_leg");
const bands = cfg.dd_bands || [];
const gm = cfg.kl_grade_multipliers || {};
const ddPct = ftmo?.total_dd_used_pct ?? 0;
const curBand = bands.find(b => ddPct >= b.lo_pct && (b.hi_pct == null || ddPct < b.hi_pct));
const curMult = curBand ? (curBand.mult == null ? "HALT" : curBand.mult.toFixed(2) + "×") : "1.00×";
const dailyHead = (ftmo?.daily_loss_limit_usd ?? 0) - (ftmo?.daily_loss_used_usd ?? 0);
const ddHead = (ftmo?.total_dd_limit_usd ?? 0) - (ftmo?.total_dd_used_usd ?? 0);
const metric = (lbl, val, sub) => (
{lbl}
{val}
{sub &&
{sub}
}
);
return (
<>
Portfolio
Live parameters & risk config · read from .env + portfolio_guard
Live exposure
Concurrency, drawdown band & headroom — live
{metric("Open pairs", (m.open_pairs ?? 0) + " / " + (m.max_pairs ?? 3))}
{metric("Open single-leg", (m.open_single_leg ?? 0) + " / " + (m.max_single_leg ?? 5))}
{metric("DD-band size", curMult, "at " + ddPct.toFixed(2) + "% DD")}
{metric("Daily-loss buffer", fmt$0(dailyHead))}
{metric("Total-DD buffer", fmt$0(ddHead))}
{metric("HITL", cfg.hitl_enabled ? "ON" : "OFF (auto)")}
Risk configuration
PortfolioRiskGuard + FTMO limits
Drawdown sizing ladder
Risk multiplier by total-drawdown band · 8% = hard halt
{bands.map((b, i) => {
const active = curBand === b;
const halt = b.mult == null;
return (
{b.lo_pct}%{b.hi_pct == null ? "+" : "–" + b.hi_pct + "%"} DD
{halt ? "HALT" : b.mult.toFixed(2) + "×"}
);
})}
{pairs.length > 0 &&
}
{pairs.map(p => (
{p.name}
{p.strategy} · {p.tier || "—"} · ${p.risk_usd.toFixed(0)}/trade
))}
{single.length > 0 &&
}
{single.map(s => (
{s.name}
{s.strategy} · ${s.risk_usd.toFixed(0)}/trade
))}
>
);
}