SPB Git

spb/findata

Public
8.0 KB · 135 lines tsx
1"use client";2import Link from "next/link";3import { useEffect, useMemo, useState } from "react";4import { api, type Dataset, type Heatmap, type SessionRow } from "@/lib/api";5import { useAsync, useDebounced, useQueryParam } from "@/lib/hooks";6import { daysAgo, fmtCompact, fmtInt, fmtPct, fmtTime, today } from "@/lib/format";7import { Card, ErrorState, Field, PageHeader, Skeleton } from "@/components/ui";8import { DataTable } from "@/components/DataTable";9import { DateRange } from "@/components/DateRange";10import { CompletenessHeatmap, VolumeBarChart } from "@/components/charts";1112const DEFAULT_DS = "market_bars.stock.1m.raw";1314export default function CoveragePage() {15  const qDs = useQueryParam("dataset_id");16  const qSym = useQueryParam("symbol");17  const [datasetId, setDatasetId] = useState(DEFAULT_DS);18  const [start, setStart] = useState(daysAgo(120));19  const [end, setEnd] = useState(today());20  const [symbol, setSymbol] = useState("");21  const dsym = useDebounced(symbol.trim().toUpperCase(), 400);2223  useEffect(() => {24    if (qDs) setDatasetId(qDs);25  }, [qDs]);26  useEffect(() => {27    if (qSym) setSymbol(qSym.toUpperCase());28  }, [qSym]);2930  const datasets = useAsync((s) => api.get<{ data: Dataset[] }>("/catalog/datasets", undefined, s), []);31  const heat = useAsync((s) => api.get<Heatmap>("/coverage/heatmap", { dataset_id: datasetId, start, end }, s), [datasetId, start, end]);32  const interval = datasetId.includes(".5m.") ? "5m" : "1m";33  const sessions = useAsync((s) => api.get<{ data: SessionRow[]; meta: { dataset_id: string; symbol: string } }>("/market/sessions", { symbol: dsym, interval, start, end }, s), [dsym, interval, start, end], { enabled: dsym.length > 0 });3435  const barData = useMemo(() => (heat.data?.sessions ?? []).map((s) => ({ label: s.session_date, t: s.session_date, symbols: s.symbols, bars: s.bars })), [heat.data]);36  const totals = useMemo(() => {37    const ss = heat.data?.sessions ?? [];38    const bars = ss.reduce((a, s) => a + Number(s.bars ?? 0), 0);39    const comp = ss.filter((s) => s.completeness !== null);40    const avg = comp.length ? comp.reduce((a, s) => a + Number(s.completeness), 0) / comp.length : null;41    return { sessions: ss.length, bars, avg, maxSymbols: Math.max(0, ...ss.map((s) => s.symbols)) };42  }, [heat.data]);4344  const candidates = (datasets.data?.data ?? []).filter((d) => d.kind === "market_bars" || d.kind === "derived");4546  return (47    <>48      <PageHeader title="Coverage" subtitle="Sessions × completeness (bars ÷ expected regular-hours bars, capped at 100 %)" />49      <Card className="mb-4">50        <div className="flex flex-col gap-3 lg:flex-row lg:items-end lg:justify-between">51          <DateRange start={start} end={end} onChange={(s, e) => { setStart(s); setEnd(e); }} />52          <Field label="Dataset" className="lg:w-80">53            <select className="input font-mono text-xs" value={datasetId} onChange={(e) => setDatasetId(e.target.value)}>54              {!candidates.some((d) => d.id === datasetId) && <option value={datasetId}>{datasetId}</option>}55              {candidates.map((d) => (56                <option key={d.id} value={d.id}>57                  {d.id} {d.status !== "active" ? `(${d.status})` : ""}58                </option>59              ))}60            </select>61          </Field>62        </div>63      </Card>6465      {heat.error && <ErrorState error={heat.error} retry={heat.reload} />}66      <div className="mb-4 grid grid-cols-2 gap-3 md:grid-cols-4">67        <div className="card"><div className="text-xs text-ink-2">Sessions in range</div><div className="mt-1 text-2xl font-semibold">{fmtInt(totals.sessions)}</div></div>68        <div className="card"><div className="text-xs text-ink-2">Bars in range</div><div className="mt-1 text-2xl font-semibold" title={fmtInt(totals.bars)}>{fmtCompact(totals.bars)}</div></div>69        <div className="card"><div className="text-xs text-ink-2">Mean completeness</div><div className="mt-1 text-2xl font-semibold">{fmtPct(totals.avg)}</div></div>70        <div className="card"><div className="text-xs text-ink-2">Max symbols / session</div><div className="mt-1 text-2xl font-semibold">{fmtInt(totals.maxSymbols)}</div></div>71      </div>7273      <div className="grid gap-4 lg:grid-cols-[1fr_minmax(300px,0.7fr)]">74        <div className="flex flex-col gap-4">75          <Card title="Completeness per session" className={heat.loading ? "opacity-70" : undefined}>76            {heat.loading && !heat.data ? <Skeleton rows={4} /> : heat.data ? <CompletenessHeatmap cells={heat.data.sessions} start={start} end={end} /> : null}77          </Card>78          <Card title="Bars per session">79            {heat.data && barData.length > 0 ? <VolumeBarChart data={barData} valueKey="bars" title="bars" height={140} /> : <div className="text-sm text-ink-3">No sessions in range.</div>}80          </Card>81          <Card title="Symbols per session">82            {heat.data && barData.length > 0 ? <VolumeBarChart data={barData} valueKey="symbols" title="symbols" height={120} color="var(--series-3)" /> : <div className="text-sm text-ink-3">No sessions in range.</div>}83          </Card>84        </div>85        <Card title={`Top symbols by bars (${heat.data?.top_symbols.length ?? 0})`}>86          <DataTable87            rows={heat.data?.top_symbols}88            loading={heat.loading}89            rowKey={(r) => r.symbol}90            mobile="scroll"91            maxHeight="32rem"92            onRowClick={(r) => setSymbol(r.symbol)}93            columns={[94              { key: "symbol", header: "Symbol", render: (r) => <Link href={`/symbols/detail/?symbol=${encodeURIComponent(r.symbol)}`} className="font-mono font-medium text-accent-ink hover:underline" onClick={(e) => e.stopPropagation()}>{r.symbol}</Link> },95              { key: "sessions", header: "Sessions", num: true, render: (r) => fmtInt(r.sessions) },96              { key: "bars", header: "Bars", num: true, render: (r) => fmtCompact(r.bars) },97              { key: "completeness", header: "Compl.", num: true, render: (r) => fmtPct(r.completeness) },98            ]}99            empty="No coverage rows in this range."100          />101        </Card>102      </div>103104      <Card title="Per-symbol sessions" className="mt-4" actions={<span className="text-xs text-ink-3">click a top symbol or type one</span>}>105        <Field label="Symbol" className="mb-3 max-w-xs">106          <input className="input font-mono uppercase" placeholder="AAPL" value={symbol} onChange={(e) => setSymbol(e.target.value)} />107        </Field>108        {sessions.error && <ErrorState error={sessions.error} retry={sessions.reload} />}109        {dsym ? (110          <DataTable111            rows={sessions.data?.data}112            loading={sessions.loading}113            rowKey={(r) => r.session_date}114            mobile="scroll"115            maxHeight="28rem"116            columns={[117              { key: "session_date", header: "Session", render: (r) => <span className="tnum">{r.session_date}</span> },118              { key: "bar_count", header: "Bars", num: true, render: (r) => fmtInt(r.bar_count) },119              { key: "expected_bars", header: "Expected", num: true, render: (r) => fmtInt(r.expected_bars) },120              { key: "completeness", header: "Compl.", num: true, render: (r) => (r.expected_bars ? fmtPct(Math.min(1, r.bar_count / r.expected_bars)) : "—") },121              { key: "first_ts", header: "First (UTC)", render: (r) => <span className="tnum">{fmtTime(r.first_ts)}</span> },122              { key: "last_ts", header: "Last (UTC)", render: (r) => <span className="tnum">{fmtTime(r.last_ts)}</span> },123              { key: "flags", header: "Flags", render: (r) => (r.flags?.length ? r.flags.map((f) => <span key={f} className="pill mr-1 bg-warn-soft text-warn-ink">{f}</span>) : <span className="text-ink-3">—</span>) },124            ]}125            empty={`No sessions for ${dsym} in this range.`}126            footer={<span>{fmtInt(sessions.data?.data.length ?? 0)} sessions · dataset {sessions.data?.meta.dataset_id}</span>}127          />128        ) : (129          <div className="text-sm text-ink-3">Enter a symbol to list its sessions.</div>130        )}131      </Card>132    </>133  );134}135
Files · b31ccfb82046b1137ca27720f0467b178d4cebd6
Menu

Theme