fix: backfill symbol_demand for sidebar-added symbols + analyst ratings schema fix
- Add await ctx.cache.subscribe() to addSymbol mutation so symbols added via the sidebar get registered in symbol_demand and yfinance jobs are queued immediately - Backfill PEP, WYNN, STZ, CELH into symbol_demand + adapter_queue - Upgrade yahoo-finance2 3.15.3 -> 3.15.4 and pass validateResult:false to quoteSummary() to handle Yahoo schema drift - Add error detail logging for analyst ratings schema failures - Update .gitignore with common ignores
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Chart theme — token-driven colors for all data viz (recharts + custom SVG).
|
||||
* Values reference CSS variables so charts re-theme with the [data-theme]
|
||||
* presets in globals.css. EMA hues are fixed, distinct colors (work on any
|
||||
* background); grid/axis/up/down/accent follow the active theme.
|
||||
*/
|
||||
export const chart = {
|
||||
grid: "var(--color-line)",
|
||||
axis: "var(--color-fg-muted)",
|
||||
text: "var(--color-fg-muted)",
|
||||
up: "var(--color-up)",
|
||||
down: "var(--color-down)",
|
||||
accent: "var(--color-accent)",
|
||||
neutral: "#6b7280",
|
||||
emaColors: {
|
||||
9: "#f472b6",
|
||||
21: "#a78bfa",
|
||||
50: "#60a5fa",
|
||||
200: "#34d399",
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: "var(--color-surface-raised)",
|
||||
border: "1px solid var(--color-line)",
|
||||
borderRadius: 6,
|
||||
color: "var(--color-fg)",
|
||||
fontSize: 12,
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,124 @@
|
||||
// Options Pure — pure math utilities for the options DD panel.
|
||||
// No I/O, no React. Fully testable.
|
||||
|
||||
import type { OptionChainRow } from "@/types/options";
|
||||
|
||||
export interface IVRankResult {
|
||||
currentIV: number;
|
||||
rank: number;
|
||||
percentile: number;
|
||||
iv52wLow: number;
|
||||
iv52wHigh: number;
|
||||
}
|
||||
|
||||
/** Compute IV rank and percentile from low/high range. */
|
||||
export function computeIVRank(currentIV: number, high: number, low: number): IVRankResult {
|
||||
const range = high - low;
|
||||
if (range === 0) return { currentIV, rank: 50, percentile: 50, iv52wLow: low, iv52wHigh: high };
|
||||
const raw = (currentIV - low) / range;
|
||||
const clamped = Math.max(0, Math.min(1, raw));
|
||||
return {
|
||||
currentIV,
|
||||
rank: Math.round(clamped * 100),
|
||||
percentile: Math.round(clamped * 100),
|
||||
iv52wLow: low,
|
||||
iv52wHigh: high,
|
||||
};
|
||||
}
|
||||
|
||||
/** Classify an option position as defined-risk (spread) or undefined (naked). */
|
||||
export function classifyRisk(right: 'call' | 'put', isSpread: boolean): 'defined' | 'undefined' {
|
||||
return isSpread ? 'defined' : 'undefined';
|
||||
}
|
||||
|
||||
/** Compute PnL for a single option at a given stock price. */
|
||||
export function computeOptionPayoff(
|
||||
stockPrice: number,
|
||||
strike: number,
|
||||
right: 'call' | 'put',
|
||||
premium: number,
|
||||
): number {
|
||||
const intrinsic = right === 'call'
|
||||
? Math.max(0, stockPrice - strike)
|
||||
: Math.max(0, strike - stockPrice);
|
||||
return intrinsic - premium;
|
||||
}
|
||||
|
||||
export interface PayoffPoint {
|
||||
stockPrice: number;
|
||||
pnl: number;
|
||||
}
|
||||
|
||||
/** Generate a payoff curve over a range of stock prices. */
|
||||
export function generatePayoffCurve(
|
||||
strikes: number[],
|
||||
stockPrice: number,
|
||||
right: 'call' | 'put',
|
||||
premium: number,
|
||||
numPoints: number,
|
||||
): PayoffPoint[] {
|
||||
const minPrice = stockPrice * 0.5;
|
||||
const maxPrice = stockPrice * 1.5;
|
||||
const step = (maxPrice - minPrice) / (numPoints - 1);
|
||||
const points: PayoffPoint[] = [];
|
||||
for (let i = 0; i < numPoints; i++) {
|
||||
const sp = minPrice + step * i;
|
||||
points.push({
|
||||
stockPrice: Math.round(sp * 100) / 100,
|
||||
pnl: computeOptionPayoff(sp, strikes[0], right, premium),
|
||||
});
|
||||
}
|
||||
// Include breakeven point (where PnL = 0) if within range
|
||||
const breakeven = strikes[0] + (right === 'call' ? premium : -premium);
|
||||
if (breakeven >= minPrice && breakeven <= maxPrice) {
|
||||
points.push({
|
||||
stockPrice: Math.round(breakeven * 100) / 100,
|
||||
pnl: 0,
|
||||
});
|
||||
}
|
||||
points.sort((a, b) => a.stockPrice - b.stockPrice);
|
||||
return points;
|
||||
}
|
||||
|
||||
export interface OIWall {
|
||||
strike: number;
|
||||
openInterest: number;
|
||||
right: 'call' | 'put';
|
||||
}
|
||||
|
||||
/** Extract open interest walls from option chain rows. Sorted by strike. */
|
||||
export function extractOIWalls(
|
||||
rows: OptionChainRow[],
|
||||
): OIWall[] {
|
||||
return rows
|
||||
.filter((r) => (r.openInterest ?? 0) > 0)
|
||||
.map((r) => ({ strike: r.strike, openInterest: r.openInterest ?? 0, right: r.right as 'call' | 'put' }))
|
||||
.sort((a, b) => a.strike - b.strike);
|
||||
}
|
||||
|
||||
/** Compute max pain strike — the strike where total option value is minimized. */
|
||||
export function computeMaxPain(
|
||||
rows: OptionChainRow[],
|
||||
): number {
|
||||
if (rows.length === 0) return 0;
|
||||
|
||||
const strikes = [...new Set(rows.map((r) => r.strike))].sort((a, b) => a - b);
|
||||
let minPain = Infinity;
|
||||
let maxPainStrike = strikes[0];
|
||||
|
||||
for (const sp of strikes) {
|
||||
let total = 0;
|
||||
for (const r of rows) {
|
||||
const intrinsic = r.right === 'call'
|
||||
? Math.max(0, sp - r.strike)
|
||||
: Math.max(0, r.strike - sp);
|
||||
total += intrinsic * (r.openInterest ?? 0);
|
||||
}
|
||||
if (total < minPain) {
|
||||
minPain = total;
|
||||
maxPainStrike = sp;
|
||||
}
|
||||
}
|
||||
|
||||
return maxPainStrike;
|
||||
}
|
||||
+32
-2
@@ -43,10 +43,30 @@ export const UI_STRINGS = {
|
||||
completeOnboardingButton: "Finish setup",
|
||||
onboardingDone: "Setup complete — your starter watchlist is tracking.",
|
||||
|
||||
// Portfolio & Watchlist panels
|
||||
portfolioTitle: "Your holdings",
|
||||
portfolioCaption:
|
||||
"Record positions you already hold or want to model: symbol, shares, and average cost. Investor Flow does not connect to a broker. Exposure uses cost basis (shares × avg cost), not live marks.",
|
||||
emptyPortfolio:
|
||||
"No open holdings yet. Enter symbol, shares, and average cost below. The sidebar watchlist is only a research list - it does not feed risk posture.",
|
||||
portfolioVsWatchlist:
|
||||
"Watchlist = tickers to research. Holdings = your position book (shares + cost) that risk posture reads.",
|
||||
manageHoldingsLink: "Manage holdings on Risk",
|
||||
optionLegsTitle: "Option legs",
|
||||
optionLegsCaption:
|
||||
"Record calls and puts you already hold: long debits, covered calls, and cash-secured puts. Premium is per share; capital math uses cost basis / cash reserve - not live marks or greeks. Full options sleeve workflow comes later.",
|
||||
emptyOptionLegs:
|
||||
"No open option legs yet. Choose a role, then enter underlying, strike, expiry, contracts, and premium.",
|
||||
optionPremiumAtRiskLabel: "Premium at risk",
|
||||
optionCashReservedLabel: "Cash reserved (CSPs)",
|
||||
optionCreditLabel: "Credits received",
|
||||
watchlistTitle: "Watchlist",
|
||||
emptyWatchlist: "Your watchlist is empty.",
|
||||
|
||||
// Chart Lab (Slice 5c)
|
||||
chartLabTitle: "Chart Lab — price, volume, and indicator overlays",
|
||||
chartsTitle: "Charts — price, volume, and indicator overlays",
|
||||
priceChartCaption:
|
||||
"Daily price range (bar) with closing-price line — visual context, not a signal.",
|
||||
"Closing-price line with EMA overlays — visual context, not a signal.",
|
||||
volumeLabel: "Volume",
|
||||
volumeCaption:
|
||||
"Number of shares traded per day — relative size, not a directional cue.",
|
||||
@@ -67,6 +87,16 @@ export const UI_STRINGS = {
|
||||
relvolTooltip:
|
||||
"Relative volume shows whether trading activity is above or below the recent average; it does not predict direction.",
|
||||
noCandleData: "No candle data available for this symbol/timeframe.",
|
||||
|
||||
// M20 Risk Posture
|
||||
riskPostureTitle: "Risk posture",
|
||||
riskPostureSubtitle:
|
||||
"Your complete risk picture: holdings exposure, drawdown vs tolerance, clusters, asymmetry, and the gentle-halt circuit breaker - plus the sizing math cascade.",
|
||||
riskAccountInputsCaption:
|
||||
"Equity and peak equity are for drawdown teaching - enter total account value. Holdings below are the position book for cluster and exposure math. Cash or other assets can make equity differ from holdings total; that is fine for this lesson. Investor Flow does not connect to a broker.",
|
||||
sizingTitle: "Position sizing math",
|
||||
sizingCaption:
|
||||
"Hypothetical calculator - not how you enter the live book. Layers cascade from risk budget → stop width → conviction tier → macro gate. The result is math, not an order.",
|
||||
} as const;
|
||||
|
||||
export const UI_STRING_LIST: string[] = Object.values(UI_STRINGS);
|
||||
|
||||
+484
-12
@@ -2,6 +2,9 @@
|
||||
// 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;
|
||||
@@ -27,8 +30,295 @@ export interface SymbolMeta {
|
||||
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 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;
|
||||
@@ -97,15 +387,20 @@ 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 {
|
||||
ema?: number[];
|
||||
rsi?: { period?: number };
|
||||
relvol?: { period?: number };
|
||||
periods?: IndicatorPeriods;
|
||||
timeframe?: '1d' | '1wk' | '1mo';
|
||||
}
|
||||
export interface IndicatorResult {
|
||||
ema?: Record<number, Array<{ ts: string; value: number }>>;
|
||||
rsi?: Array<{ ts: string; value: number }>;
|
||||
relvol?: Array<{ ts: string; value: number }>;
|
||||
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) -------------------------------
|
||||
@@ -161,10 +456,19 @@ export interface FilingsSearchResult {
|
||||
export const api = {
|
||||
market: {
|
||||
snapshot: (symbol: string) => trpcQuery<Snapshot>("market.snapshot", { symbol }),
|
||||
candles: (symbol: string, timeframe = "1d") =>
|
||||
trpcQuery<PriceCandle[]>("market.candles", { symbol, timeframe }),
|
||||
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 } }) =>
|
||||
@@ -206,14 +510,48 @@ export const api = {
|
||||
},
|
||||
options: {
|
||||
chain: (symbol: string, opts?: { expiry?: string }) =>
|
||||
trpcQuery<{ symbol: string; expiration: string | null; rows: Array<Record<string, unknown>> }>("options.chain", { symbol, ...opts }),
|
||||
trpcQuery<{ symbol: string; expiration: string | null; rows: OptionChainRow[] }>("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 }),
|
||||
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", {}),
|
||||
@@ -223,6 +561,12 @@ export const api = {
|
||||
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 }),
|
||||
},
|
||||
emotionLogger: {
|
||||
add: (input: { tradeExecutionId: string; priceAtEvent?: number; emotion: string; note?: string }) =>
|
||||
@@ -233,11 +577,18 @@ export const api = {
|
||||
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"),
|
||||
acknowledgeAll: () => trpcMutate<{ ok: boolean }>("alerts.acknowledgeAll", {}),
|
||||
},
|
||||
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 }>>("admin.usersList"),
|
||||
usersList: () => trpcQuery<Array<{ id: string; email: string; complexity: string; created_at: string; is_admin: number; status: string }>>("admin.usersList"),
|
||||
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"),
|
||||
@@ -259,8 +610,21 @@ export const api = {
|
||||
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 }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -301,6 +665,112 @@ export interface PortfolioHolding {
|
||||
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;
|
||||
@@ -312,6 +782,8 @@ export interface QueueStatus {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user