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:
Investor Flow Build
2026-07-05 08:50:12 -04:00
parent 1e818c0881
commit 55f07e6b42
27 changed files with 2331 additions and 555 deletions
+113
View File
@@ -108,6 +108,55 @@ export interface IndicatorResult {
relvol?: Array<{ ts: string; value: number }>;
}
// --- EDGAR / SEC Filings types (Slice 6 / M6) -------------------------------
export interface EdgarFiling {
form: string;
dateReporter: string;
accessionNumber: string;
accessionNormalization: string;
reportDate: string;
reportFile: string;
primaryDocument: string;
}
export interface FilerMeta {
cik: string;
name: string | null;
sic: string | null;
}
export interface Form13fHolding {
cusip: string;
issuerName: string;
value: number;
sshPrnamt: number;
}
export interface Form13fResult {
holdings: Form13fHolding[];
accession: string;
}
export interface Form4Transaction {
reporter: string;
relationship: string;
securityTitle: string;
transactionDate: string;
transactionCode: string;
shares: number;
price: number;
}
export interface Form4Result {
transactions: Form4Transaction[];
accession: string;
}
export interface FilingsSearchResult {
filings: Array<Record<string, unknown>>;
}
export const api = {
market: {
snapshot: (symbol: string) => trpcQuery<Snapshot>("market.snapshot", { symbol }),
@@ -116,6 +165,20 @@ export const api = {
indicators: (symbol: string, opts: IndicatorOptions) =>
trpcQuery<IndicatorResult>("market.indicators", { symbol, ...opts }),
},
edgar: {
filings_index: (input: { cik: string; formTypes?: string[]; dateRange?: { from?: string; to?: string } }) =>
trpcQuery<{ filings: EdgarFiling[]; cik: string }>("edgar.filings_index", input),
company_facts: (cik: string) =>
trpcQuery<{ facts: Record<string, unknown>; cik: string }>("edgar.company_facts", { cik }),
filer_cik_meta: (cik: string) =>
trpcQuery<{ meta: FilerMeta; cik: string }>("edgar.filer_cik_meta", { cik }),
full_text_search: (q: string) =>
trpcQuery<FilingsSearchResult>("edgar.full_text_search", { q }),
form13f_holdings: (cik: string, accession: string) =>
trpcQuery<{ holdings: Form13fResult }>("edgar.form13f_holdings", { cik, accession }),
form4_tx: (cik: string, accession: string) =>
trpcQuery<{ transactions: Form4Result }>("edgar.form4_tx", { cik, accession }),
},
auth: {
signup: (email: string, password: string) => trpcMutate<{ userId: string }>("auth.signup", { email, password }),
login: (email: string, password: string, totp?: string) => trpcMutate<{ userId: string }>("auth.login", { email, password, ...(totp ? { totp } : {}) }),
@@ -130,4 +193,54 @@ export const api = {
complete: (input: { complexity: Complexity; drawdownTolerancePct?: number; firstWatchlistSymbols?: string[]; portfolio?: PortfolioEntry[] }) =>
trpcMutate<{ ok: boolean; watchlistId: string }>("onboarding.complete", input),
},
watchlists: {
list: () => trpcQuery<WatchlistEntry[]>("watchlists.list"),
addSymbol: (symbol: string) => trpcMutate<{ added: boolean }>("watchlists.addSymbol", { symbol }),
removeSymbol: (symbol: string) => trpcMutate<{ removed: boolean }>("watchlists.removeSymbol", { symbol }),
},
options: {
chain: (symbol: string, opts?: { expiry?: string }) =>
trpcQuery<{ symbol: string; expiration: string | null; rows: Array<Record<string, unknown>> }>("options.chain", { symbol, ...opts }),
greeks: (symbol: string, opts?: { expiry?: string; strike?: number }) =>
trpcQuery<{ symbol: string; greeks: Record<string, unknown> | null; strike?: number; right?: string; lastPrice?: number; impliedVolatility?: number }>("options.greeks", { symbol, ...opts }),
},
portfolio: {
holdings: () => trpcQuery<PortfolioHolding[]>("portfolio.holdings"),
addHolding: (symbol: string, shares: number, avgCost: number) => trpcMutate<{ created: boolean }>("portfolio.addHolding", { symbol, shares, avgCost }),
removeHolding: (symbol: string) => trpcMutate<{ removed: boolean }>("portfolio.removeHolding", { symbol }),
},
dashboard: {
rollup: () => trpcQuery<Record<string, unknown>[]>("dashboard.rollup"),
},
institutional: {
flow: (symbol: string) =>
trpcQuery<{ symbol: string; flow: Array<Record<string, unknown>>; quarters: string[] }>("institutional.flow", { symbol }),
insiderStream: (symbol: string, limit = 50) =>
trpcQuery<{ symbol: string; events: Array<Record<string, unknown>>; netShares: number; count: number }>("institutional.insiderStream", { symbol, limit }),
},
emotionLogger: {
add: (input: { tradeExecutionId: string; priceAtEvent?: number; emotion: string; note?: string }) =>
trpcMutate<{ success: boolean; log: Record<string, unknown> }>("emotionLogger.add", input),
getByTrade: (tradeExecutionId: string) =>
trpcQuery<{ logs: Record<string, unknown>[] }>("emotionLogger.getByTrade", { tradeExecutionId }),
delete: (id: string) =>
trpcMutate<{ success: boolean }>("emotionLogger.delete", { id }),
},
alerts: {
unackedCount: () => trpcQuery<{ count: number }>("alerts.unackedCount"),
acknowledgeAll: () => trpcMutate<{ ok: boolean }>("alerts.acknowledgeAll"),
},
};
export interface WatchlistEntry {
symbol: string;
added_at: string;
notes?: string | null;
}
export interface PortfolioHolding {
symbol: string;
shares: number;
avg_cost: number;
added_at: string;
}
+39
View File
@@ -0,0 +1,39 @@
"use client";
import { useEffect } from "react";
interface KeyboardShortcut {
key: string;
ctrl?: boolean;
shift?: boolean;
alt?: boolean;
handler: (e: KeyboardEvent) => void;
// Only fire when no input/modal is focused
when?: () => boolean;
}
/**
* Shared keyboard shortcut hook.
* Registers shortcuts at the LayoutShell level (closest to root).
* Uses keydown events with preventDefault only when needed.
*/
export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
for (const shortcut of shortcuts) {
const matchesKey = e.key === shortcut.key;
const matchesCtrl = shortcut.ctrl ? e.ctrlKey || e.metaKey : !e.ctrlKey && !e.metaKey;
const matchesShift = shortcut.shift ? e.shiftKey : !e.shiftKey;
const matchesAlt = shortcut.alt ? e.altKey : !e.altKey;
if (matchesKey && matchesCtrl && matchesShift && matchesAlt) {
if (shortcut.when && !shortcut.when()) continue;
e.preventDefault();
shortcut.handler(e);
}
}
};
window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler);
}, [shortcuts]);
}