fix: wire data adapters, fix OptionsAdapter v3, build Research pages
- Fix OptionsAdapter for yahoo-finance2 v3 API ({ date: expiry } instead of positional arg)
- Fix institutional.flow and insiderStream endpoints to query correct DB tables
- Fix anonymous user INSERT (pw_hash column, not password_hash)
- Create secDataFetcher module for 13F/Form 4 data population
- Add institutional and marketOutlook endpoints to frontend trpc client
- Build out Chart Lab, Institutional, Market Outlook pages with real components
- Add focus-visible rings, aria-labels, hex tokens, shape icons across all panels
- Integrate EmotionLogger, MobileTabNav, useKeyboardShortcuts
- Add localStorage persistence for emotion logs
- Normalize spacing, type scale, hover effects across all components
This commit is contained in:
@@ -100,26 +100,61 @@ export class OptionsAdapter implements SourceFetch {
|
||||
const yf = await this.yf();
|
||||
|
||||
if (kind === 'expiry_dates') {
|
||||
const rawDates = await yf.optionsExpiryDates(id);
|
||||
// yahoo-finance2 returns string[] (ISO dates). Sort ascending.
|
||||
const dates = [...rawDates].sort() as OptionExpiryDate[];
|
||||
// yahoo-finance2 v3: options() returns expirationDates as Date objects
|
||||
const result = await yf.options(id);
|
||||
const dates = (result.expirationDates ?? [])
|
||||
.map((d: Date) => d.toISOString().slice(0, 10))
|
||||
.sort() as OptionExpiryDate[];
|
||||
return {
|
||||
value: dates,
|
||||
ttlClass: 'intraday', // 5 min TTL — short-lived, shifts around events
|
||||
ttlClass: 'intraday',
|
||||
provenance: { fetchedAt, sourceKind: 'yfinance', rawSourceId: `options:expiry:${id}` },
|
||||
};
|
||||
}
|
||||
|
||||
if (kind === 'chain') {
|
||||
const [symbol, expiry] = id.split(':');
|
||||
const rawChain = await yf.options(symbol, expiry);
|
||||
const rawChain = await yf.options(symbol, { date: expiry });
|
||||
// v3: options is indexed by number, extract calls/puts from first entry
|
||||
const optionsObj = rawChain.options;
|
||||
const firstKey = Object.keys(optionsObj)[0];
|
||||
const expiryData = firstKey ? optionsObj[firstKey] : null;
|
||||
const allRows = [
|
||||
...(expiryData?.calls ?? []).map((c: Record<string, unknown>) => ({ ...c, right: 'call' as const })),
|
||||
...(expiryData?.puts ?? []).map((p: Record<string, unknown>) => ({ ...p, right: 'put' as const })),
|
||||
];
|
||||
const rows = parseOptionChainRows(symbol, allRows);
|
||||
return {
|
||||
value: parseOptionChain(symbol, rawChain),
|
||||
ttlClass: 'options_snapshot', // 15 min (matches CacheRepository TTL_MS)
|
||||
value: rows,
|
||||
ttlClass: 'options_snapshot',
|
||||
provenance: { fetchedAt, sourceKind: 'yfinance', rawSourceId: `options:chain:${symbol}:${expiry}` },
|
||||
};
|
||||
}
|
||||
|
||||
if (kind === 'greeks') {
|
||||
const parts = id.split(':');
|
||||
const symbol = parts[0];
|
||||
const expiry = parts[1];
|
||||
const strike = parseFloat(parts[2] ?? '0');
|
||||
const rawChain = await yf.options(symbol, { date: expiry });
|
||||
const optionsObj = rawChain.options;
|
||||
const firstKey = Object.keys(optionsObj)[0];
|
||||
const expiryData = firstKey ? optionsObj[firstKey] : null;
|
||||
const allRows = [
|
||||
...(expiryData?.calls ?? []).map((c: Record<string, unknown>) => ({ ...c, right: 'call' as const })),
|
||||
...(expiryData?.puts ?? []).map((p: Record<string, unknown>) => ({ ...p, right: 'put' as const })),
|
||||
];
|
||||
const rows = parseOptionChainRows(symbol, allRows);
|
||||
const target = strike > 0
|
||||
? rows.find((r) => r.strike === strike)
|
||||
: rows[0];
|
||||
return {
|
||||
value: target ?? null,
|
||||
ttlClass: 'options_snapshot',
|
||||
provenance: { fetchedAt, sourceKind: 'yfinance', rawSourceId: `options:greeks:${symbol}:${expiry}:${strike}` },
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`OptionsAdapter: unknown kind '${kind}'`);
|
||||
}
|
||||
|
||||
@@ -133,7 +168,9 @@ export class OptionsAdapter implements SourceFetch {
|
||||
async chain(symbol: string, expiry: OptionExpiryDate): Promise<OptionChain> {
|
||||
const key = `yfinance:chain:${symbol}:${expiry}`;
|
||||
const result = await this.fetchOne(key);
|
||||
return result.value as OptionChain;
|
||||
// fetchOne returns OptionChainRow[] (the cached shape). Re-wrap into OptionChain.
|
||||
const rows = result.value as OptionChainRow[];
|
||||
return { symbol, expiration: expiry, rows };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,15 +178,25 @@ export class OptionsAdapter implements SourceFetch {
|
||||
|
||||
/**
|
||||
* Parse the raw option chain from yahoo-finance2 into typed OptionChainRow[].
|
||||
* yfinance2 returns `{ calls: [...], puts: [...] }` — we flatten and tag each row.
|
||||
* Handles both formats:
|
||||
* - v2: `{ calls: [...], puts: [...] }` object
|
||||
* - v3: array of row objects with `right` field already set
|
||||
*/
|
||||
export function parseOptionChain(symbol: string, raw: Record<string, unknown>): OptionChain {
|
||||
const calls = (raw.calls ?? []) as Array<Record<string, unknown>>;
|
||||
const puts = (raw.puts ?? []) as Array<Record<string, unknown>>;
|
||||
export function parseOptionChainRows(symbol: string, raw: Record<string, unknown> | Array<Record<string, unknown>>): OptionChainRow[] {
|
||||
let rows: OptionChainRow[] = [];
|
||||
|
||||
const rows: OptionChainRow[] = [];
|
||||
for (const r of [...calls, ...puts]) {
|
||||
rows.push(parseOneRow(r));
|
||||
if (Array.isArray(raw)) {
|
||||
// Already flattened array with right field
|
||||
for (const r of raw) {
|
||||
rows.push(parseOneRow(r));
|
||||
}
|
||||
} else {
|
||||
// Legacy format: { calls: [...], puts: [...] }
|
||||
const calls = (raw.calls ?? []) as Array<Record<string, unknown>>;
|
||||
const puts = (raw.puts ?? []) as Array<Record<string, unknown>>;
|
||||
for (const r of [...calls, ...puts]) {
|
||||
rows.push(parseOneRow(r));
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by strike ascending, calls first then puts at same strike (standard convention).
|
||||
@@ -162,9 +209,16 @@ export function parseOptionChain(symbol: string, raw: Record<string, unknown>):
|
||||
return 0;
|
||||
});
|
||||
|
||||
// Derive expiration from the first row (all rows in a chain share it).
|
||||
const expiration = rows[0]?.expiration ?? '';
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the raw option chain and return a full OptionChain wrapper.
|
||||
* Used when callers need the OptionChain shape (e.g. direct API responses).
|
||||
*/
|
||||
export function parseOptionChain(symbol: string, raw: Record<string, unknown>): OptionChain {
|
||||
const rows = parseOptionChainRows(symbol, raw);
|
||||
const expiration = rows[0]?.expiration ?? '';
|
||||
return { symbol, expiration, rows };
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user