Files
investor-flow/app/src/lib/trpc.ts
T

876 lines
31 KiB
TypeScript
Raw Normal View History

// Thin typed client over the tRPC backend (DESIGN.md §2). Slice 1 uses a fetch wrapper
// rather than @trpc/client to keep the Next app decoupled from the server's Node-native
// type graph; the server is a real tRPC server and full end-to-end client types can be
// layered later without changing this surface.
import type { OptionChainRow, OptionGreeks } from "@/types/options";
export interface Quote {
symbol: string;
price: number;
bid?: number | null;
ask?: number | null;
change?: number | null;
changePercent?: number | null;
iv?: number | null;
}
export interface PriceCandle {
ts: string;
o: number;
h: number;
l: number;
c: number;
v: number;
adjClose?: number | null;
}
export interface SymbolMeta {
symbol: string;
name?: string | null;
sector?: string | null;
industry?: string | null;
exchange?: string | null;
tickerKind: string;
description?: string | null;
website?: string | null;
marketCap?: number | null;
currency?: string | null;
employees?: number | null;
country?: string | null;
city?: string | null;
peers?: string[] | null;
}
export interface XPost {
post_id: string;
author_handle: string;
body_text: string | null;
posted_at: string;
engagement: number;
cashtag?: string | null;
attribution?: string | null;
}
export interface XAccountRow {
id: string;
symbol: string;
handle: string;
label: string;
created_at: string;
}
export interface XCredentialsStatus {
configured: boolean;
healthy: 'healthy' | 'degraded';
lastError: string | null;
updatedAt: string | null;
}
export interface SectorRotationItem {
symbol: string;
name: string;
/** Legacy: inflow ≈ leading, outflow ≈ lagging (RS vs SPY, not abs flow). */
group: 'outflow' | 'inflow';
subGroup: string;
kind?: 'sector' | 'style' | 'region' | 'thematic';
leadership?: 'leading' | 'lagging' | 'inline' | 'unknown';
earlyWatch?: boolean;
rank1M?: number | null;
rank1W?: number | null;
relVol?: number | null;
oneWeek: number | null;
oneMonth: number | null;
threeMonth: number | null;
sixMonth: number | null;
oneYear: number | null;
rsOneWeek?: number | null;
rsOneMonth?: number | null;
rsThreeMonth?: number | null;
rsSixMonth?: number | null;
rsOneYear?: number | null;
}
export interface AlertEventRow {
id: string;
userId: string;
type: string;
severity: string;
title: string;
description: string;
symbol?: string;
createdAt: string;
acknowledged: boolean;
dedupKey: string;
payload: Record<string, unknown>;
}
export interface AlertSubscriptionRow {
id: string;
owner_id: string;
watchlist_id: string | null;
symbol: string | null;
alert_type: string;
enabled: boolean;
params: string;
last_fired: string | null;
created_at: string;
}
export interface SmtpConfigRow {
host: string;
port: number;
secure: boolean;
user: string | null;
passEnc: string | null;
fromName: string;
fromEmail: string;
enabled: boolean;
}
export interface SmtpConfigUpdate {
host?: string;
port?: number;
secure?: boolean;
user?: string | null;
pass?: string | null;
fromName?: string;
fromEmail?: string;
enabled?: boolean;
}
export interface RotationData {
sectors: SectorRotationItem[];
signal: {
strength: 'none' | 'weak' | 'moderate' | 'strong';
spread: number;
inflowAvg: number;
outflowAvg: number;
leadingSubGroup: string;
laggingSubGroup?: string;
inflowConfirmation: string;
outflowConfirmation: string;
earlyWatchCount?: number;
daysSince: number | null;
summary?: string;
};
benchmark?: string;
coveredSymbols?: number;
totalSymbols?: number;
earlyHistory?: Array<{ date: string; symbol: string; name: string; rs1w: number | null }>;
lesson?: string;
fetchedAt: string;
}
export interface MarketCondition {
regime: 'trending-up' | 'trending-down' | 'range-bound';
regimePlain?: string;
confidence: number;
factors: string[];
explanation: string;
metrics: {
spy1M: number | null;
vix: number | null;
treasury10Y: number | null;
treasury2Y: number | null;
curve10y2y: number | null;
unemployment: number | null;
};
risks: Array<{
id: string;
label: string;
severity: 'info' | 'warning' | 'elevated';
detail: string;
}>;
updatedAt: string;
lesson: string;
}
export interface SeasonalityMonth {
month: number;
monthName: string;
avgReturnPct: number;
winRate: number;
sampleYears: number;
}
export interface HorizonReturns {
oneWeek: number | null;
oneMonth: number | null;
threeMonth: number | null;
sixMonth: number | null;
oneYear: number | null;
threeYear: number | null;
fiveYear: number | null;
}
export interface TickerContext {
symbol: string;
name: string | null;
sector: string | null;
industry: string | null;
tickerKind: string;
performance: {
symbol: HorizonReturns;
vsMarket: HorizonReturns;
vsSector: HorizonReturns | null;
vsPeers1M: number | null;
marketStance1M: "outperforming" | "underperforming" | "inline" | "unknown";
sectorStance1M: "outperforming" | "underperforming" | "inline" | "unknown";
peerStance1M: "outperforming" | "underperforming" | "inline" | "unknown";
};
market: {
regime: string | null;
confidence: number | null;
spyReturns: HorizonReturns;
};
sectorContext: {
label: string | null;
etf: string | null;
etfReturns: HorizonReturns | null;
leadership: string | null;
};
themeContext: {
label: string | null;
etf: string | null;
etfReturns: HorizonReturns | null;
};
classification?: {
note: string | null;
vendorSector: string | null;
vendorIndustry: string | null;
};
peers: Array<{
symbol: string;
name: string | null;
oneMonth: number | null;
rsVsMarket1M: number | null;
}>;
summary: {
headline: string;
market: string;
sector: string;
peers: string;
synthesis: string;
};
}
export interface SeasonalityData {
symbol: string;
months: SeasonalityMonth[];
currentMonth: number;
currentMonthAvgPct: number | null;
currentMonthWinRate: number | null;
currentMonthSampleYears: number;
halfYear: {
winterAvgPct: number | null;
summerAvgPct: number | null;
whichHalf: 'winter' | 'summer';
};
electionCycle: {
year: number;
yearInCycle: 1 | 2 | 3 | 4;
label: string;
};
calendar: {
dayOfMonth: number;
nearTurnOfMonth: boolean;
quarter: 1 | 2 | 3 | 4;
nearQuarterEnd: boolean;
};
events: Array<{
id: string;
title: string;
when: string;
impact: 'high' | 'medium';
plainWhy: string;
}>;
hasEnoughHistory: boolean;
lesson: string;
}
export interface RotationAlertCheckResult {
signal: string;
spread: number;
inflowAvg: number;
outflowAvg: number;
prevSignal: string | null;
alertsCreated: number;
}
export interface SectorHolding {
symbol: string;
name: string;
weight: number;
price: number | null;
change: number | null;
changePercent: number | null;
volume: number | null;
marketCap: number | null;
returns1W: number | null;
returns1M: number | null;
returns3M: number | null;
returns6M: number | null;
returns1Y: number | null;
}
export interface SectorHoldingsResult {
symbol: string;
holdings: SectorHolding[];
/** composition source: live Yahoo, 24h cache, static fallback, or empty */
source?: "live" | "cache" | "static" | "empty";
}
export interface TruckSalesObservation {
date: string;
value: number;
}
export interface TruckSalesData {
observations: TruckSalesObservation[];
seriesTitle: string;
units: string;
configured: boolean;
error?: string;
}
export interface ManufacturingPmiData {
observations: TruckSalesObservation[];
seriesTitle: string;
units: string;
configured: boolean;
error?: string;
}
export interface XFedFeed {
cashtagPosts: XPost[];
accountPosts: XPost[];
nextCursor: string | null;
configured: boolean;
health?: { healthy: 'healthy' | 'degraded'; lastError: string | null } | null;
}
export interface MacroCommentary {
shortTerm: string;
longTerm: string;
disclaimer: string;
}
export interface Snapshot {
symbol: string;
quote: Quote | null;
candles: PriceCandle[] | null;
sector: SymbolMeta | null;
stale: { quote: boolean; candles: boolean; sector: boolean };
}
export interface AuthUser {
userId: string;
email?: string;
complexity?: string;
riskTolerance?: string;
convexityPosture?: string;
onboarded?: boolean;
modules?: string[];
}
// 8-second timeout for all tRPC calls to prevent hung requests.
const TRPC_TIMEOUT_MS = 8_000;
async function parse<T>(res: Response): Promise<T> {
if (!res.ok) {
const body = await res.text();
throw new Error(`tRPC ${res.status}: ${body}`);
}
const json = await res.json();
if (json?.error) {
throw new Error(`tRPC error: ${json.error.message ?? json.error.data?.code ?? "unknown"}`);
}
return json.result.data as T;
}
async function trpcQuery<T>(procedure: string, input?: unknown): Promise<T> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TRPC_TIMEOUT_MS);
try {
const search = input ? `?input=${encodeURIComponent(JSON.stringify(input))}` : "";
const res = await fetch(`/api/trpc/${procedure}${search}`, {
headers: { "content-type": "application/json" },
credentials: "include",
signal: controller.signal,
});
return parse<T>(res);
} catch (e) {
if (e instanceof DOMException && e.name === "AbortError") {
throw new Error(`Request timed out (${TRPC_TIMEOUT_MS / 1000}s)`);
}
throw e;
} finally {
clearTimeout(timer);
}
}
async function trpcMutate<T>(procedure: string, input: unknown): Promise<T> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TRPC_TIMEOUT_MS);
try {
const res = await fetch(`/api/trpc/${procedure}`, {
method: "POST",
headers: { "content-type": "application/json" },
credentials: "include",
body: JSON.stringify(input),
signal: controller.signal,
});
return parse<T>(res);
} catch (e) {
if (e instanceof DOMException && e.name === "AbortError") {
throw new Error(`Request timed out (${TRPC_TIMEOUT_MS / 1000}s)`);
}
throw e;
} finally {
clearTimeout(timer);
}
}
export type Complexity = "beginner" | "intermediate" | "advanced";
export interface StarterEntry { symbol: string; tickerKind: string; reason: string; }
export interface PortfolioEntry { symbol: string; qty: number; avgCost: number; acquiredAt: string; }
export interface IndicatorPeriods {
ema: number[];
rsi: number;
relvol: number;
}
export interface IndicatorOptions {
periods?: IndicatorPeriods;
timeframe?: '1d' | '1wk' | '1mo';
}
export interface IndicatorResult {
ema?: Record<number, (number | null)[]>;
rsi?: (number | null)[];
relvol?: (number | null)[];
macd?: { macdLine: (number | null)[]; signalLine: (number | null)[]; histogram: (number | null)[] };
}
// --- 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[];
total: number;
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 }),
tickerContext: (symbol: string) => trpcQuery<TickerContext>("market.tickerContext", { symbol }),
candles: async (symbol: string, timeframe = "1d") => trpcQuery<{ symbol: string; timeframe: string; candles: PriceCandle[]; isStale: boolean }>("market.candles", { symbol, timeframe }).then((r) => r?.candles ?? []),
indicators: (symbol: string, opts: IndicatorOptions) =>
trpcQuery<IndicatorResult>("market.indicators", { symbol, ...opts }),
truckSales: () => trpcQuery<TruckSalesData>("market.truckSales"),
manufacturingPmi: () => trpcQuery<ManufacturingPmiData>("market.manufacturingPmi"),
condition: () => trpcQuery<MarketCondition>("market.condition"),
rotation: () => trpcQuery<RotationData>("market.rotation"),
rotationCheckForAlert: () => trpcMutate<RotationAlertCheckResult>("market.rotationCheckForAlert", {}),
seasonality: (symbol?: string) =>
trpcQuery<SeasonalityData>("market.seasonality", symbol ? { symbol } : {}),
sectorHoldings: (etfSymbol: string) => trpcQuery<SectorHoldingsResult>("market.sectorHoldings", { etfSymbol }),
commentary: () => trpcQuery<MacroCommentary>("macro.commentary"),
},
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, opts?: { limit?: number; offset?: number }) =>
trpcQuery<Form13fResult>("edgar.form13f_holdings", {
cik,
accession,
...(opts?.limit != null ? { limit: opts.limit } : {}),
...(opts?.offset != null ? { offset: opts.offset } : {}),
}),
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 } : {}) }),
logout: () => trpcMutate<{ ok: boolean }>("auth.logout", {}),
me: () => trpcQuery<AuthUser | null>("auth.me"),
enable2fa: () => trpcMutate<{ totpSecret: string; qrUrl: string; backupCodes: string[] }>("auth.enable2fa", {}),
confirm2fa: (totp: string) => trpcMutate<{ ok: boolean }>("auth.confirm2fa", { totp }),
oauthStart: (provider: "github" | "google", redirectUri: string) => trpcMutate<{ redirectUrl: string; state: string }>("auth.oauthStart", { provider, redirectUri }),
},
onboarding: {
starter: () => trpcQuery<{ watchlist: StarterEntry[]; disclaimer: string }>("onboarding.starter"),
complete: (input: { complexity: Complexity; drawdownTolerancePct?: number; firstWatchlistSymbols?: string[]; portfolio?: PortfolioEntry[] }) =>
trpcMutate<{ ok: boolean; watchlistId: string }>("onboarding.complete", input),
},
watchlists: {
list: () => trpcQuery<WatchlistEntry[]>("watchlists.list"),
listByWatchlist: (name: string) => trpcQuery<WatchlistEntry[]>("watchlists.listByWatchlist", { name }),
listWatchlists: () => trpcQuery<Array<{ id: string; name: string; symbol_count: number; sort_order: number; created_at: string; kind?: string; class_key?: string | null; class_label?: string | null }>>("watchlists.listWatchlists"),
create: (name: string) => trpcMutate<{ id: string; name: string; symbol_count: number; sort_order: number; created_at: string; kind?: string }>("watchlists.create", { name }),
delete: (name: string) => trpcMutate<{ deleted: boolean }>("watchlists.delete", { name }),
rename: (oldName: string, newName: string) => trpcMutate<{ renamed: boolean }>("watchlists.rename", { oldName, newName }),
reorder: (orders: { id: string; sort_order: number }[]) => trpcMutate<{ ok: boolean }>("watchlists.reorder", { orders }),
moveSymbol: (symbol: string, fromWatchlist?: string, toWatchlist?: string) =>
trpcMutate<{ moved: boolean }>("watchlists.moveSymbol", { symbol, ...(fromWatchlist ? { fromWatchlist } : {}), ...(toWatchlist ? { toWatchlist } : {}) }),
addSymbol: (symbol: string, watchlistName?: string, notes?: string) =>
trpcMutate<{ added: boolean }>("watchlists.addSymbol", { symbol, ...(watchlistName ? { watchlistName } : {}), ...(notes ? { notes } : {}) }),
removeSymbol: (symbol: string, watchlistName?: string) =>
trpcMutate<{ removed: boolean }>("watchlists.removeSymbol", { symbol, ...(watchlistName ? { watchlistName } : {}) }),
},
options: {
chain: (symbol: string, opts?: { expiry?: string }) =>
trpcQuery<{ symbol: string; expiration: string | null; rows: OptionChainRow[] }>("options.chain", { symbol, ...opts }),
greeks: (symbol: string, opts?: { expiry?: string; strike?: number }) =>
trpcQuery<{ symbol: string; greeks: OptionGreeks | null; strike?: number; right?: "call" | "put"; 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 }),
optionLegs: () => trpcQuery<PortfolioOptionLeg[]>("portfolio.optionLegs"),
addOptionLeg: (input: AddOptionLegInput) =>
trpcMutate<{ id: string; legs: PortfolioOptionLeg[] }>("portfolio.addOptionLeg", input),
removeOptionLeg: (id: string) => trpcMutate<{ removed: boolean }>("portfolio.removeOptionLeg", { id }),
},
sizing: {
compute: (input: {
symbol: string;
tier: "A_STAR" | "A" | "B" | "C";
riskFraction: number;
stopPerShare: number;
equity: number;
complexity?: "beginner" | "intermediate" | "advanced";
regime?: "trending-up" | "trending-down" | "range-bound";
aStarUnlocked?: boolean;
macroOverrideReason?: string;
}) =>
trpcQuery<SizingResult>("sizing.compute", input),
},
risk: {
posture: (input: {
equity: number;
peakEquity?: number;
drawdownTolerancePct?: number;
regime?: "trending-up" | "trending-down" | "range-bound";
plans?: Array<{
symbol: string;
stopPrice?: number;
rewardTarget?: number;
cluster?: string;
}>;
}) => trpcQuery<RiskPostureResult>("risk.posture", input),
haltStatus: () =>
trpcQuery<{ active: boolean; haltedUntil: string | null; triggeredBy: string | null }>("risk.haltStatus"),
},
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 = 200) =>
trpcQuery<{ symbol: string; events: Array<Record<string, unknown>>; netShares: number; count: number }>("institutional.insiderStream", { symbol, limit }),
ownershipHistory: (symbol: string) =>
trpcQuery<{ symbol: string; months: Array<{ month: string; total_shares: number; num_filers: number }> }>("institutional.ownershipHistory", { symbol }),
buyEvents: (symbol: string) =>
trpcQuery<{ symbol: string; events: Array<{ filerName: string | null; sharesAdded: number; estimatedPrice: number; date: string }> }>("institutional.buyEvents", { symbol }),
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 }),
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; 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: {
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: {
list: (limit = 50) => trpcQuery<AlertEventRow[]>("alerts.list", { limit }),
acknowledge: (alertId: string) => trpcMutate<{ ok: boolean }>("alerts.acknowledge", { alertId }),
unackedCount: () => trpcQuery<{ count: number }>("alerts.unackedCount"),
acknowledgeAll: () => trpcMutate<{ ok: boolean }>("alerts.acknowledgeAll", {}),
createSubscription: (input: { alertType: string; watchlistId?: string; symbol?: string; params?: string }) =>
trpcMutate<AlertSubscriptionRow>("alerts.createSubscription", input),
listSubscriptions: (symbol?: string) =>
trpcQuery<AlertSubscriptionRow[]>("alerts.listSubscriptions", symbol ? { symbol } : {}),
updateSubscription: (id: string, updates: { enabled?: boolean; params?: string }) =>
trpcMutate<AlertSubscriptionRow>("alerts.updateSubscription", { id, ...updates }),
deleteSubscription: (id: string) =>
trpcMutate<{ ok: boolean }>("alerts.deleteSubscription", { id }),
},
x: {
cashtag_search: (symbol: string, limit = 30) => trpcQuery<{ posts: XPost[]; configured: boolean }>("x.cashtag_search", { symbol, limit }),
feed: (symbol: string, cursor?: string) => trpcQuery<XFedFeed>("x.feed", { symbol, limit: 10, cursor }),
accountsForSymbol: (symbol: string) => trpcQuery<Array<{id:string; handle:string; label:string}>>("x.accountsForSymbol", { symbol }),
},
admin: {
usersList: () => trpcQuery<Array<{ id: string; email: string; complexity: string; created_at: string; is_admin: number; status: string; modules: string }>>("admin.usersList"),
setUserModules: (userId: string, modules: string[]) => trpcMutate<{ ok: boolean }>("admin.setUserModules", { userId, modules }),
disableUser: (userId: string) => trpcMutate<{ ok: boolean }>("admin.disableUser", { userId }),
enableUser: (userId: string) => trpcMutate<{ ok: boolean }>("admin.enableUser", { userId }),
deleteUser: (userId: string) => trpcMutate<{ ok: boolean }>("admin.deleteUser", { userId }),
userSessions: (userId: string) => trpcQuery<Array<{ id: string; user_id: string; expires_at: string; created_at: string }>>("admin.userSessions", { userId }),
resetPassword: (email: string, tempPassword: string) => trpcMutate<{ userId: string }>("admin.resetPassword", { email, tempPassword }),
queueHealth: () => trpcQuery<Array<{ source: string; status: string; count: number; last_attempt: string | null; retry_count: number; backoff_until: string | null }>>("admin.queueHealth"),
resetBackoff: (sourceKind: string) => trpcMutate<{ cleared: number }>("admin.resetBackoff", { sourceKind }),
queueSecFetch: (symbol: string) => trpcMutate<{ ok: boolean }>("admin.queueSecFetch", { symbol }),
queueStatus: () => trpcQuery<QueueStatus>("admin.queueStatus"),
queuePause: () => trpcMutate<{ paused: boolean }>("admin.queuePause", undefined),
queueResume: () => trpcMutate<{ paused: boolean }>("admin.queueResume", undefined),
queueClearDone: (olderThanHours: number) => trpcMutate<{ cleared: number }>("admin.queueClearDone", { olderThanHours }),
queueSchedules: () => trpcQuery<ScheduleEntry[]>("admin.queueSchedules"),
queueSetSchedule: (sourceKind: string, intervalMs: number) => trpcMutate<{ ok: boolean }>("admin.queueSetSchedule", { sourceKind, intervalMs }),
queueDeleteSchedule: (sourceKind: string) => trpcMutate<{ ok: boolean }>("admin.queueDeleteSchedule", { sourceKind }),
queueLogs: (key: string, limit: number) => trpcQuery<ErrorLogEntry[]>("admin.queueLogs", { key, limit }),
queueRetryJob: (key: string) => trpcMutate<{ ok: boolean }>("admin.queueRetryJob", { key }),
queueRetrySource: (sourceKind: string) => trpcMutate<{ cleared: number }>("admin.queueRetrySource", { sourceKind }),
auditLog: (limit = 50, offset = 0, actor?: string, action?: string) =>
trpcQuery<Array<{ id: string; actor: string; action: string; target: string | null; ts: string; meta: string | null }>>("admin.auditLog", { limit, offset, ...(actor ? { actor } : {}), ...(action ? { action } : {}) }),
queueLint: (symbol: string, kind: 'sec-lint-holders' | 'sec-lint-insiders') =>
trpcMutate<LintResult>("admin.queueLint", { symbol, kind }),
queueLintAll: (input: { kind: 'sec-lint-holders' | 'sec-lint-insiders' }) =>
trpcMutate<{ total: number; results: LintResult[] }>("admin.queueLintAll", input),
xCredentialsStatus: () => trpcQuery<XCredentialsStatus>("admin.xCredentialsStatus"),
xCredentialsSet: (ct0: string, auth_token: string) => trpcMutate<{ ok: boolean }>("admin.xCredentialsSet", { ct0, auth_token }),
fredKeyStatus: () => trpcQuery<{ configured: boolean }>("admin.fredKeyStatus"),
fredKeySet: (apiKey: string) => trpcMutate<{ ok: boolean }>("admin.fredKeySet", { apiKey }),
xAccountsList: () => trpcQuery<XAccountRow[]>("admin.xAccountsList"),
xAccountAdd: (symbol: string, handle: string, label?: string) => trpcMutate<{ id: string }>("admin.xAccountAdd", { symbol, handle, ...(label ? { label } : {}) }),
xAccountRemove: (id: string) => trpcMutate<{ ok: boolean }>("admin.xAccountRemove", { id }),
xPrune: (olderThanDays?: number) => trpcMutate<{ deleted: number }>("admin.xPrune", { olderThanDays: olderThanDays ?? 30 }),
pendingUsers: () => trpcQuery<Array<{ id: string; email: string; complexity: string; created_at: string }>>("admin.pendingUsers"),
approveUser: (userId: string) => trpcMutate<{ ok: boolean }>("admin.approveUser", { userId }),
rejectUser: (userId: string) => trpcMutate<{ ok: boolean }>("admin.rejectUser", { userId }),
dataQualityList: (input?: { symbol?: string; kind?: 'institution_filings' | 'insider_transactions' }) =>
trpcQuery<DataQualityRow[]>("admin.dataQualityList", input ?? {}),
serverRestart: (target: 'backend' | 'frontend' | 'all' = 'all') =>
trpcMutate<{ ok: boolean; backend: string; frontend: string }>("admin.serverRestart", { target }),
smtpConfig: () => trpcQuery<SmtpConfigRow>("admin.smtpConfig"),
smtpConfigUpdate: (input: SmtpConfigUpdate) =>
trpcMutate<{ ok: boolean }>("admin.smtpConfigUpdate", input),
smtpConfigTest: () => trpcMutate<{ ok: boolean }>("admin.smtpConfigTest", {}),
},
};
export interface LintResult {
symbol: string;
kind: string;
discoveredCount: number;
storedCount: number;
missingCount: number;
backfilled: number;
stale: number;
status: 'ok' | 'gaps_found' | 'stale' | 'error';
detail: Record<string, unknown>;
}
export interface DataQualityRow {
symbol: string;
kind: string;
last_checked_at: string;
stored_count: number;
discovered_count: number;
missing_count: number;
stale: number;
status: string;
detail: string;
}
export interface WatchlistEntry {
symbol: string;
added_at: string;
notes?: string | null;
}
export interface PortfolioHolding {
symbol: string;
shares: number;
avg_cost: number;
added_at: string;
}
export type OptionRight = "call" | "put";
export type OptionSide = "long" | "short";
export type OptionRole =
| "long_call"
| "long_put"
| "covered_call"
| "cash_secured_put"
| "other";
export interface PortfolioOptionLeg {
id: string;
underlying: string;
right: OptionRight;
side: OptionSide;
strike: number;
expiry: string;
contracts: number;
premium: number;
multiplier: number;
role: OptionRole;
acquired_at: string;
note: string | null;
}
export interface AddOptionLegInput {
underlying: string;
right: OptionRight;
side: OptionSide;
strike: number;
expiry: string;
contracts: number;
premium: number;
multiplier?: number;
role?: OptionRole;
note?: string;
}
export interface OptionRiskSummary {
legsCount: number;
premiumAtRiskUsd: number;
cashReservedUsd: number;
creditReceivedUsd: number;
capitalCommittedUsd: number;
uncoveredShortCallCount: number;
byUnderlying: Record<
string,
{
premiumAtRiskUsd: number;
cashReservedUsd: number;
creditReceivedUsd: number;
legs: number;
uncoveredShortCalls: number;
}
>;
}
export interface SizingLayer {
name: string;
value: number;
explanation: string;
}
export interface SizingResult {
shares: number;
layers: SizingLayer[];
explanations: string[];
blocked: boolean;
blockReason: string | null;
override: "macro" | "cluster" | null;
halted?: boolean;
footer?: string;
}
export interface RiskRecommendation {
id: string;
tradeOff: string;
explanation: string;
severity: "warning" | "info";
}
export interface RiskPostureResult {
asymmetry: number;
clusterExposure: Record<string, number>;
clusterCaps?: Record<string, number>;
drawdownVsTolerance: {
currentDrawdownPct: number;
tolerancePct: number;
status: -1 | 0 | 1;
};
halted: boolean;
recommendedActions: RiskRecommendation[];
halt: { haltedUntil: string; triggeredBy: string; ts: string } | null;
holdingsCount: number;
totalExposureUsd: number;
options?: OptionRiskSummary;
footer: string;
}
export interface SourceCooldown {
source: string;
until: string | null;
remainingMs: number;
consecutiveHits: number;
active: boolean;
}
export interface QueueStatus {
paused: boolean;
queued: number;
in_flight: number;
failed: number;
backoff: number;
done: number;
backoff_until: string | null;
last_error: string | undefined;
counts: Record<string, number>;
lastErrors: Array<{ key: string; error: string | null }>;
/** Active vendor cool-downs after 429 (ADR-0009). */
sourceCooldowns?: SourceCooldown[];
}
export interface ScheduleEntry {
sourceKind: string;
intervalMs: number;
lastEnqueued: string | null;
nextEnqueue: string | null;
}
export interface ErrorLogEntry {
attempt: number;
errorMessage: string | null;
errorStack: string | null;
ts: string;
}