feat: dealer flow, mirror portfolio (M21), options convexity, FINRA short interest, alert producers, vendor gate
CI / Test & Type-Check (push) Canceled after 0s

Snapshot of in-progress module work across multiple slices:

- Dealer Flow: dealerExposureEngine, dealerMapService, dealerMapExplain,
  dealerMapIntegrity, dealerMapReplay, dealerStudyEngine, hanStyleLevels
- Mirror Portfolio (M21): fundRepository, captureIngest, mirrorAlertProducers,
  fund holdings strip, live book, position capture ingest
- Options: BSM, NormalizedOptionSurface types, OptionsChainRouter,
  ConvexityGate, option legs panel
- Alert producers: vixLevel, rotation, thesis, unlock, portfolioRisk,
  mirror (fund_capture, fund_13f, mirror_diff)
- FINRA short interest adapter + queue integration
- SEC company tickers adapter + ingest (symbol search index seed)
- Vendor gate (rate-limit-first data plane, ADR-0009)
- CUSIP registry, reverse 13F refresh, stock float service
- LRU cache, portfolio backtest engine
- Frontend: dealer-flow, funds, journal, lab, monitor, plan, portfolio,
  reports, screener, strategies, theses, guided-start, exits, more pages
- Volume profile, workspace profile, visibility-aware poll
- ADRs 0010 (mirror math not advice), 0011 (symbol search index)
- VENDOR_INTEGRATIONS.md, END_USER_TEST.md
- .gitignore: exclude DBs, .DS_Store, local config, agent scratch
This commit is contained in:
Investor Flow Build
2026-08-10 13:36:26 -04:00
parent 04fc11b2fd
commit ac94acf9e3
229 changed files with 32617 additions and 3934 deletions
+790 -36
View File
@@ -13,6 +13,10 @@ export interface Quote {
change?: number | null;
changePercent?: number | null;
iv?: number | null;
/** Session of the displayed last print. */
session?: 'regular' | 'pre' | 'post' | null;
/** RTH last when price is an extended print. */
regularPrice?: number | null;
}
export interface PriceCandle {
ts: string;
@@ -40,6 +44,25 @@ export interface SymbolMeta {
peers?: string[] | null;
}
export interface SymbolSearchResult {
symbol: string;
name: string | null;
sector: string | null;
industry: string | null;
exchange: string | null;
ticker_kind: string;
cik: string | null;
}
export interface FundHoldersResult {
symbol: string;
tracked: Array<{
fundId: string; fundName: string; shares: number | null; valueUsd: number | null;
asOf: string; source: string; weightPct: number | null;
}>;
institutionCount: number;
}
export interface XPost {
post_id: string;
author_handle: string;
@@ -58,6 +81,261 @@ export interface XAccountRow {
created_at: string;
}
// ─── Dealer Flow types ─────────────────────────────────────────────────────
export interface DealerMapCell {
strike: number;
expiry: string;
gex: number;
vex: number;
oi: number;
volume: number;
polarity: "pos" | "neg" | "near_zero";
isKing: boolean;
}
export interface DealerMapLevels {
king: { strike: number; expiry: string; gex: number } | null;
gatekeepers: Array<{ strike: number; absGex: number }>;
flip: number | null;
callWall: number | null;
putWall: number | null;
airPockets: Array<{ from: number; to: number }>;
clusters: Array<{ strikes: number[]; absGexSum: number }>;
}
export interface DealerMapEducator {
headline: string;
regime: string;
levels: string[];
scenario: string;
patterns: string[];
questions: string[];
caveats: string[];
footer: string;
}
export interface DealerMapPayload {
symbol: string;
spot: number;
asOf: string;
delayNote: string;
delaySeconds: number | null;
oiFreshness: string;
providerId: string;
convention: string;
unit: string;
computation: string;
expiries: string[];
cells: DealerMapCell[];
byStrike: Array<{ strike: number; gex: number; vex: number; absGex: number }>;
netVexByStrike: Array<{ strike: number; vex: number }>;
levels: DealerMapLevels;
regimeAtSpot: "positive" | "negative" | "mixed";
scenario: string;
patterns: Array<{ id: string; confidence?: number; [k: string]: unknown }>;
velocity?: {
asOfPrev: string;
nodes: Array<{ strike: number; growing: boolean; deltaAbsGex: number }>;
growingNearAirPocket: boolean;
} | null;
opexHints: { isNearOpex: boolean; note: string };
coveragePct: number;
bsmFillPct: number;
quality?: {
greekQuality: "ok" | "degraded" | "poor";
oiEligible: number;
included: number;
skippedBadIv: number;
bsmFills: number;
fallbackIvUses: number;
bsmFillPct: number;
badIvSkipPct: number;
fallbackIvPct: number;
notes: string[];
};
}
export interface DealerIntegrityReport {
status: "complete" | "degraded" | "incomplete";
checks: Array<{ id: string; level: "hard" | "soft"; ok: boolean; detail: string }>;
hardFailed: string[];
softFailed: string[];
metrics: {
expiries: number;
oiEligible: number;
included: number;
cells: number;
byStrike: number;
nearSpotOiStrikes: number;
minOiStrikesPerExpiry: number;
includePct: number;
badIvSkipPct: number;
bsmFillPct: number;
};
}
/** Han-style day script under the active GEX convention. */
export interface HanStyleLevels {
symbol: string;
spot: number;
asOf: string;
convention: string;
script: string;
chips: string[];
longSupport: number | null;
shortKing: number | null;
resistance: number | null;
opexPath: number[];
regimeLabel: "long_gamma" | "short_gamma" | "mixed";
}
export interface DealerMapGetResult {
map: DealerMapPayload | null;
educator: DealerMapEducator | null;
/** Support / resist / short king / OPEX path (updates with Classic vs Dealer toggle). */
hanScript?: HanStyleLevels | null;
status: "ok" | "stale" | "pending" | "empty" | "incomplete";
isStale: boolean;
scheduled: boolean;
providerId: string;
message?: string;
integrity?: DealerIntegrityReport;
}
export interface DealerMapLevelsResult {
symbol: string;
status: string;
scheduled: boolean;
levels: DealerMapLevels | null;
spot: number | null;
delayNote: string | null;
providerId: string;
regimeAtSpot?: string;
scenario?: string;
patterns?: string[];
}
export interface DealerMapScenarioResult {
symbol: string;
status: string;
scenario: string;
regimeAtSpot: string;
patterns: Array<{ id: string; confidence?: number }>;
educator: DealerMapEducator | null;
delayNote: string | null;
providerId: string;
}
export interface DealerMapVelocityResult {
symbol: string;
status: string;
velocity: DealerMapPayload["velocity"];
message: string | null;
}
export interface DealerMapExplainResult {
ok: boolean;
source: "llm" | "layer0_fallback" | "rate_limited" | "no_endpoint" | "error";
headline: string;
sections: {
whatMapShows: string;
keyLevels: string[];
scenarioInPlainEnglish: string;
questionsToAsk: string[];
caveats: string[];
dataFreshness: string;
};
model?: string;
cached?: boolean;
error?: string;
}
/** Study Desk practice setups. */
export interface DealerStudyProposal {
hypothesisId: string;
title: string;
bias: "educational_long" | "educational_short" | "educational_range";
mapReading: string;
studyQuestion: string;
entry: number;
invalidation: number;
target: number;
horizonDays: number;
confidence: number;
disclaimer: string;
histWinRate?: number | null;
histN?: number;
histAvgScore?: number | null;
mentorWinRate?: number | null;
mentorN?: number;
mentorClaimType?: string | null;
rankScore?: number;
}
export interface DealerStudyRow {
id: string;
symbol: string;
hypothesis_id: string;
title: string;
bias: string;
map_reading: string;
study_question: string;
entry: number;
invalidation: number;
target: number;
horizon_days: number;
confidence: number | null;
status: string;
grade: string | null;
score: number | null;
grade_summary: string | null;
grade_path: string | null;
graded_at: string | null;
logged_at: string;
disclaimer: string;
}
export interface DealerStudyScorecardRow {
hypothesisId: string;
n: number;
wins: number;
losses: number;
expired: number;
incomplete: number;
winRate: number | null;
avgScore: number | null;
}
export interface MentorCallRow {
id: string;
handle: string;
post_id: string;
posted_at: string | null;
symbol: string | null;
bias: string;
claim_type: string;
entry: number;
invalidation: number;
target: number;
horizon_days: number;
excerpt: string | null;
extract_confidence: number | null;
status: string;
grade: string | null;
score: number | null;
grade_summary: string | null;
logged_at: string;
}
export interface UserLlmStatus {
configured: boolean;
baseUrl: string | null;
model: string | null;
keyHint: string | null;
updatedAt: string | null;
}
export interface XCredentialsStatus {
configured: boolean;
healthy: 'healthy' | 'degraded';
@@ -71,7 +349,11 @@ export interface SectorRotationItem {
/** Legacy: inflow ≈ leading, outflow ≈ lagging (RS vs SPY, not abs flow). */
group: 'outflow' | 'inflow';
subGroup: string;
kind?: 'sector' | 'style' | 'region' | 'thematic';
kind?: 'sector' | 'style' | 'region' | 'thematic' | 'custom' | 'auto';
/** True when this ETF is a sector/theme proxy for at least one open portfolio holding. */
portfolioLinked?: boolean;
/** Holding symbols that map into this ETF (Auto filter provenance). */
portfolioHoldings?: string[];
leadership?: 'leading' | 'lagging' | 'inline' | 'unknown';
earlyWatch?: boolean;
rank1M?: number | null;
@@ -82,11 +364,13 @@ export interface SectorRotationItem {
threeMonth: number | null;
sixMonth: number | null;
oneYear: number | null;
oneDay?: number | null;
rsOneWeek?: number | null;
rsOneMonth?: number | null;
rsThreeMonth?: number | null;
rsSixMonth?: number | null;
rsOneYear?: number | null;
rsOneDay?: number | null;
}
export interface AlertEventRow {
@@ -115,6 +399,33 @@ export interface AlertSubscriptionRow {
created_at: string;
}
export interface AlertTypeToggleRow {
type: string;
label: string;
description: string;
enabled: boolean;
}
export interface ProducerRunStatusRow {
producer: string;
frequency: string;
lastRunAt: string | null;
lastEventsFired: number;
lastDurationMs: number;
lastError: string | null;
}
export interface AlertStatusRow {
producers: ProducerRunStatusRow[];
subscriptionsByType: Array<{ alertType: string; enabledCount: number }>;
freshness: {
vixLevelAgeSec: number | null;
latestScFilingAt: string | null;
latestInsiderAt: string | null;
oldestQuoteAgeSec: number | null;
};
}
export interface SmtpConfigRow {
host: string;
port: number;
@@ -173,6 +484,8 @@ export interface MarketCondition {
treasury2Y: number | null;
curve10y2y: number | null;
unemployment: number | null;
consumerSentiment?: number | null;
context?: Record<string,string>;
};
risks: Array<{
id: string;
@@ -368,15 +681,38 @@ export interface AuthUser {
convexityPosture?: string;
onboarded?: boolean;
modules?: string[];
experienceStage?: string | null;
goal?: string | null;
horizon?: string | null;
density?: string | null;
jargonComfort?: string | null;
}
// 8-second timeout for all tRPC calls to prevent hung requests.
// Default short timeout for normal cache/UI calls. LLM paths need much longer
// (Ornith reasoning models often take 30–120s).
const TRPC_TIMEOUT_MS = 8_000;
/** OpenAI-compatible explain / test against local reasoning models. */
const TRPC_LLM_TIMEOUT_MS = 180_000;
async function parse<T>(res: Response): Promise<T> {
if (!res.ok) {
const body = await res.text();
throw new Error(`tRPC ${res.status}: ${body}`);
// Prefer structured proxy / tRPC error message over raw HTML "Internal Server Error".
try {
const j = JSON.parse(body) as {
error?: { message?: string; data?: { code?: string } };
message?: string;
};
const msg =
j?.error?.message ??
j?.message ??
(body.trim() || res.statusText);
throw new Error(`tRPC ${res.status}: ${msg}`);
} catch (e) {
if (e instanceof Error && e.message.startsWith("tRPC ")) throw e;
const snippet = body.replace(/\s+/g, " ").trim().slice(0, 200);
throw new Error(`tRPC ${res.status}: ${snippet || res.statusText}`);
}
}
const json = await res.json();
if (json?.error) {
@@ -385,9 +721,13 @@ async function parse<T>(res: Response): Promise<T> {
return json.result.data as T;
}
async function trpcQuery<T>(procedure: string, input?: unknown): Promise<T> {
async function trpcQuery<T>(
procedure: string,
input?: unknown,
timeoutMs: number = TRPC_TIMEOUT_MS,
): Promise<T> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TRPC_TIMEOUT_MS);
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const search = input ? `?input=${encodeURIComponent(JSON.stringify(input))}` : "";
const res = await fetch(`/api/trpc/${procedure}${search}`, {
@@ -398,7 +738,7 @@ async function trpcQuery<T>(procedure: string, input?: unknown): Promise<T> {
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 new Error(`Request timed out (${Math.round(timeoutMs / 1000)}s)`);
}
throw e;
} finally {
@@ -406,9 +746,13 @@ async function trpcQuery<T>(procedure: string, input?: unknown): Promise<T> {
}
}
async function trpcMutate<T>(procedure: string, input: unknown): Promise<T> {
async function trpcMutate<T>(
procedure: string,
input: unknown,
timeoutMs: number = TRPC_TIMEOUT_MS,
): Promise<T> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), TRPC_TIMEOUT_MS);
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(`/api/trpc/${procedure}`, {
method: "POST",
@@ -420,7 +764,7 @@ async function trpcMutate<T>(procedure: string, input: unknown): Promise<T> {
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 new Error(`Request timed out (${Math.round(timeoutMs / 1000)}s)`);
}
throw e;
} finally {
@@ -501,6 +845,8 @@ export interface FilingsSearchResult {
export const api = {
market: {
snapshot: (symbol: string) => trpcQuery<Snapshot>("market.snapshot", { symbol }),
/** Batched snapshot for multiple symbols — reduces N+1 API calls */
snapshots: (symbols: string[]) => trpcQuery<Array<{ symbol: string; quote: Snapshot['quote'] | null; candles: PriceCandle[] | null; sector: SymbolMeta | null; stale: { quote: boolean; candles: boolean; sector: boolean } }>>("market.snapshots", { symbols }),
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) =>
@@ -509,6 +855,12 @@ export const api = {
manufacturingPmi: () => trpcQuery<ManufacturingPmiData>("market.manufacturingPmi"),
condition: () => trpcQuery<MarketCondition>("market.condition"),
rotation: () => trpcQuery<RotationData>("market.rotation"),
addCustomEtf: (symbol: string, opts?: { name?: string; group?: string }) =>
trpcMutate<{ added: boolean }>("market.addCustomEtf", { symbol, ...opts }),
removeCustomEtf: (symbol: string) =>
trpcMutate<{ removed: boolean }>("market.removeCustomEtf", { symbol }),
listCustomEtfs: () =>
trpcQuery<Array<{ symbol: string; name: string; grp: string; created_at: string }>>("market.listCustomEtfs"),
rotationCheckForAlert: () => trpcMutate<RotationAlertCheckResult>("market.rotationCheckForAlert", {}),
seasonality: (symbol?: string) =>
trpcQuery<SeasonalityData>("market.seasonality", symbol ? { symbol } : {}),
@@ -534,6 +886,96 @@ export const api = {
form4_tx: (cik: string, accession: string) =>
trpcQuery<{ transactions: Form4Result }>("edgar.form4_tx", { cik, accession }),
},
funds: {
list: () => trpcQuery<Array<{
id: string; ci_key: string; fund_name: string; manager_name: string;
x_handle: string | null; paywall_status: string; enabled: boolean;
}>>("funds.list", {}),
get: (input: { id: string }) => trpcQuery<{
id: string; ci_key: string; fund_name: string; manager_name: string;
x_handle: string | null; paywall_status: string; enabled: boolean;
} | null>("funds.get", input),
liveBook: (input: { fundId: string }) =>
trpcQuery<Array<{
symbol: string; shares: number | null; value_usd: number | null;
cost_basis: number | null; as_of: string; source: string; evidence_url: string | null;
notes: string | null; post_text: string | null;
}>>("funds.liveBook", input),
adminCreate: (input: {
ci_key: string; fund_name: string; manager_name: string; x_handle?: string;
paywall_status?: "unknown" | "open" | "paywalled"; enabled?: boolean;
}) => trpcMutate<{ id: string; ci_key: string; fund_name: string; manager_name: string; x_handle: string | null; paywall_status: string; enabled: boolean }>("funds.adminCreate", input),
adminDelete: (input: { id: string }) => trpcMutate<{ deleted: boolean }>("funds.adminDelete", input),
adminSetEnabled: (input: { id: string; enabled: boolean }) => trpcMutate<{ updated: boolean }>("funds.adminSetEnabled", input),
adminSync13f: (input: { fundId: string }) => trpcMutate<{
fetched_filings: number; holdings_seen: number; symbols_upserted: number;
records_inserted: number; unresolved: Array<{ cusip: string; issuerName: string }>;
}>("funds.adminSync13f", input),
adminIngestCaptures: (input: { fundId: string }) => trpcMutate<{
fundId: string; postsScanned: number; captures: number; inserted: number;
refreshed: number; claims: number; skipped: number; disabled: boolean;
}>("funds.adminIngestCaptures", input),
},
symbols: {
search: (input: { q: string; limit?: number }) => trpcQuery<{
q: string;
results: Array<{
symbol: string; name: string | null; sector: string | null; industry: string | null;
exchange: string | null; ticker_kind: string; cik: string | null;
}>;
}>("symbols.search", input),
holders: (input: { symbol: string }) => trpcQuery<{
symbol: string;
tracked: Array<{
fundId: string; fundName: string; shares: number | null; valueUsd: number | null;
asOf: string; source: string; weightPct: number | null;
}>;
institutionCount: number;
}>("symbols.holders", input),
},
mirror: {
diff: (input: {
fundId: string; base?: number; locked?: boolean;
minPositionUsd?: number; minWeightPct?: number; tickSize?: number;
}) => trpcQuery<{
base: number; locked: boolean; book_value: number; placed_value: number;
rows: Array<{
symbol: string; weight: number; fund_avg_cost: number | null; target_value: number;
target_qty: number | null; actual_qty: number; actual_value: number;
delta_qty: number; delta_value: number; message: string; excluded: boolean;
}>;
excluded: Array<{ symbol: string; weight: number; fund_avg_cost: number | null; target_value: number; target_qty: number | null; actual_qty: number; actual_value: number; delta_qty: number; delta_value: number; message: string; excluded: boolean }>;
excluded_tail: { count: number; value_usd: number } | null;
}>("mirror.diff", input),
},
sectorCrosslink: {
confirm: (input: { strategyId: string; sector: string }) => trpcQuery<{
strategyId: string;
sector: string;
matchedSymbols: Array<{
symbol: string; convictionStrength: number; conditionsFired: string[]; oneTapWorkbenchLink: string;
}>;
rotationSignal: { fromSector: string; toSector: string; confidence: number; date: string } | null;
sectorConfirmationReason: string;
educationalDisclaimer: string;
}>("sectorCrosslink.confirm", input),
},
reddit: {
subreddit: (input: { subreddit: string; limit?: number }) => trpcQuery<{
posts: Array<{
post_id: string; author_handle: string; subreddit: string; body_text: string | null;
posted_at: string; engagement: number; sentiment_score?: number | null; attribution?: string | null;
}>;
isStale: boolean;
}>("reddit.subreddit", input),
search: (input: { q: string; limit?: number }) => trpcQuery<{
posts: Array<{
post_id: string; author_handle: string; subreddit: string; body_text: string | null;
posted_at: string; engagement: number; sentiment_score?: number | null; attribution?: string | null;
}>;
isStale: boolean;
}>("reddit.search", input),
},
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 } : {}) }),
@@ -544,9 +986,39 @@ export const api = {
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),
starter: (input?: { density?: "focused" | "standard" | "full"; experienceStage?: "never_invested" | "some_stocks" | "active_self_directed" }) =>
trpcQuery<{ watchlist: StarterEntry[]; density: string; disclaimer: string }>("onboarding.starter", input ?? {}),
complete: (input: {
experienceStage?: "never_invested" | "some_stocks" | "active_self_directed";
goal?: "grow" | "income" | "protect";
horizon?: "short" | "medium" | "long";
density?: "focused" | "standard" | "full";
jargonComfort?: "plain" | "mixed" | "technical";
complexity?: Complexity;
drawdownTolerancePct?: number;
firstWatchlistSymbols?: string[];
portfolio?: PortfolioEntry[];
}) =>
trpcMutate<{ ok: boolean; watchlistId: string; density?: string; complexity?: string; experienceStage?: string }>(
"onboarding.complete",
input,
),
updateProfile: (input: {
experienceStage?: "never_invested" | "some_stocks" | "active_self_directed";
goal?: "grow" | "income" | "protect";
horizon?: "short" | "medium" | "long";
density?: "focused" | "standard" | "full";
jargonComfort?: "plain" | "mixed" | "technical";
}) =>
trpcMutate<{
ok: boolean;
experienceStage: string;
goal: string;
horizon: string;
density: string;
jargonComfort: string;
complexity: string;
}>("onboarding.updateProfile", input),
},
watchlists: {
list: () => trpcQuery<WatchlistEntry[]>("watchlists.list"),
@@ -569,9 +1041,148 @@ export const api = {
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 }),
},
dealerMap: {
get: (
symbol: string,
opts?: {
forceSchedule?: boolean;
maxExpiries?: number;
convention?: "classic_call_pos_put_neg" | "dealer_inventory";
},
) => trpcQuery<DealerMapGetResult>("dealerMap.get", { symbol, ...opts }),
levels: (symbol: string) =>
trpcQuery<DealerMapLevelsResult>("dealerMap.levels", { symbol }),
scenario: (symbol: string) =>
trpcQuery<DealerMapScenarioResult>("dealerMap.scenario", { symbol }),
velocity: (symbol: string) =>
trpcQuery<DealerMapVelocityResult>("dealerMap.velocity", { symbol }),
explain: (symbol: string) =>
trpcMutate<DealerMapExplainResult>(
"dealerMap.explain",
{ symbol },
TRPC_LLM_TIMEOUT_MS,
),
},
dealerStudy: {
propose: (symbol: string, opts?: { useMentorHistory?: boolean }) =>
trpcQuery<{
symbol: string;
proposals: DealerStudyProposal[];
message: string | null;
status: string;
useMentorHistory?: boolean;
}>("dealerStudy.propose", {
symbol,
useMentorHistory: opts?.useMentorHistory === true,
}),
log: (input: DealerStudyProposal & { symbol: string }) =>
trpcMutate<{ ok: boolean; id: string; loggedAt: string }>("dealerStudy.log", {
symbol: input.symbol,
hypothesisId: input.hypothesisId,
title: input.title,
bias: input.bias,
mapReading: input.mapReading,
studyQuestion: input.studyQuestion,
entry: input.entry,
invalidation: input.invalidation,
target: input.target,
horizonDays: input.horizonDays,
confidence: input.confidence,
disclaimer: input.disclaimer,
}),
list: (opts?: { symbol?: string; limit?: number }) =>
trpcQuery<DealerStudyRow[]>("dealerStudy.list", opts ?? {}),
grade: (id: string) =>
trpcMutate<{
ok: boolean;
grade: string;
score: number;
summary: string;
pathNotes: string[];
status: string;
}>("dealerStudy.grade", { id }),
gradeDue: () =>
trpcMutate<{ ok: boolean; considered: number; graded: number; incomplete: number }>(
"dealerStudy.gradeDue",
{},
),
scorecard: () => trpcQuery<DealerStudyScorecardRow[]>("dealerStudy.scorecard"),
promoteToJournal: (studyId: string) =>
trpcMutate<{ ok: boolean; tradeId: string; message: string }>("dealerStudy.promoteToJournal", {
studyId,
}),
exportCsv: (opts?: { symbol?: string; limit?: number }) =>
trpcQuery<{ filename: string; csv: string; rowCount: number }>(
"dealerStudy.exportCsv",
opts ?? {},
),
},
mentorLedger: {
importFromHarvest: (opts?: { handle?: string; maxFiles?: number }) =>
trpcMutate<{
ok: boolean;
imported: number;
skipped: number;
filesSeen?: number;
message: string | null;
}>("mentorLedger.importFromHarvest", opts ?? {}),
list: (opts?: { handle?: string; status?: string; limit?: number }) =>
trpcQuery<MentorCallRow[]>("mentorLedger.list", opts ?? {}),
confirm: (input: {
id: string;
entry?: number;
invalidation?: number;
target?: number;
bias?: "educational_long" | "educational_short" | "educational_range";
horizonDays?: number;
symbol?: string;
}) => trpcMutate<{ ok: boolean }>("mentorLedger.confirm", input),
discard: (id: string) => trpcMutate<{ ok: boolean }>("mentorLedger.discard", { id }),
grade: (id: string) =>
trpcMutate<{
ok: boolean;
grade: string;
score: number;
summary: string;
status: string;
}>("mentorLedger.grade", { id }),
gradeDue: () =>
trpcMutate<{ ok: boolean; considered: number; graded: number; incomplete: number }>(
"mentorLedger.gradeDue",
{},
),
scorecard: (handle?: string) =>
trpcQuery<
Array<{
handle: string;
claimType: string;
n: number;
wins: number;
losses: number;
expired: number;
incomplete: number;
winRate: number | null;
avgScore: number | null;
}>
>("mentorLedger.scorecard", handle ? { handle } : {}),
},
userLlm: {
status: () => trpcQuery<UserLlmStatus>("userLlm.status"),
upsertEndpoint: (input: { baseUrl: string; apiKey?: string; model?: string; keepExistingKey?: boolean }) =>
trpcMutate<UserLlmStatus>("userLlm.upsertEndpoint", input),
clear: () => trpcMutate<{ ok: boolean }>("userLlm.clear", {}),
test: (input?: { baseUrl?: string; apiKey?: string; model?: string }) =>
trpcMutate<{ ok: true; model: string; sample: string } | { ok: false; error: string }>(
"userLlm.test",
input ?? {},
TRPC_LLM_TIMEOUT_MS,
),
},
portfolio: {
holdings: () => trpcQuery<PortfolioHolding[]>("portfolio.holdings"),
addHolding: (symbol: string, shares: number, avgCost: number) => trpcMutate<{ created: boolean }>("portfolio.addHolding", { symbol, shares, avgCost }),
updateHolding: (symbol: string, opts: { shares?: number; avgCost?: number }) =>
trpcMutate<{ updated: boolean }>("portfolio.updateHolding", { symbol, ...opts }),
removeHolding: (symbol: string) => trpcMutate<{ removed: boolean }>("portfolio.removeHolding", { symbol }),
optionLegs: () => trpcQuery<PortfolioOptionLeg[]>("portfolio.optionLegs"),
addOptionLeg: (input: AddOptionLegInput) =>
@@ -605,8 +1216,6 @@ export const api = {
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", {}),
@@ -623,7 +1232,7 @@ export const api = {
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; discrepancyPct: number | null; discrepancyBetween: string | null; sources: { yfinance: { available: boolean; stale: boolean }; nasdaq: { available: boolean; stale: boolean }; finra: { available: boolean; stale: boolean } }; stale: boolean }>("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; finraBiweeklyHistory: Array<{ settlementDate: string; currentShortPosition: number; previousShortPosition: number | null; daysToCover: number | null; changePercent: number | null }>; finraWeekly: Array<{ weekEnding: string; shortVolume: number; totalVolume: number; shortPct: number }>; discrepancyPct: number | null; discrepancyBetween: string | null; sources: { yfinance: { available: boolean; stale: boolean }; nasdaq: { available: boolean; stale: boolean }; finra: { available: boolean; stale: boolean } }; stale: boolean }>("institutional.shortInterest", { symbol }),
},
emotionLogger: {
add: (input: { tradeExecutionId: string; priceAtEvent?: number; emotion: string; note?: string }) =>
@@ -646,6 +1255,9 @@ export const api = {
trpcMutate<AlertSubscriptionRow>("alerts.updateSubscription", { id, ...updates }),
deleteSubscription: (id: string) =>
trpcMutate<{ ok: boolean }>("alerts.deleteSubscription", { id }),
listTypes: () => trpcQuery<AlertTypeToggleRow[]>("alerts.listTypes"),
toggleType: (alertType: string, enabled: boolean) =>
trpcMutate<AlertTypeToggleRow[]>("alerts.toggleType", { alertType, enabled }),
},
x: {
cashtag_search: (symbol: string, limit = 30) => trpcQuery<{ posts: XPost[]; configured: boolean }>("x.cashtag_search", { symbol, limit }),
@@ -661,8 +1273,27 @@ export const api = {
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"),
alertStatus: () => trpcQuery<AlertStatusRow>("admin.alertStatus"),
resetBackoff: (sourceKind: string) => trpcMutate<{ cleared: number }>("admin.resetBackoff", { sourceKind }),
queueSecFetch: (symbol: string) => trpcMutate<{ ok: boolean }>("admin.queueSecFetch", { symbol }),
dealerMapIntegrity: (input: { symbol: string; asOf?: string; recompute?: boolean }) =>
trpcMutate<{
mode: "live" | "replay";
symbol: string;
ok: boolean;
summary: string;
integrityText: string;
integrity: DealerIntegrityReport;
king: { strike: number; expiry: string; gex: number } | null;
spot: number | null;
asOf?: string;
spotSource?: string;
surfaceRowCount?: number;
keptPriorMap?: boolean;
cells?: number;
quality?: unknown;
snapshotCompare?: unknown;
}>("admin.dealerMapIntegrity", input),
queueStatus: () => trpcQuery<QueueStatus>("admin.queueStatus"),
queuePause: () => trpcMutate<{ paused: boolean }>("admin.queuePause", undefined),
queueResume: () => trpcMutate<{ paused: boolean }>("admin.queueResume", undefined),
@@ -678,7 +1309,7 @@ export const api = {
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),
trpcMutate<{ total: number; enqueued: number }>("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"),
@@ -699,8 +1330,143 @@ export const api = {
trpcMutate<{ ok: boolean }>("admin.smtpConfigUpdate", input),
smtpConfigTest: () => trpcMutate<{ ok: boolean }>("admin.smtpConfigTest", {}),
},
strategies: {
list: () => trpcQuery<Array<{ id: string; name: string; components: string; unlocked: boolean; created_at: string }>>("strategies.list"),
create: (input: { name: string; components?: unknown[] }) => trpcMutate<{ id: string }>("strategies.create", input),
get: (id: string) => trpcQuery<Record<string, unknown>>("strategies.get", { id }),
listPresets: () => trpcQuery<StrategyPreset[]>("strategies.listPresets"),
getPreset: (id: string) => trpcQuery<StrategyPreset>("strategies.getPreset", { id }),
forkPreset: (presetId: string) => trpcMutate<{ id: string; name: string }>("strategies.forkPreset", { presetId }),
suggestTickers: (presetId: string) => trpcQuery<TickerSuggestion[]>("strategies.suggestTickers", { presetId }),
},
backtest: {
run: (input: { strategyId: string; symbol: string; timeframe?: string }) =>
trpcQuery<{ equityCurve: Array<{ ts: string; equity: number }>; exitReasons: string[]; sampleSize: number }>("backtest.run", input),
evaluateLatest: (input: { strategyId: string; symbol: string }) =>
trpcQuery<{ fires: boolean; conditions: Record<string, boolean | null> }>("backtest.evaluateLatest", input),
runPortfolio: (input: { allocations: Array<{ symbol: string; targetPct: number }>; rebalanceFreq?: string; driftThreshold?: number }) =>
trpcQuery<PortfolioBacktestResult>("backtest.runPortfolio", input),
},
screener: {
filter: (input: { expression: string; scope?: "watchlist" | "sector"; sector?: string }) =>
trpcQuery<{ results: Array<{ symbol: string; matched: boolean; why?: string[] } | string>; scope: string }>("screener.filter", input),
strategy: (input: { strategyId: string }) =>
trpcQuery<{ results: Array<{ symbol: string; matched: boolean; why?: string[] } | string> }>("screener.strategy", input),
},
reports: {
generate: (input: {
scope: "symbol" | "watchlist" | "portfolio" | "rotation" | "sizing_year" | "risk_posture";
symbol?: string;
data?: Record<string, unknown>;
}) => trpcQuery<{ html?: string; markdown?: string; title?: string; [k: string]: unknown }>("reports.generate", input),
},
derisking: {
suggest: (input: { symbol: string; currentPrice: number; avgCost: number; shares: number; ema21?: number; ema50?: number; thesisStatus?: string; currentRegime?: string; portfolioCorrelation?: number; profitTargets?: number[]; dividendYield?: number; payoutRatio?: number }) =>
trpcQuery<{ suggestions: Array<{ strategy: { id: string; name: string; description: string; alfredFraming: string; requiresOptionsSleeve: boolean }; applicable: boolean; reason: string; details: string }> }>("derisking.suggest", input),
dividendHealth: (symbol: string) =>
trpcQuery<{ data: Record<string, unknown> | null }>("derisking.dividendHealth", { symbol }),
},
thesisMonitor: {
assess: (input: { thesis: string; events: Array<{ type: string; date: string; detail?: string }> }) => trpcQuery<{ status: string; confidence: number; reasoning: string[] }>("thesisMonitor.assess", input),
},
trades: {
list: () => trpcQuery<Array<Record<string, unknown>>>("trades.list"),
get: (id: string) => trpcQuery<Record<string, unknown>>("trades.get", { id }),
save: (input: {
id: string;
symbol: string;
date: string;
status: string;
tier: string;
environment?: string;
strategyId?: string;
confluenceScore?: number;
riskPct?: number;
positionSize?: number;
entryPrice?: number;
stopLoss?: number;
targets?: string;
justifications?: string;
checklistCompleted?: number;
allocationPlanId?: string;
allocationRole?: string;
tradeId?: string;
}) => trpcMutate<{ ok: boolean; id: string }>("trades.save", input),
close: (id: string, realizedPnl: number, reflection?: string) =>
trpcMutate<{ ok: boolean }>("trades.close", { id, realizedPnl, ...(reflection ? { reflection } : {}) }),
stats: () => trpcQuery<{ totalClosed: number; profitable: number }>("trades.stats"),
},
theses: {
list: () => trpcQuery<Array<{
id: string;
user_id: string;
symbol: string;
statement: string;
invalidation_criteria: string;
status: string;
created_at: string;
updated_at: string;
}>>("theses.list"),
get: (id: string) => trpcQuery<Record<string, unknown>>("theses.get", { id }),
create: (input: {
symbol: string;
statement: string;
invalidationCriteria?: string[];
status?: "intact" | "weakening" | "broken";
}) => trpcMutate<{ id: string; symbol: string }>("theses.create", input),
update: (input: {
id: string;
statement?: string;
invalidationCriteria?: string[];
status?: "intact" | "weakening" | "broken";
}) => trpcMutate<{ ok: boolean }>("theses.update", input),
delete: (id: string) => trpcMutate<{ ok: boolean }>("theses.delete", { id }),
},
macro: {
commentary: () => trpcQuery<{ shortTerm: string; longTerm: string; disclaimer: string }>("macro.commentary"),
regimeClassify: () => trpcQuery<{ regime: string; confidence?: number; factors?: unknown }>("macro.regimeClassify"),
},
};
export interface TickerSuggestion {
symbol: string;
name: string | null;
role: string;
allocationPct: number;
reason: string;
source: 'live' | 'default';
}
export interface StrategyPreset {
id: string;
name: string;
category: string;
complexity: string;
scope: string;
description: string;
entry_rules: string;
exit_profile: string;
risk_policy: string;
instrument_type: string;
portfolio_template: string | null;
conviction_tier_default: string | null;
requires_options_unlock: number;
forkable: number;
example_tickers: string | null;
created_at: string;
}
export interface PortfolioBacktestResult {
equityCurve: Array<{ date: string; value: number }>;
totalReturn: number;
cagr: number;
maxDrawdown: number;
positions: Array<{ symbol: string; targetPct: number; totalReturn: number; finalValue: number }>;
rebalanceEvents: Array<{ date: string; type: string }>;
correlationMatrix: Record<string, Record<string, number>>;
sampleSize: number;
}
export interface LintResult {
symbol: string;
kind: string;
@@ -775,25 +1541,6 @@ export interface AddOptionLegInput {
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;
@@ -831,8 +1578,15 @@ export interface RiskPostureResult {
recommendedActions: RiskRecommendation[];
halt: { haltedUntil: string; triggeredBy: string; ts: string } | null;
holdingsCount: number;
optionLegsCount?: number;
totalExposureUsd: number;
options?: OptionRiskSummary;
optionRisk?: {
premiumAtRiskUsd: number;
cashReservedUsd: number;
creditReceivedUsd: number;
capitalCommittedUsd: number;
uncoveredShortCallCount: number;
};
footer: string;
}