// ORATS Data API v2 client // // Documentation: https://docs.orats.io/ // Base URL: https://api.orats.io/datav2 // Auth: Bearer token in Authorization header // // Key endpoints: // - GET /strikes?ticker=SPY - Strike-level gamma, OI, delta, vega // - GET /summaries?ticker=SPY - IV, IVR, expected move, skew // - GET /cores?ticker=SPY - Spot price, daily series // - GET /monies/implied?ticker=SPY - Implied volatility skew curves // // Rate limits: 100 req/min per API key import type { Summary, GexProfile, ExpirationsResponse, SymbolInfo } from "@shared/schema"; export interface OratsClientConfig { apiKey?: string; baseUrl?: string; } export class OratsClient { private apiKey: string; private baseUrl: string; private cache = new Map(); private cacheTtl = 5 * 60 * 1000; // 5 minute cache constructor(config: OratsClientConfig = {}) { this.apiKey = config.apiKey ?? process.env.ORATS_API_KEY ?? ""; this.baseUrl = config.baseUrl ?? "https://api.orats.io/v1"; } isConfigured(): boolean { return this.apiKey.length > 0; } private async request(endpoint: string, params?: Record): Promise { const url = new URL(`${this.baseUrl}${endpoint}`); if (params) { Object.entries(params).forEach(([k, v]) => url.searchParams.append(k, v)); } const cacheKey = url.toString(); const cached = this.cache.get(cacheKey); if (cached && cached.expires > Date.now()) { return cached.data as T; } const response = await fetch(url.toString(), { headers: { "X-API-Key": this.apiKey, Accept: "application/json", }, signal: AbortSignal.timeout(10000), // 10 second timeout }); if (!response.ok) { throw new Error(`ORATS API ${response.status}: ${endpoint}`); } const data = await response.json(); this.cache.set(cacheKey, { data, expires: Date.now() + this.cacheTtl }); return data as T; } // Fetch strike-level data for gamma exposure calculation async fetchStrikes(ticker: string): Promise { return this.request("/strikes", { ticker }); } // Fetch summary data (IV, IVR, expected move, skew) async fetchSummaries(ticker: string): Promise { return this.request("/summaries", { ticker }); } // Fetch core data (spot price, daily series) async fetchCores(ticker: string): Promise { return this.request("/cores", { ticker }); } // Fetch implied volatility skew curves async fetchMonies(ticker: string): Promise { return this.request("/monies/implied", { ticker }); } // Fetch available symbols/exchanges async fetchSymbols(): Promise { return this.request("/symbols"); } // Compute Summary from ORATS data async computeSummary(ticker: string): Promise { const [cores, summaries] = await Promise.all([ this.fetchCores(ticker), this.fetchSummaries(ticker), ]); // Get the latest core data const latestCore = cores[0]?.data?.[0]; const spot = latestCore?.price ?? 0; const spotChange = latestCore?.change ?? 0; const spotChangePct = ((spotChange / spot) * 100) || 0; // Get the latest summary const latestSummary = summaries[0]?.data?.[0]; const ivx = latestSummary?.iv ?? 15; const ivRank = latestSummary?.ivr ?? 50; const skew = latestSummary?.skew ?? 0; const expectedMove = latestSummary?.expected_move ?? spot * 0.01; const expectedMovePct = ((expectedMove / spot) * 100) || 0; // Fetch strikes to compute GEX metrics const strikesData = await this.fetchStrikes(ticker); const strikes = strikesData[0]?.data ?? []; // Compute call wall, put wall, HVL from strikes const { callWall, putWall, hvl, netGex } = this.computeGexMetrics(strikes, spot); const gammaRegime = netGex >= 0 ? "positive" : "negative"; return { ticker, spot: round2(spot), spotChange: round2(spotChange), spotChangePct: round2(spotChangePct), netGex: Math.round(netGex), gammaRegime, hvl: round2(hvl), callWall: round2(callWall), putWall: round2(putWall), ivRank: round1(ivRank), ivx: round2(ivx), expectedMove: round2(expectedMove), expectedMovePct: round2(expectedMovePct), skew: round2(skew), asOf: new Date().toISOString(), }; } // Compute GEX profile from ORATS strike data async computeGexProfile(ticker: string): Promise { const [summary, strikesData] = await Promise.all([ this.computeSummary(ticker), this.fetchStrikes(ticker), ]); const strikes = strikesData[0]?.data ?? []; const spot = summary.spot; // Process strikes into GEX bars const bars = strikes .filter((s: any) => s.strike && s.strike > 0) .map((s: any) => { const callGex = (s.call_gamma || 0) * (s.call_oi || 0) * 100 * spot * spot * 0.01; const putGex = (s.put_gamma || 0) * (s.put_oi || 0) * 100 * spot * spot * 0.01; return { strike: round2(s.strike), callGex: Math.round(callGex), putGex: Math.round(-putGex), // Dealer convention: puts negative netGex: Math.round(callGex - putGex), }; }) .sort((a: any, b: any) => a.strike - b.strike); return { ticker, spot, hvl: summary.hvl, callWall: summary.callWall, putWall: summary.putWall, bars, asOf: new Date().toISOString(), }; } // Compute expirations response from ORATS data async computeExpirations(ticker: string): Promise { const summary = await this.computeSummary(ticker); const strikesData = await this.fetchStrikes(ticker); const strikes = strikesData[0]?.data ?? []; // Group by expiration and compute metrics for each const expiryMap = new Map(); for (const strike of strikes) { const expiry = strike.expiry || strike.expiration_date; if (expiry) { if (!expiryMap.has(expiry)) expiryMap.set(expiry, []); expiryMap.get(expiry)!.push(strike); } } const rows = Array.from(expiryMap.entries()).map(([expiry, expiryStrikes]) => { const expiryDate = new Date(expiry); const dte = Math.max(0, Math.floor((expiryDate.getTime() - Date.now()) / (24 * 60 * 60 * 1000))); // Compute IV, skew, expected move from strikes for this expiry const ivs = expiryStrikes.map(s => s.iv).filter(v => v > 0); const avgIv = ivs.length ? ivs.reduce((a, b) => a + b, 0) / ivs.length : 15; const expectedMovePct = (avgIv / 100) * Math.sqrt(Math.max(dte, 1) / 365) * 100; const expectedMove = (expectedMovePct / 100) * summary.spot; // Compute net GEX for this expiry let netGex = 0; for (const s of expiryStrikes) { const callGex = (s.call_gamma || 0) * (s.call_oi || 0) * 100 * summary.spot * summary.spot * 0.01; const putGex = (s.put_gamma || 0) * (s.put_oi || 0) * 100 * summary.spot * summary.spot * 0.01; netGex += callGex - putGex; } // Compute call/put walls for this expiry let callWall = 0; let putWall = 0; let maxCallGex = -Infinity; let minPutGex = Infinity; for (const s of expiryStrikes) { const callGex = (s.call_gamma || 0) * (s.call_oi || 0) * 100 * summary.spot * summary.spot * 0.01; const putGex = (s.put_gamma || 0) * (s.put_oi || 0) * 100 * summary.spot * summary.spot * 0.01; if (callGex > maxCallGex) { maxCallGex = callGex; callWall = s.strike; } if (putGex < minPutGex) { minPutGex = putGex; putWall = s.strike; } } return { expiry: expiry.slice(0, 10), dte, netGex: Math.round(netGex), ivx: round2(avgIv), skew: round2(summary.skew), expectedMove: round2(expectedMove), expectedMovePct: round2(expectedMovePct), callWall: round2(callWall), putWall: round2(putWall), callSkew: round2(avgIv * 0.9), putSkew: round2(avgIv * 1.1), }; }); // Sort by DTE rows.sort((a, b) => a.dte - b.dte); return { ticker, spot: summary.spot, rows, asOf: new Date().toISOString(), }; } private computeGexMetrics(strikes: any[], spot: number) { let callWall = spot; let putWall = spot; let maxCallGex = -Infinity; let minPutGex = Infinity; let cumulativeNetGex = 0; let hvl = spot; let totalNetGex = 0; // Sort by strike const sorted = [...strikes].sort((a, b) => a.strike - b.strike); for (const strike of sorted) { const callGex = (strike.call_gamma || 0) * (strike.call_oi || 0) * 100 * spot * spot * 0.01; const putGex = (strike.put_gamma || 0) * (strike.put_oi || 0) * 100 * spot * spot * 0.01; const netGex = callGex - putGex; totalNetGex += netGex; cumulativeNetGex += netGex; if (callGex > maxCallGex) { maxCallGex = callGex; callWall = strike.strike; } if (putGex > minPutGex) { minPutGex = putGex; putWall = strike.strike; } // HVL is where cumulative net GEX crosses zero if (cumulativeNetGex >= 0 && hvl === spot) { hvl = strike.strike; } } return { callWall, putWall, hvl, netGex: totalNetGex }; } } // ORATS API response types export interface OratsStrikesResponse { [ticker: string]: { data: Array<{ strike: number; expiry: string; call_oi: number; put_oi: number; call_gamma: number; put_gamma: number; call_delta: number; put_delta: number; call_vega: number; put_vega: number; call_iv: number; put_iv: number; }>; }; } export interface OratsSummariesResponse { [ticker: string]: { data: Array<{ iv: number; ivr: number; skew: number; expected_move: number; }>; }; } export interface OratsCoresResponse { [ticker: string]: { data: Array<{ price: number; change: number; change_pct: number; volume: number; }>; }; } export interface OratsMoniesResponse { [ticker: string]: { data: Array<{ strike: number; call_iv: number; put_iv: number; }>; }; } export interface OratsSymbolsResponse { symbols: Array<{ ticker: string; name: string; type: string; }>; } function round1(n: number): number { return Math.round(n * 10) / 10; } function round2(n: number): number { return Math.round(n * 100) / 100; } export const oratsClient = new OratsClient();