spb/crypto-trade-bot
Public
Python 54.7%
TypeScript 42.5%
SQL 1.9%
1"use client";2import { useQuery } from "@tanstack/react-query";3import Link from "next/link";4import { useRiskStatus, useSystemStatus } from "@/components/shell";5import { Badge, Card, Dot, Empty, ErrorBox, KV, PageHeader, Spinner, Table, Td, Th, Tile } from "@/components/ui";6import { api } from "@/lib/api";7import { fmtAge, fmtBps, fmtNum, fmtPct, fmtPctPlain, fmtPrice, fmtUsd, signClass } from "@/lib/format";8import type { OverviewSymbol, RunRow } from "@/lib/types";910export default function OverviewPage() {11 const sys = useSystemStatus();12 const risk = useRiskStatus();13 const ov = useQuery({ queryKey: ["overview"], queryFn: () => api<{ symbols: OverviewSymbol[] }>("/api/market/overview"), refetchInterval: 5_000 });14 const runs = useQuery({ queryKey: ["runs", 5], queryFn: () => api<{ items: RunRow[] }>("/api/backtests?limit=5"), refetchInterval: 10_000 });15 const s = sys.data;16 return (17 <>18 <PageHeader title="Overview" subtitle="Portfolio, universe, connectivity and risk posture at a glance." />19 <div className="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-6">20 <Tile label="Total equity" value="—" sub="paper ledger not started (Phase 4)" tone="muted" />21 <Tile label="Cash" value="—" sub="paper ledger not started (Phase 4)" tone="muted" />22 <Tile label="Realized / unrealized P&L" value="—" sub="no fills yet" tone="muted" />23 <Tile label="Gross exposure" value={fmtUsd(0)} sub="no positions" tone="muted" />24 <Tile label="Trading mode" value={<span className="uppercase">{s?.mode ?? "…"}</span>} sub={s?.live_trading_enabled ? "live flag ON" : "live flag off"} tone={s?.mode === "live" ? "down" : "muted"} />25 <Tile label="Kill switch" value={risk.data ? (risk.data.kill_switch.active ? "ACTIVE" : "armed") : "…"} sub={risk.data?.kill_switch.reason ?? "blocks all new orders when active"} tone={risk.data?.kill_switch.active ? "down" : "up"} />26 </div>2728 <div className="mt-4 grid gap-4 xl:grid-cols-3">29 <Card title="Universe" subtitle="Live ticker from the Kraken WebSocket; realized vol from stored hourly bars." className="xl:col-span-2">30 {ov.isLoading && <Spinner />}31 {ov.error && <ErrorBox error={ov.error} />}32 {ov.data && (33 <Table>34 <thead>35 <tr>36 <Th>Symbol</Th><Th align="right">Last</Th><Th align="right">24h</Th><Th align="right">Bid / Ask</Th><Th align="right">Spread</Th><Th align="right">24h volume</Th><Th align="right">RV 7d (ann.)</Th><Th align="right">1h bars</Th><Th>Data</Th>37 </tr>38 </thead>39 <tbody>40 {ov.data.symbols.map((r) => (41 <tr key={r.symbol} className="hover:bg-bg-sub">42 <Td><Link href={`/market?symbol=${encodeURIComponent(r.symbol)}`} className="font-medium text-accent hover:underline">{r.symbol}</Link></Td>43 <Td align="right" num>{fmtPrice(r.last)}</Td>44 <Td align="right" num className={signClass(r.change_pct)}>{fmtPct(r.change_pct, 2, true)}</Td>45 <Td align="right" num>{fmtPrice(r.bid)} / {fmtPrice(r.ask)}</Td>46 <Td align="right" num>{fmtBps(r.spread_bps, 2)}</Td>47 <Td align="right" num>{fmtNum(r.volume, 0)}</Td>48 <Td align="right" num>{fmtPctPlain(r.rv_7d_annualized)}</Td>49 <Td align="right" num>{r.hourly_bars}</Td>50 <Td>{r.stale ? <Badge tone="warn">stale {fmtAge(r.age_s)}</Badge> : <Badge tone="up">live {fmtAge(r.age_s)}</Badge>}</Td>51 </tr>52 ))}53 </tbody>54 </Table>55 )}56 </Card>57 <div className="flex flex-col gap-4">58 <Card title="Exchange connectivity">59 {sys.isLoading && <Spinner />}60 {sys.error && <ErrorBox error={sys.error} />}61 {s && (62 <KV items={[63 ["Kraken status", <span key="k" className="flex items-center gap-1.5"><Dot ok={s.kraken.status === "online"} /> {s.kraken.status}{s.kraken.error ? ` — ${s.kraken.error}` : ""}</span>],64 ["REST round-trip", s.kraken.rtt_ms != null ? `${s.kraken.rtt_ms} ms` : "—"],65 ["Clock drift", s.kraken.clock_drift_s != null ? `${s.kraken.clock_drift_s.toFixed(2)} s` : "—"],66 ["WebSocket", s.stream ? <span key="w" className="flex items-center gap-1.5"><Dot ok={s.stream.connected} /> {s.stream.connected ? "connected" : "disconnected"} · {s.stream.messages.toLocaleString()} msgs · {s.stream.reconnects} reconnects · {s.stream.checksum_failures} checksum fails</span> : "worker not reporting"],67 ["Last message", s.stream?.last_msg_age != null ? `${s.stream.last_msg_age}s ago` : "—"],68 ["Data key", s.credentials.data_key ? "configured" : "not configured"],69 ["Trade key", s.credentials.trade_key ? "configured" : "not configured"],70 ]} />71 )}72 </Card>73 <Card title="Active strategies" subtitle="Paper/live strategy runners arrive in Phase 4.">74 <Empty title="No strategies running">Backtests run on demand from the Strategy Lab; there is no scheduled execution yet.</Empty>75 </Card>76 <Card title="Risk limits">77 {risk.data && (78 <KV items={[79 ["Max order notional", fmtUsd(risk.data.limits.max_order_notional, 0)],80 ["Max position notional", fmtUsd(risk.data.limits.max_position_notional, 0)],81 ["Max gross exposure", `${risk.data.limits.max_gross_exposure}×`],82 ["Daily loss limit", fmtPctPlain(risk.data.limits.daily_loss_limit)],83 ["Max drawdown", fmtPctPlain(risk.data.limits.max_drawdown_limit)],84 ["Stale data cutoff", `${risk.data.limits.stale_data_seconds}s`],85 ]} />86 )}87 <Link href="/risk" className="mt-3 inline-block text-xs text-accent hover:underline">Open Risk Center →</Link>88 </Card>89 </div>90 </div>91 <Card title="Recent backtests" className="mt-4" actions={<Link href="/backtests" className="text-xs text-accent hover:underline">All runs →</Link>}>92 {runs.data?.items.length ? (93 <Table>94 <thead><tr><Th>Status</Th><Th>Strategy</Th><Th>Symbol</Th><Th align="right">Interval</Th><Th align="right">Sharpe</Th><Th align="right">Return</Th><Th align="right">Max DD</Th></tr></thead>95 <tbody>96 {runs.data.items.map((r) => (97 <tr key={r.id} className="hover:bg-bg-sub">98 <Td><Link href={`/backtests/${r.id}`} className="text-accent hover:underline"><StatusBadge status={r.status} /></Link></Td>99 <Td>{r.strategy}</Td><Td>{r.symbol}</Td><Td align="right" num>{r.interval_min}m</Td>100 <Td align="right" num>{fmtNum(r.sharpe)}</Td><Td align="right" num className={signClass(r.total_return)}>{fmtPct(r.total_return)}</Td><Td align="right" num className="text-down">{fmtPct(r.max_drawdown)}</Td>101 </tr>102 ))}103 </tbody>104 </Table>105 ) : <Empty title="No backtests yet">Launch one from the Strategy Lab.</Empty>}106 </Card>107 </>108 );109}110111export function StatusBadge({ status }: { status: string }) {112 const tone = status === "done" ? "up" : status === "failed" ? "down" : status === "running" ? "accent" : "muted";113 return <Badge tone={tone}>{status}</Badge>;114}115