feat: Phase 2 NASDAQ adapter for days-to-cover + 24mo history
- Add nasdaq SourceKind + nasdaqShortinterest KindHandler (kv_cache with nasdaq: prefix) - Create NasdaqAdapter: calls api.nasdaq.com/quote/SYM/short-interest, parses settlementDate/sharesShort/daysToCover/avgVolume, returns 24mo history; handles non-NASDAq gracefully - Register NasdaqAdapter in server index.ts adapters map - Update shortInterest procedure: reads both yfinance + nasdaq caches in parallel, reconciles (NASDAQ for daysToCover/sharesShort, Yahoo for %float/shortRatio and NYSE fallback) - Update ShortInterestPanel: display daysToCover, settlement date, source badges, 6-month history table, updated disclaimer - Update trpc.ts client type for merged response shape
This commit is contained in:
@@ -0,0 +1,91 @@
|
|||||||
|
// Investor Flow — NasdaqAdapter (DESIGN.md §3a Module 2 + §5 yfinance policy).
|
||||||
|
// SourceFetch for NASDAQ short-interest data via the Nasdaq Data Link API.
|
||||||
|
// Per-symbol JSON endpoint, no API key required (public, rate-limited).
|
||||||
|
// Covers NASDAQ-listed stocks only; NYSE/other tickers return an error.
|
||||||
|
import type { CacheKey, TtlClass, Provenance, SourceKind } from '../cache/CacheRepository.ts';
|
||||||
|
import { parseCacheKey } from '../cache/CacheRepository.ts';
|
||||||
|
import type { SourceFetch, FetchResult } from './SourceAdapter.ts';
|
||||||
|
|
||||||
|
const UA = `InvestorFlow/${process.env.APP_VERSION ?? 'dev'} (+https://github.com/anomalyco/investor-flow)`;
|
||||||
|
const NASDAQ_SHORTINTEREST_TTL_MS = 12 * 60 * 60_000; // 12h (NASDAQ publishes bi-monthly)
|
||||||
|
|
||||||
|
interface NasdaqShortInterestRow {
|
||||||
|
settlementDate: string;
|
||||||
|
interest: string;
|
||||||
|
avgDailyShareVolume: string;
|
||||||
|
daysToCover: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NasdaqShortInterestResponse {
|
||||||
|
data?: {
|
||||||
|
symbol: string;
|
||||||
|
shortInterestTable?: {
|
||||||
|
headers: Array<{ key: string }>;
|
||||||
|
rows: NasdaqShortInterestRow[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
status?: { rCode: number };
|
||||||
|
message?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NasdaqAdapter implements SourceFetch {
|
||||||
|
readonly sourceKind = 'nasdaq' as SourceKind;
|
||||||
|
|
||||||
|
async fetchOne(key: CacheKey): Promise<FetchResult> {
|
||||||
|
const { kind, id } = parseCacheKey(key);
|
||||||
|
const fetchedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
if (kind !== 'nasdaqShortinterest') {
|
||||||
|
throw new Error(`NasdaqAdapter: unknown kind '${kind}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`https://api.nasdaq.com/api/quote/${encodeURIComponent(id)}/short-interest?assetclass=stocks`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'User-Agent': UA,
|
||||||
|
Accept: 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`NASDAQ API returned ${res.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = (await res.json()) as NasdaqShortInterestResponse;
|
||||||
|
|
||||||
|
if (!body.data?.shortInterestTable?.rows?.length) {
|
||||||
|
throw new Error(body.message ?? 'No short-interest data available');
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = body.data.shortInterestTable.rows as NasdaqShortInterestRow[];
|
||||||
|
const latest = rows[0];
|
||||||
|
const history = rows.map((r) => ({
|
||||||
|
settlementDate: r.settlementDate,
|
||||||
|
sharesShort: parseShares(r.interest),
|
||||||
|
avgDailyVolume: parseShares(r.avgDailyShareVolume),
|
||||||
|
daysToCover: r.daysToCover,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: {
|
||||||
|
sharesShort: parseShares(latest.interest),
|
||||||
|
avgDailyShareVolume: parseShares(latest.avgDailyShareVolume),
|
||||||
|
daysToCover: latest.daysToCover,
|
||||||
|
settlementDate: latest.settlementDate,
|
||||||
|
history,
|
||||||
|
source: 'nasdaq',
|
||||||
|
},
|
||||||
|
ttlClass: 'short_interest' as TtlClass,
|
||||||
|
provenance: { fetchedAt, sourceKind: 'nasdaq', rawSourceId: id },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseShares(raw: string): number | null {
|
||||||
|
if (!raw) return null;
|
||||||
|
const cleaned = raw.replace(/,/g, '');
|
||||||
|
const n = Number(cleaned);
|
||||||
|
return Number.isNaN(n) || n < 0 ? null : n;
|
||||||
|
}
|
||||||
+18
-1
@@ -6,7 +6,7 @@
|
|||||||
import { DatabaseSync } from 'node:sqlite';
|
import { DatabaseSync } from 'node:sqlite';
|
||||||
import { db as defaultDb } from '../db/client.ts';
|
import { db as defaultDb } from '../db/client.ts';
|
||||||
|
|
||||||
export type SourceKind = 'yfinance' | 'sec' | 'sec-fetch' | 'reddit' | 'x' | 'macro' | 'llm' | 'sec-lint-holders' | 'sec-lint-insiders' | 'fred';
|
export type SourceKind = 'yfinance' | 'nasdaq' | 'sec' | 'sec-fetch' | 'reddit' | 'x' | 'macro' | 'llm' | 'sec-lint-holders' | 'sec-lint-insiders' | 'fred';
|
||||||
export type TickerKind = 'equity' | 'crypto' | 'etf' | 'index';
|
export type TickerKind = 'equity' | 'crypto' | 'etf' | 'index';
|
||||||
export type CacheKey = string; // `${SourceKind}:${kind}:${id}` e.g. 'yfinance:quote:NVDA', 'yfinance:candles:NVDA:1d'
|
export type CacheKey = string; // `${SourceKind}:${kind}:${id}` e.g. 'yfinance:quote:NVDA', 'yfinance:candles:NVDA:1d'
|
||||||
export type TtlClass =
|
export type TtlClass =
|
||||||
@@ -302,6 +302,19 @@ const shortInterestHandler: KindHandler = {
|
|||||||
isStale(ts, now) { return tsAgeMs(ts, now) > TTL_MS.short_interest; },
|
isStale(ts, now) { return tsAgeMs(ts, now) > TTL_MS.short_interest; },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nasdaqShortinterestHandler: KindHandler = {
|
||||||
|
ttlClass: 'short_interest',
|
||||||
|
read(d, id) {
|
||||||
|
const r = d.prepare('SELECT value, observed_at FROM kv_cache WHERE key=?').get(`nasdaq:nasdaqShortinterest:${id}`) as { value: string; observed_at: string } | undefined;
|
||||||
|
if (!r) return null;
|
||||||
|
try { return { value: JSON.parse(r.value), stalenessTs: r.observed_at }; } catch { return null; }
|
||||||
|
},
|
||||||
|
write(d, id, value, provenance) {
|
||||||
|
d.prepare('INSERT OR REPLACE INTO kv_cache (key, value, observed_at) VALUES (?,?,?)').run(`nasdaq:nasdaqShortinterest:${id}`, JSON.stringify(value), provenance.fetchedAt);
|
||||||
|
},
|
||||||
|
isStale(ts, now) { return tsAgeMs(ts, now) > TTL_MS.short_interest; },
|
||||||
|
};
|
||||||
|
|
||||||
const HANDLERS = new Map<string, KindHandler>([
|
const HANDLERS = new Map<string, KindHandler>([
|
||||||
['quote', quoteHandler],
|
['quote', quoteHandler],
|
||||||
['candles', candlesHandler],
|
['candles', candlesHandler],
|
||||||
@@ -314,6 +327,7 @@ const HANDLERS = new Map<string, KindHandler>([
|
|||||||
['holders', lintHoldersHandler],
|
['holders', lintHoldersHandler],
|
||||||
['insiders', lintInsidersHandler],
|
['insiders', lintInsidersHandler],
|
||||||
['shortinterest', shortInterestHandler],
|
['shortinterest', shortInterestHandler],
|
||||||
|
['nasdaqShortinterest', nasdaqShortinterestHandler],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export interface CacheRepository {
|
export interface CacheRepository {
|
||||||
@@ -424,6 +438,9 @@ export class CacheRepositoryImpl implements CacheRepository {
|
|||||||
case 'shortinterest':
|
case 'shortinterest':
|
||||||
d.prepare('DELETE FROM kv_cache WHERE key=?').run(`yfinance:shortinterest:${id}`);
|
d.prepare('DELETE FROM kv_cache WHERE key=?').run(`yfinance:shortinterest:${id}`);
|
||||||
break;
|
break;
|
||||||
|
case 'nasdaqShortinterest':
|
||||||
|
d.prepare('DELETE FROM kv_cache WHERE key=?').run(`nasdaq:nasdaqShortinterest:${id}`);
|
||||||
|
break;
|
||||||
default: {
|
default: {
|
||||||
// Unknown/wildcard kind (e.g. `x:cashtag:*`): best-effort delete from kv_cache via LIKE.
|
// Unknown/wildcard kind (e.g. `x:cashtag:*`): best-effort delete from kv_cache via LIKE.
|
||||||
const like = key.endsWith(':*') ? `${key.slice(0, -1)}%` : key;
|
const like = key.endsWith(':*') ? `${key.slice(0, -1)}%` : key;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
|||||||
import { db } from './db/client.ts';
|
import { db } from './db/client.ts';
|
||||||
import { createCacheRepository, type SourceKind } from './cache/CacheRepository.ts';
|
import { createCacheRepository, type SourceKind } from './cache/CacheRepository.ts';
|
||||||
import { YFinanceAdapter } from './adapters/YFinanceAdapter.ts';
|
import { YFinanceAdapter } from './adapters/YFinanceAdapter.ts';
|
||||||
|
import { NasdaqAdapter } from './adapters/NasdaqAdapter.ts';
|
||||||
import { SecFetchAdapter } from './adapters/SecFetchAdapter.ts';
|
import { SecFetchAdapter } from './adapters/SecFetchAdapter.ts';
|
||||||
import { SecLintAdapter } from './adapters/SecLintAdapter.ts';
|
import { SecLintAdapter } from './adapters/SecLintAdapter.ts';
|
||||||
import { XCookieAdapter } from './adapters/XCookieAdapter.ts';
|
import { XCookieAdapter } from './adapters/XCookieAdapter.ts';
|
||||||
@@ -18,6 +19,7 @@ const PORT = Number(process.env.PORT ?? 3001);
|
|||||||
const database = db();
|
const database = db();
|
||||||
const adapters = new Map<SourceKind, SourceFetch>([
|
const adapters = new Map<SourceKind, SourceFetch>([
|
||||||
['yfinance' as const, new YFinanceAdapter() as unknown as SourceFetch],
|
['yfinance' as const, new YFinanceAdapter() as unknown as SourceFetch],
|
||||||
|
['nasdaq' as const, new NasdaqAdapter() as unknown as SourceFetch],
|
||||||
['sec-fetch' as const, new SecFetchAdapter(database) as unknown as SourceFetch],
|
['sec-fetch' as const, new SecFetchAdapter(database) as unknown as SourceFetch],
|
||||||
['sec-lint-holders' as const, new SecLintAdapter(() => database, 'sec-lint-holders') as unknown as SourceFetch],
|
['sec-lint-holders' as const, new SecLintAdapter(() => database, 'sec-lint-holders') as unknown as SourceFetch],
|
||||||
['sec-lint-insiders' as const, new SecLintAdapter(() => database, 'sec-lint-insiders') as unknown as SourceFetch],
|
['sec-lint-insiders' as const, new SecLintAdapter(() => database, 'sec-lint-insiders') as unknown as SourceFetch],
|
||||||
|
|||||||
@@ -1900,27 +1900,48 @@ const institutionalRouter = router({
|
|||||||
return { symbol, ratings: result.ratings, consensus: result.consensus };
|
return { symbol, ratings: result.ratings, consensus: result.consensus };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
/** Short interest (shares short, % float, short ratio) from yahoo-finance2 defaultKeyStatistics. */
|
/** Short interest reconciled across Yahoo Finance + NASDAQ API.
|
||||||
shortInterest: publicProcedure
|
* Yahoo covers all US-listed tickers (basic: shares short, % float, short ratio).
|
||||||
.input(z.object({ symbol: z.string().min(1) }))
|
* NASDAQ covers NASDAQ-listed only but adds days-to-cover + 24mo history.
|
||||||
.query(async ({ ctx, input }) => {
|
* Reconciliation: prefer NASDAQ's sharesShort/daysToCover when available (richer),
|
||||||
const symbol = input.symbol.toUpperCase();
|
* fall back to Yahoo for NYSE/other tickers. */
|
||||||
const k = `yfinance:shortinterest:${symbol}`;
|
shortInterest: publicProcedure
|
||||||
const entry = await ctx.cache.get<unknown>(k);
|
.input(z.object({ symbol: z.string().min(1) }))
|
||||||
const value = entry.value as Record<string, unknown> | null;
|
.query(async ({ ctx, input }) => {
|
||||||
return {
|
const symbol = input.symbol.toUpperCase();
|
||||||
symbol,
|
const [yfEntry, ndqEntry] = await Promise.all([
|
||||||
sharesShort: value ? (value.sharesShort as number | null) : null,
|
ctx.cache.get<Record<string, unknown>>(`yfinance:shortinterest:${symbol}`),
|
||||||
sharesShortPriorMonth: value ? (value.sharesShortPriorMonth as number | null) : null,
|
ctx.cache.get<Record<string, unknown>>(`nasdaq:nasdaqShortinterest:${symbol}`),
|
||||||
sharesPercentSharesOut: value ? (value.sharesPercentSharesOut as number | null) : null,
|
]);
|
||||||
shortRatio: value ? (value.shortRatio as number | null) : null,
|
const yf = yfEntry.value;
|
||||||
shortPercentOfFloat: value ? (value.shortPercentOfFloat as number | null) : null,
|
const ndq = ndqEntry.value;
|
||||||
dateShortInterest: value ? (value.dateShortInterest as string | null) : null,
|
|
||||||
floatShares: value ? (value.floatShares as number | null) : null,
|
// Prefer NASDAQ's sharesShort + daysToCover when available (NASDAQ-listed);
|
||||||
sharesOutstanding: value ? (value.sharesOutstanding as number | null) : null,
|
// otherwise fall back to Yahoo (covers NYSE/other tickers).
|
||||||
stale: entry.isStale,
|
const sharesShort = ndq?.sharesShort ?? yf?.sharesShort ?? null;
|
||||||
provenance: entry.provenance,
|
const daysToCover = ndq?.daysToCover ?? null;
|
||||||
};
|
const settlementDate = ndq?.settlementDate ?? null;
|
||||||
|
const history = ndq?.history ?? null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
symbol,
|
||||||
|
sharesShort,
|
||||||
|
sharesShortPriorMonth: yf?.sharesShortPriorMonth ?? null,
|
||||||
|
sharesPercentSharesOut: yf?.sharesPercentSharesOut ?? null,
|
||||||
|
shortRatio: yf?.shortRatio ?? null,
|
||||||
|
shortPercentOfFloat: yf?.shortPercentOfFloat ?? null,
|
||||||
|
dateShortInterest: yf?.dateShortInterest ?? null,
|
||||||
|
floatShares: yf?.floatShares ?? null,
|
||||||
|
sharesOutstanding: yf?.sharesOutstanding ?? null,
|
||||||
|
daysToCover,
|
||||||
|
settlementDate,
|
||||||
|
history,
|
||||||
|
sources: {
|
||||||
|
yfinance: { available: !!yf, stale: yfEntry.isStale },
|
||||||
|
nasdaq: { available: !!ndq, stale: ndqEntry.isStale },
|
||||||
|
},
|
||||||
|
stale: yfEntry.isStale && ndqEntry.isStale,
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { api } from '@/lib/trpc';
|
import { api } from '@/lib/trpc';
|
||||||
|
|
||||||
|
interface HistoryEntry {
|
||||||
|
settlementDate: string;
|
||||||
|
sharesShort: number | null;
|
||||||
|
avgDailyVolume: number | null;
|
||||||
|
daysToCover: number;
|
||||||
|
}
|
||||||
|
|
||||||
interface ShortInterestData {
|
interface ShortInterestData {
|
||||||
symbol: string;
|
symbol: string;
|
||||||
sharesShort: number | null;
|
sharesShort: number | null;
|
||||||
@@ -12,8 +19,11 @@ interface ShortInterestData {
|
|||||||
dateShortInterest: string | null;
|
dateShortInterest: string | null;
|
||||||
floatShares: number | null;
|
floatShares: number | null;
|
||||||
sharesOutstanding: number | null;
|
sharesOutstanding: number | null;
|
||||||
|
daysToCover: number | null;
|
||||||
|
settlementDate: string | null;
|
||||||
|
history: HistoryEntry[] | null;
|
||||||
|
sources: { yfinance: { available: boolean; stale: boolean }; nasdaq: { available: boolean; stale: boolean } };
|
||||||
stale: boolean;
|
stale: boolean;
|
||||||
provenance: { fetchedAt: string; sourceKind: string; rawSourceId?: string } | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatNumber(n: number | null): string {
|
function formatNumber(n: number | null): string {
|
||||||
@@ -62,34 +72,65 @@ export function ShortInterestPanel({ symbol }: { symbol: string | null }) {
|
|||||||
);
|
);
|
||||||
if (!data) return <div className="text-xs text-fg-muted py-2">No short interest data available for {symbol}.</div>;
|
if (!data) return <div className="text-xs text-fg-muted py-2">No short interest data available for {symbol}.</div>;
|
||||||
|
|
||||||
const dateLabel = data.dateShortInterest
|
const dateLabel = data.settlementDate
|
||||||
? `as of ${data.dateShortInterest}`
|
? `Settlement: ${data.settlementDate}`
|
||||||
: data.provenance?.fetchedAt
|
: data.dateShortInterest
|
||||||
? `fetched ${data.provenance.fetchedAt.slice(0, 10)}`
|
? `Yahoo: ${data.dateShortInterest}`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
const sourceTags = [] as string[];
|
||||||
|
if (data.sources.nasdaq.available) sourceTags.push('NASDAQ');
|
||||||
|
if (data.sources.yfinance.available) sourceTags.push('Yahoo');
|
||||||
|
const sourceLabel = sourceTags.length ? sourceTags.join(' + ') : '—';
|
||||||
|
|
||||||
|
const historySlice = data.history ? data.history.slice(0, 6) : [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg bg-surface-sunken border border-line p-4 space-y-3">
|
<div className="rounded-lg bg-surface-sunken border border-line p-4 space-y-3">
|
||||||
<h4 className="text-sm font-semibold text-fg">Short Interest</h4>
|
<h4 className="text-sm font-semibold text-fg">Short Interest</h4>
|
||||||
{dateLabel && (
|
{dateLabel && (
|
||||||
<p className="text-[10px] text-fg-muted">{dateLabel}{data.stale ? ' — stale' : ''}</p>
|
<p className="text-[10px] text-fg-muted">{dateLabel}{data.stale ? ' — stale' : ''} ({sourceLabel})</p>
|
||||||
)}
|
)}
|
||||||
{data.sharesShort !== null && (
|
{data.sharesShort !== null && (
|
||||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
|
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
|
||||||
<div className="flex justify-between"><dt className="text-fg-muted">Shares Short</dt><dd className="text-fg font-mono">{formatNumber(data.sharesShort)}</dd></div>
|
<div className="flex justify-between"><dt className="text-fg-muted">Shares Short</dt><dd className="text-fg font-mono">{formatNumber(data.sharesShort)}</dd></div>
|
||||||
<div className="flex justify-between"><dt className="text-fg-muted">% of Float</dt><dd className="text-fg font-mono">{formatPct(data.sharesPercentSharesOut)}</dd></div>
|
<div className="flex justify-between"><dt className="text-fg-muted">% of Float</dt><dd className="text-fg font-mono">{formatPct(data.sharesPercentSharesOut)}</dd></div>
|
||||||
<div className="flex justify-between"><dt className="text-fg-muted">Short Ratio</dt><dd className="text-fg font-mono">{data.shortRatio !== null ? `${data.shortRatio.toFixed(1)}d` : '—'}</dd></div>
|
<div className="flex justify-between"><dt className="text-fg-muted">Short Ratio</dt><dd className="text-fg font-mono">{data.shortRatio !== null ? `${data.shortRatio.toFixed(1)}d` : '—'}</dd></div>
|
||||||
<div className="flex justify-between"><dt className="text-fg-muted">% of Float (prior mo)</dt><dd className="text-fg font-mono">{formatPct(data.sharesShortPriorMonth !== null && data.floatShares ? data.sharesShortPriorMonth / data.floatShares : null)}</dd></div>
|
{data.daysToCover !== null && (
|
||||||
|
<div className="flex justify-between"><dt className="text-fg-muted">Days to Cover</dt><dd className="text-fg font-mono">{data.daysToCover.toFixed(2)}d</dd></div>
|
||||||
|
)}
|
||||||
|
{data.sharesShortPriorMonth !== null && data.floatShares ? (
|
||||||
|
<div className="flex justify-between"><dt className="text-fg-muted">% Float (prior)</dt><dd className="text-fg font-mono">{formatPct(data.sharesShortPriorMonth / data.floatShares)}</dd></div>
|
||||||
|
) : null}
|
||||||
{data.floatShares !== null && (
|
{data.floatShares !== null && (
|
||||||
<div className="flex justify-between"><dt className="text-fg-muted">Float Shares</dt><dd className="text-fg font-mono">{formatNumber(data.floatShares)}</dd></div>
|
<div className="flex justify-between"><dt className="text-fg-muted">Float Shares</dt><dd className="text-fg font-mono">{formatNumber(data.floatShares)}</dd></div>
|
||||||
)}
|
)}
|
||||||
{data.sharesOutstanding !== null && (
|
{data.sharesOutstanding !== null && (
|
||||||
<div className="flex justify-between"><dt className="text-fg-muted">Shares Outstanding</dt><dd className="text-fg font-mono">{formatNumber(data.sharesOutstanding)}</dd></div>
|
<div className="flex justify-between"><dt className="text-fg-muted">Shares Out</dt><dd className="text-fg font-mono">{formatNumber(data.sharesOutstanding)}</dd></div>
|
||||||
)}
|
)}
|
||||||
</dl>
|
</dl>
|
||||||
)}
|
)}
|
||||||
<p className="text-[10px] text-fg-muted">
|
{historySlice.length > 0 && (
|
||||||
Short interest from Yahoo Finance (derived from exchange filings). Educational display only — not investment advice.
|
<div className="mt-2">
|
||||||
|
<p className="text-[10px] text-fg-muted mb-1">6-month history (NASDAQ source)</p>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-[10px] text-fg-muted">
|
||||||
|
<thead><tr><th className="text-left py-0.5">Date</th><th className="text-right py-0.5">Short</th><th className="text-right py-0.5">Days</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{historySlice.map((h, i) => (
|
||||||
|
<tr key={i} className="border-b border-line/40">
|
||||||
|
<td className="py-0.5">{h.settlementDate}</td>
|
||||||
|
<td className="text-right font-mono">{formatNumber(h.sharesShort)}</td>
|
||||||
|
<td className="text-right font-mono">{h.daysToCover.toFixed(2)}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="text-[10px] text-fg-muted mt-1">
|
||||||
|
Reconciled from NASDAQ (days-to-cover, history) + Yahoo Finance (all tickers). Educational display only.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
+1
-1
@@ -623,7 +623,7 @@ export const api = {
|
|||||||
analystRatings: (symbol: string) =>
|
analystRatings: (symbol: string) =>
|
||||||
trpcQuery<{ symbol: string; ratings: Array<{ firm: string; action: string | null; gradeFrom: string | null; gradeTo: string | null; ratingDate: string; targetFrom: number | null; targetTo: number | null }>; consensus: { strongBuy: number; buy: number; hold: number; sell: number; strongSell: number } | null; error?: string }>("institutional.analystRatings", { symbol }),
|
trpcQuery<{ symbol: string; ratings: Array<{ firm: string; action: string | null; gradeFrom: string | null; gradeTo: string | null; ratingDate: string; targetFrom: number | null; targetTo: number | null }>; consensus: { strongBuy: number; buy: number; hold: number; sell: number; strongSell: number } | null; error?: string }>("institutional.analystRatings", { symbol }),
|
||||||
shortInterest: (symbol: string) =>
|
shortInterest: (symbol: string) =>
|
||||||
trpcQuery<{ symbol: string; sharesShort: number | null; sharesShortPriorMonth: number | null; sharesPercentSharesOut: number | null; shortRatio: number | null; shortPercentOfFloat: number | null; dateShortInterest: string | null; floatShares: number | null; sharesOutstanding: number | null; stale: boolean; provenance: { fetchedAt: string; sourceKind: string; rawSourceId?: string } | null }>("institutional.shortInterest", { symbol }),
|
trpcQuery<{ symbol: string; sharesShort: number | null; sharesShortPriorMonth: number | null; sharesPercentSharesOut: number | null; shortRatio: number | null; shortPercentOfFloat: number | null; dateShortInterest: string | null; floatShares: number | null; sharesOutstanding: number | null; daysToCover: number | null; settlementDate: string | null; history: Array<{ settlementDate: string; sharesShort: number | null; avgDailyVolume: number | null; daysToCover: number }> | null; sources: { yfinance: { available: boolean; stale: boolean }; nasdaq: { available: boolean; stale: boolean } }; stale: boolean }>("institutional.shortInterest", { symbol }),
|
||||||
},
|
},
|
||||||
emotionLogger: {
|
emotionLogger: {
|
||||||
add: (input: { tradeExecutionId: string; priceAtEvent?: number; emotion: string; note?: string }) =>
|
add: (input: { tradeExecutionId: string; priceAtEvent?: number; emotion: string; note?: string }) =>
|
||||||
|
|||||||
Reference in New Issue
Block a user