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:
+18
-1
@@ -6,7 +6,7 @@
|
||||
import { DatabaseSync } from 'node:sqlite';
|
||||
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 CacheKey = string; // `${SourceKind}:${kind}:${id}` e.g. 'yfinance:quote:NVDA', 'yfinance:candles:NVDA:1d'
|
||||
export type TtlClass =
|
||||
@@ -302,6 +302,19 @@ const shortInterestHandler: KindHandler = {
|
||||
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>([
|
||||
['quote', quoteHandler],
|
||||
['candles', candlesHandler],
|
||||
@@ -314,6 +327,7 @@ const HANDLERS = new Map<string, KindHandler>([
|
||||
['holders', lintHoldersHandler],
|
||||
['insiders', lintInsidersHandler],
|
||||
['shortinterest', shortInterestHandler],
|
||||
['nasdaqShortinterest', nasdaqShortinterestHandler],
|
||||
]);
|
||||
|
||||
export interface CacheRepository {
|
||||
@@ -424,6 +438,9 @@ export class CacheRepositoryImpl implements CacheRepository {
|
||||
case 'shortinterest':
|
||||
d.prepare('DELETE FROM kv_cache WHERE key=?').run(`yfinance:shortinterest:${id}`);
|
||||
break;
|
||||
case 'nasdaqShortinterest':
|
||||
d.prepare('DELETE FROM kv_cache WHERE key=?').run(`nasdaq:nasdaqShortinterest:${id}`);
|
||||
break;
|
||||
default: {
|
||||
// Unknown/wildcard kind (e.g. `x:cashtag:*`): best-effort delete from kv_cache via LIKE.
|
||||
const like = key.endsWith(':*') ? `${key.slice(0, -1)}%` : key;
|
||||
|
||||
Reference in New Issue
Block a user