import { useCallback, useMemo } from "react";
import { listingIdentity } from "../shared/ticker-request";
import { Box, ScrollBox, useUiCapabilities } from "../../../ui";
import { useAsyncResource, useAutoRefresh, usePaneSettingValue, usePluginPaneState, useShortcut, useUpdatedAgo } from "../../../public/react";
import { CompositeChart, DataTableStackView, EmptyState, KeyValueRow, PaneStatusBody, usePaneNoticeFooter, usePaneStatusLinkFooter, type DataTableCell, StatGrid } from "../../../components";
import { colors } from "../../../theme/colors";
import type { ShortVolumeObservation } from "../../../api-client/short-volume";
import { ApiRequestError } from "../../../api-client/errors";
import { staticSeries } from "../../../components/chart/static/series";
import type { PaneProps } from "../../../types/plugin";
import { isPlainKey } from "../../../utils/keyboard";
import { SignInWall } from "../cloud/auth-actions";
import { isCloudSessionRequired, useResearchCloudSession } from "../shared/research-cloud-session";
import { cachedShortVolume, loadShortVolume } from "./client";
import { exactQuantity, sortedVolumeHistory, VOLUME_COLUMNS, volumeChange, volumeHistoryPoints, volumePercent, volumePointStatus, volumeQuantity, type VolumeColumn, type VolumeSort } from "./model";
import { usePaneTickerIdentity } from "../../../state/hooks/pane-ticker";
const PANELS = [{ id: "main" }];
const DETAIL_LABEL_WIDTH = 26;
const clearDenied = (error: unknown) => error instanceof ApiRequestError && [401, 403].includes(error.status ?? 0);
function renderCell(row: ShortVolumeObservation, column: VolumeColumn, _index: number, state: { selected: boolean }): DataTableCell {
const text = column.id === "date" ? row.date : column.id === "ratioPercent" ? volumePercent(row.ratioPercent)
: column.id === "status" ? volumePointStatus(row) : volumeQuantity(row[column.id]);
return { text, color: state.selected ? colors.selectedText : column.id === "ratioPercent" ? colors.warning
: row.unavailableReason ? colors.textMuted : colors.text };
}
function VolumeDetail({ row, width, height }: { row: ShortVolumeObservation; width: number; height: number }) {
const { nativePaneChrome } = useUiCapabilities();
// The stack title is the date, so the rows start with the figures.
return
;
}
export function ShortVolumePane({ width, height, focused }: Pick) {
const { ticker } = usePaneTickerIdentity();
const [sourceSymbol] = usePaneSettingValue("finraSymbol", "");
const [scopeValue] = usePaneSettingValue("shortVolumeScope", "nms");
const scope = scopeValue === "otc" ? "otc" : "nms";
const symbol = sourceSymbol.trim() || listingIdentity(ticker?.metadata.ticker)?.symbol || null;
const session = useResearchCloudSession();
const loader = useCallback((force: boolean) => loadShortVolume(symbol!, scope, force), [symbol, scope, session.requestKey]);
const resource = useAsyncResource(symbol ? loader : null, {
initialData: () => symbol ? cachedShortVolume(symbol, scope) : null, clearOnError: clearDenied,
});
const data = resource.data?.payload;
const identity = `${scope}:${symbol}`;
const [sort, setSort] = usePluginPaneState("short-volume:sort", { column: "date", direction: "desc" });
const [selectedId, setSelectedId] = usePluginPaneState("short-volume:selected", null);
const [openId, setOpenId] = usePluginPaneState("short-volume:open", null);
const rowKey = (row: ShortVolumeObservation) => `${identity}:${row.date}`;
const rows = useMemo(() => data ? sortedVolumeHistory(data, sort) : [], [data, sort]);
const selectedIndex = Math.max(0, rows.findIndex((row) => rowKey(row) === selectedId));
const openRow = rows.find((row) => rowKey(row) === openId);
const selected = openRow ?? rows[selectedIndex];
const updatedAgo = useUpdatedAgo(resource.updatedAt);
const series = useMemo(() => [staticSeries(data ? volumeHistoryPoints(data) : [], {
id: "daily-short-ratio", label: "Off-exchange short ratio (%)", color: colors.warning, calendarSpaced: true,
})], [data]);
const chartHeight = height >= 16 ? Math.max(5, Math.min(12, Math.floor(height * .38))) : 0;
const latest = data?.latest;
const stats = latest?.percentile;
useAutoRefresh(resource.updatedAt, resource.load);
useShortcut((event) => { if (focused && isPlainKey(event, "r")) { event.preventDefault(); void resource.reload(); } });
usePaneNoticeFooter({ registrationId: "short-volume:notices", focused,
notices: [...(data?.warnings ?? []), ...(resource.data?.refreshError ? [resource.data.refreshError] : [])] });
usePaneStatusLinkFooter({ registrationId: "short-volume", focused, loading: resource.loading, error: resource.error,
url: selected?.sourceUrl ?? data?.source.url ?? null, showOpenHint: true,
info: data ? [
...(updatedAgo ? [{ id: "updated", parts: [{ text: updatedAgo, tone: "muted" as const }] }] : []),
...(resource.data?.stale ? [{ id: "stale", parts: [{ text: "stale", tone: "warning" as const }] }] : []),
] : [],
});
if (!data && isCloudSessionRequired(resource.error)) return ;
if (!symbol) return ;
return
{data ?
focused={focused} rootWidth={width} rootHeight={height} emptyStateTitle="No reported daily volume." columns={VOLUME_COLUMNS} items={rows} freezeFirstColumn
getItemKey={rowKey} renderCell={renderCell} resetScrollKey={identity}
selection={{ kind: "index", selectedIndex: rows.length ? selectedIndex : -1, onChange: (index) => setSelectedId(rows[index] ? rowKey(rows[index]!) : null) }}
onActivate={(row) => setOpenId(rowKey(row))} detailOpen={!!openRow} onBack={() => setOpenId(null)}
detailTitle={openRow?.date} detailContent={openRow ? : null}
sortColumnId={sort.column} sortDirection={sort.direction}
onHeaderClick={(column) => setSort((current) => ({ column: column as VolumeSort["column"], direction: current.column === column && current.direction === "desc" ? "asc" : "desc" }))}
rootBefore={
{/* "Short ratio" reads as days to cover, so the band names the table's SHORT % column. */}
{chartHeight && data.history.some((point) => point.ratioPercent !== null) ?
`${value.toFixed(0)}%`} remoteKind="short-volume-history" />
: null}
}
/> : null}
;
}