SPB Git

spb/findata

Public
5.9 KB · 137 lines tsx
1"use client";2import Link from "next/link";3import { ArrowLeft } from "lucide-react";4import { api, type InstrumentDetail } from "@/lib/api";5import { useAsync, useQueryParam } from "@/lib/hooks";6import { fmtCompact, fmtInt } from "@/lib/format";7import { Card, Empty, ErrorState, KV, PageHeader, Skeleton, StatusBadge } from "@/components/ui";8import { DataTable } from "@/components/DataTable";9import { JsonView } from "@/components/JsonView";1011export default function SymbolDetailPage() {12  const symbol = useQueryParam("symbol")?.toUpperCase() ?? null;13  const { data, error, loading, reload } = useAsync((s) => api.get<InstrumentDetail>(`/instruments/${encodeURIComponent(symbol!)}`, undefined, s), [symbol], { enabled: !!symbol });14  const inst = data?.instrument;1516  return (17    <>18      <Link href="/symbols/" className="mb-2 inline-flex items-center gap-1 text-xs text-ink-2 hover:text-ink-1">19        <ArrowLeft className="h-3.5 w-3.5" /> Symbols20      </Link>21      <PageHeader22        title={symbol ?? "Symbol"}23        subtitle={inst?.name ?? (data && !inst ? "Unknown instrument (no master record) — coverage may still exist" : undefined)}24        actions={25          symbol && (26            <>27              {inst && <StatusBadge status={inst.status} />}28              <Link href={`/bars/?symbol=${encodeURIComponent(symbol)}`} className="btn btn-sm btn-primary">29                Bars30              </Link>31              <Link href={`/coverage/?symbol=${encodeURIComponent(symbol)}`} className="btn btn-sm">32                Sessions33              </Link>34              <Link href={`/fundamentals/?ticker=${encodeURIComponent(symbol)}`} className="btn btn-sm">35                Fundamentals36              </Link>37              <Link href={`/playground/?symbol=${encodeURIComponent(symbol)}`} className="btn btn-sm">38                SQL39              </Link>40            </>41          )42        }43      />44      {!symbol && <Empty>Missing <code>?symbol=</code> parameter.</Empty>}45      {error && <ErrorState error={error} retry={reload} />}46      {loading && !data && <Skeleton rows={8} />}47      {data && (48        <div className="grid gap-4 lg:grid-cols-2">49          <Card title="Instrument">50            {inst ? (51              <KV52                items={[53                  ["Instrument id", <code key="i" className="text-xs">{inst.instrument_id}</code>],54                  ["Asset class", inst.asset_class],55                  ["Currency", inst.currency ?? "—"],56                  ["First date", inst.first_date ?? "—"],57                  ["Last date", inst.last_date ?? "—"],58                  ...Object.entries(inst)59                    .filter(([k]) => !["instrument_id", "symbol", "name", "asset_class", "status", "first_date", "last_date", "currency"].includes(k))60                    .map(([k, v]) => [k, typeof v === "object" && v !== null ? <JsonView key={k} value={v} collapsed /> : String(v ?? "—")] as [string, React.ReactNode]),61                ]}62              />63            ) : (64              <Empty>No master record.</Empty>65            )}66          </Card>67          <Card title="Fundamentals">68            {data.fundamentals && data.fundamentals.facts > 0 ? (69              <KV70                items={[71                  ["Facts (current)", fmtInt(data.fundamentals.facts)],72                  ["Last fiscal period", data.fundamentals.last_period ?? "—"],73                  ["Last filing", data.fundamentals.last_filing ?? "—"],74                ]}75              />76            ) : (77              <Empty>78                No fundamentals ingested for <code>frd:{symbol}</code>.79              </Empty>80            )}81          </Card>82          <Card title={`Coverage per dataset (${data.coverage.length})`} className="lg:col-span-2">83            <DataTable84              rows={data.coverage}85              rowKey={(c) => c.dataset_id}86              mobile="scroll"87              columns={[88                {89                  key: "dataset_id",90                  header: "Dataset",91                  render: (c) => (92                    <Link href={`/datasets/detail/?id=${encodeURIComponent(c.dataset_id)}`} className="font-mono text-xs text-accent-ink hover:underline">93                      {c.dataset_id}94                    </Link>95                  ),96                },97                { key: "sessions", header: "Sessions", num: true, render: (c) => fmtInt(c.sessions) },98                { key: "bars", header: "Bars", num: true, render: (c) => <span title={fmtInt(c.bars)}>{fmtCompact(c.bars)}</span> },99                { key: "d0", header: "From" },100                { key: "d1", header: "To" },101              ]}102              empty="No bars for this symbol yet."103            />104          </Card>105          <Card title={`Corporate actions (${data.corporate_actions.length})`}>106            <DataTable107              rows={data.corporate_actions}108              rowKey={(a, i) => `${a.action_type}-${a.ex_date}-${i}`}109              mobile="scroll"110              maxHeight="24rem"111              columns={[112                { key: "ex_date", header: "Ex-date", render: (a) => <span className="tnum">{a.ex_date}</span> },113                { key: "action_type", header: "Type", render: (a) => <StatusBadge status="neutral" label={a.action_type} icon={false} /> },114                { key: "value", header: "Value", render: (a) => <JsonView value={a.value} collapsed /> },115              ]}116              empty="No splits or dividends recorded."117            />118          </Card>119          <Card title={`Aliases (${data.aliases.length})`}>120            {data.aliases.length === 0 ? (121              <Empty>No aliases.</Empty>122            ) : (123              <ul className="flex flex-col gap-1">124                {data.aliases.map((a, i) => (125                  <li key={i}>126                    <JsonView value={a} collapsed />127                  </li>128                ))}129              </ul>130            )}131          </Card>132        </div>133      )}134    </>135  );136}137
Files · main
Menu

Theme