'use client'; import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import Link from 'next/link'; import type { FirmConfig, FirmState, AccountState, AccountConfig } from '@/types'; type SortKey = 'name' | 'balance' | 'dayPnL' | 'daysTraded' | 'target' | 'status'; type SortDir = 'asc' | 'desc'; function statusRank(account: AccountState): number { if (isAccountDead(account)) return 0; if (!account.active) return 1; if (!account.hasPosition) { if (account.targetHit) return 4; // Target Hit — most accomplished return 2; // Flat } return 3; // In Trade } function getAccountConfig(name: string, firm: FirmConfig): AccountConfig | undefined { return [...firm.accounts] .sort((a, b) => b.prefix.length - a.prefix.length) .find((a) => name.startsWith(a.prefix)); } function fmt(value: number) { return value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } /** Returns true when an account's balance has breached Tradovate's auto-liquidation floor. */ function isAccountDead(account: AccountState): boolean { return account.autoLiqThreshold > 0 && account.amount <= account.autoLiqThreshold; } function SettingsIcon() { return ( ); } function ChevronIcon({ open }: { open: boolean }) { return ( ); } function DetailsIcon() { return ( ); } function EyeIcon() { return ( ); } function EyeOffIcon() { return ( ); } function maskName(name: string, privacy: boolean): string { if (!privacy) return name; const visible = name.slice(0, 5); const hidden = name.slice(5); return hidden.length > 0 ? visible + '•'.repeat(hidden.length) : visible; } function SortHeader({ label, col, sortKey, sortDir, onSort }: { label: string; col: SortKey; sortKey: SortKey | null; sortDir: SortDir; onSort: (col: SortKey) => void; }) { const active = sortKey === col; return ( onSort(col)} >
{label} {active ? (sortDir === 'asc' ? '↑' : '↓') : '↕'}
); } function AccountRow({ account, firm, hideDead, privacy }: { account: AccountState; firm: FirmConfig; hideDead: boolean; privacy: boolean }) { const cfg = getAccountConfig(account.name, firm); const dead = isAccountDead(account); if (hideDead && dead) return null; const pnlColor = account.realizedPnL > 0 ? 'text-green-600 font-medium' : account.realizedPnL < 0 ? 'text-red-600 font-medium' : 'text-slate-400'; return (
{maskName(account.name, privacy)}
${fmt(account.amount)} {account.realizedPnL >= 0 ? '+' : ''}${fmt(account.realizedPnL)} {account.daysTraded} / {cfg?.minTradingDays ?? '—'} ${cfg?.profitTarget?.toLocaleString() ?? '—'}
{dead ? Dead : !account.active ? Hit DLL : account.hasPosition ? In Trade : account.targetHit ? Target Hit : Flat} e.stopPropagation()} >
); } function FirmRows({ state, firm, deleteMode, selected, onToggle, hideDead, privacy, sortKey, sortDir }: { state: FirmState; firm: FirmConfig; deleteMode: boolean; selected: boolean; onToggle: () => void; hideDead: boolean; privacy: boolean; sortKey: SortKey | null; sortDir: SortDir; }) { const [open, setOpen] = useState(true); const sortedAccounts = sortKey === null ? state.accounts : [...state.accounts].sort((a, b) => { const dir = sortDir === 'asc' ? 1 : -1; switch (sortKey) { case 'name': return dir * a.name.localeCompare(b.name); case 'balance': return dir * (a.amount - b.amount); case 'dayPnL': return dir * (a.realizedPnL - b.realizedPnL); case 'daysTraded': return dir * (a.daysTraded - b.daysTraded); case 'target': { const at = getAccountConfig(a.name, firm)?.profitTarget ?? 0; const bt = getAccountConfig(b.name, firm)?.profitTarget ?? 0; return dir * (at - bt); } case 'status': return dir * (statusRank(a) - statusRank(b)); default: return 0; } }); return ( <> setOpen((o) => !o)} >
{deleteMode && ( e.stopPropagation()} className="w-4 h-4 accent-red-500 cursor-pointer flex-shrink-0" /> )} {state.firm}
e.stopPropagation()} title="Settings" >
{open && ( state.accounts.length === 0 ? ( Waiting for data... ) : ( sortedAccounts.map((acc) => ( )) ) )} ); } interface SchedulerStatus { running: boolean; action: 'Buy' | 'Sell' | 'Random'; symbol: string; lastRun: string | null; } export default function Home() { const router = useRouter(); const [config, setConfig] = useState([]); const [firms, setFirms] = useState([]); const [deleteMode, setDeleteMode] = useState(false); const [selected, setSelected] = useState>(new Set()); const [hideDead, setHideDead] = useState(false); const [privacy, setPrivacy] = useState(false); const [sortKey, setSortKey] = useState(null); const [sortDir, setSortDir] = useState('asc'); // Trade controls const [scheduler, setScheduler] = useState({ running: false, action: 'Buy', symbol: 'NQ', lastRun: null }); const [enabledSymbols, setEnabledSymbols] = useState([]); const [tradeSymbol, setTradeSymbol] = useState(''); const [tradeAction, setTradeAction] = useState<'Buy' | 'Sell' | 'Random'>('Buy'); const [tradeLoading, setTradeLoading] = useState(false); const handleSort = (col: SortKey) => { if (sortKey === col) { setSortDir((d) => d === 'asc' ? 'desc' : 'asc'); } else { setSortKey(col); setSortDir('asc'); } }; const fetchConfig = async () => { try { const res = await fetch('/api/firms'); if (res.ok) { const data: FirmConfig[] = await res.json(); setConfig(data); } } catch { // server not running yet } }; const fetchScheduler = async () => { try { const res = await fetch('/api/auto-trade'); if (res.ok) setScheduler(await res.json()); } catch { /* ignore */ } }; useEffect(() => { fetch('/api/instruments') .then((r) => r.json() as Promise<{ symbol: string; enabled: boolean }[]>) .then((instruments) => { const syms = instruments.filter((i) => i.enabled).map((i) => i.symbol); setEnabledSymbols(syms); setTradeSymbol((prev) => prev || syms[0] || 'NQ'); }) .catch(() => {}); fetchConfig(); fetchScheduler(); const fetchState = async () => { try { const res = await fetch('/api/state'); if (res.ok) { const data: FirmState[] = await res.json(); setFirms(data); } } catch { // server not running yet } }; fetchState(); const stateInterval = setInterval(fetchState, 5000); const schedulerInterval = setInterval(fetchScheduler, 5000); return () => { clearInterval(stateInterval); clearInterval(schedulerInterval); }; }, []); const handleStart = async () => { setTradeLoading(true); try { await fetch('/api/trade', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: tradeAction, symbol: tradeSymbol }), }); await fetchScheduler(); } finally { setTradeLoading(false); } }; const handleStop = async () => { await fetch('/api/auto-trade', { method: 'DELETE' }); setScheduler((s) => ({ ...s, running: false, lastRun: null })); }; // Count dead accounts across all firms for the toggle button label const deadCount = firms.reduce((total, firmState) => { const firmCfg = config.find((c) => c.firm === firmState.firm); if (!firmCfg) return total; return total + firmState.accounts.filter((acc) => isAccountDead(acc)).length; }, 0); const handleConfirmDelete = async () => { await Promise.all( [...selected].map((id) => fetch(`/api/firms/${id}`, { method: 'DELETE' }).catch(() => {}) ) ); setSelected(new Set()); setDeleteMode(false); fetchConfig(); }; const toggleSelected = (id: number) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; return (

AutoTrader

{deleteMode ? ( <> {selected.size > 0 && ( )} ) : ( <> {deadCount > 0 && ( )} )}
{/* ── Trade Controls ── */}
{scheduler.running ? ( <> {scheduler.symbol} · {scheduler.action} {scheduler.lastRun && ( last run {new Date(scheduler.lastRun).toLocaleTimeString()} )} ) : ( <> Idle
)}
{config.length === 0 ? ( ) : ( config.map((cfg) => { const state = firms.find((f) => f.firm === cfg.firm) ?? { firm: cfg.firm, connected: false, accounts: [] }; return ( toggleSelected(cfg.id)} hideDead={hideDead} privacy={privacy} sortKey={sortKey} sortDir={sortDir} /> ); }) )}
No firms yet — click + Add New to get started
); }