SPB Git forge
1commits 1branches 0releases
189.0 KBsize
maindefault branch
6 h agolast push
Python 54.7% TypeScript 42.5% SQL 1.9%
7.4 KB · 115 lines tsx
Raw Blame History
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