import { computeSummary, toOcc, type ChainMeta, type ChainRow } from './math.ts' import { filterStrikesNearSpot } from './vendors.ts' export function buildMassiveChain( symbol: string, contractsRows: Record[], chainRows: Record[], prevCloseRow: Record | undefined, requestedExp: string | null | undefined, maxStrikes: number, note: string | null, ): { rows: ChainRow[]; meta: ChainMeta } { const sym = symbol.toUpperCase() let spot: number | null = typeof prevCloseRow?.close === 'number' ? prevCloseRow.close : null for (const r of chainRows) { const undPx = typeof r.underlying_price === 'number' ? r.underlying_price : typeof r.underlyingPrice === 'number' ? r.underlyingPrice : null if (undPx != null && undPx > 0) { spot = undPx break } } const open = typeof prevCloseRow?.open === 'number' ? prevCloseRow.open : null const change = spot != null && open != null ? Number((spot - open).toFixed(2)) : null const changePct = change != null && open && open > 0 ? Number(((change / open) * 100).toFixed(2)) : null const high = typeof prevCloseRow?.high === 'number' ? prevCloseRow.high : null const low = typeof prevCloseRow?.low === 'number' ? prevCloseRow.low : null const dayRange = low != null && high != null ? `${low.toFixed(2)}-${high.toFixed(2)}` : null const volume = typeof prevCloseRow?.volume === 'number' ? prevCloseRow.volume : null const expSet = new Set() for (const r of [...contractsRows, ...chainRows]) { const e = typeof r.expiration === 'string' ? r.expiration : null if (e) expSet.add(e) } const expirations = [...expSet].sort() const selectedExpiration = (requestedExp && expSet.has(requestedExp) ? requestedExp : expirations[0]) ?? requestedExp ?? null const sourceList = chainRows.length > 0 ? chainRows : contractsRows const byStrike = new Map() for (const r of sourceList) { const exp = typeof r.expiration === 'string' ? r.expiration : null if (selectedExpiration && exp !== selectedExpiration) continue const strike = typeof r.strike === 'number' ? r.strike : Number(r.strike) if (!Number.isFinite(strike)) continue const type = r.type === 'put' ? 'put' : 'call' const expStr = exp ?? selectedExpiration ?? '' let row = byStrike.get(strike) if (!row) { const cc = toOcc(sym, expStr, 'C', strike) const pc = toOcc(sym, expStr, 'P', strike) row = { contract: `${cc} / ${pc}`, callContract: cc, putContract: pc, expiration: expStr, strike, callBid: null, callAsk: null, callLast: null, callChg: null, callVol: null, callOi: null, callIv: null, callDelta: null, callItm: spot != null ? strike < spot : false, putBid: null, putAsk: null, putLast: null, putChg: null, putVol: null, putOi: null, putIv: null, putDelta: null, putItm: spot != null ? strike > spot : false, } byStrike.set(strike, row) } const rawIv = typeof r.iv === 'number' ? (r.iv <= 5 ? Number((r.iv * 100).toFixed(2)) : Number(r.iv.toFixed(2))) : null const contractStr = typeof r.contract === 'string' ? r.contract.replace(/^O:/, '') : null const lastVal = typeof r.last === 'number' ? r.last : typeof r.close === 'number' ? r.close : typeof r.mid === 'number' ? r.mid : typeof r.midpoint === 'number' ? r.midpoint : null const oiVal = typeof r.open_interest === 'number' ? r.open_interest : typeof r.openInterest === 'number' ? r.openInterest : null if (type === 'call') { if (contractStr) row.callContract = contractStr row.callBid = typeof r.bid === 'number' ? r.bid : null row.callAsk = typeof r.ask === 'number' ? r.ask : null row.callLast = lastVal row.callChg = typeof r.change === 'number' ? r.change : null row.callVol = typeof r.volume === 'number' ? r.volume : null row.callOi = oiVal row.callIv = rawIv row.callDelta = typeof r.delta === 'number' ? Number(r.delta.toFixed(3)) : null } else { if (contractStr) row.putContract = contractStr row.putBid = typeof r.bid === 'number' ? r.bid : null row.putAsk = typeof r.ask === 'number' ? r.ask : null row.putLast = lastVal row.putChg = typeof r.change === 'number' ? r.change : null row.putVol = typeof r.volume === 'number' ? r.volume : null row.putOi = oiVal row.putIv = rawIv row.putDelta = typeof r.delta === 'number' ? Number(r.delta.toFixed(3)) : null } row.contract = `${row.callContract} / ${row.putContract}` } const filtered = filterStrikesNearSpot([...byStrike.values()], spot, maxStrikes) return computeSummary(filtered, { symbol: sym, provider: 'massive', feedLabel: chainRows.length > 0 ? 'Massive (OPRA Snapshot)' : 'Massive (Reference + Prev Close)', spot, change, changePct, dayRange, volume, expirations, selectedExpiration, note, }) } export interface ConsolidatedDailyBar { date: string open: number high: number low: number close: number volume: number } export function consolidateDatabentoOhlcv(ohlcvRows: Record[]): ConsolidatedDailyBar[] { const byDate = new Map() for (const r of ohlcvRows) { const rawDate = typeof r.date === 'string' ? r.date.slice(0, 10) : typeof r.ts_event === 'string' ? r.ts_event.slice(0, 10) : '' const close = typeof r.close === 'number' ? r.close : Number(r.close) if (!/^\d{4}-\d{2}-\d{2}$/.test(rawDate) || !Number.isFinite(close)) continue const open = typeof r.open === 'number' && Number.isFinite(r.open) ? r.open : close const high = typeof r.high === 'number' && Number.isFinite(r.high) ? r.high : close const low = typeof r.low === 'number' && Number.isFinite(r.low) ? r.low : close const vol = typeof r.volume === 'number' && Number.isFinite(r.volume) ? r.volume : 0 const existing = byDate.get(rawDate) if (!existing) { byDate.set(rawDate, { bar: { date: rawDate, open, high, low, close, volume: vol }, topVol: vol, }) } else { existing.bar.high = Math.max(existing.bar.high, high) existing.bar.low = Math.min(existing.bar.low, low) existing.bar.volume += vol if (vol > existing.topVol) { existing.topVol = vol existing.bar.open = open existing.bar.close = close } } } return [...byDate.values()].map((d) => d.bar).sort((a, b) => a.date.localeCompare(b.date)) } function isDatabentoPut(d: Record): boolean { if (d.put_call === 'P' || d.instrument_class === 'P' || d.right === 'P' || d.type === 'put') return true if (typeof d.raw_symbol === 'string' && /^[A-Z0-9.\s]+\d{6}P\d{8}$/i.test(d.raw_symbol.trim())) return true return false } export function selectDatabentoQuoteSymbols( defsRows: Record[], ohlcvRows: Record[], requestedExp: string | null | undefined, maxStrikes: number, ): string[] { const daily = consolidateDatabentoOhlcv(ohlcvRows) const spot = daily[daily.length - 1]?.close ?? null const expSet = new Set() for (const d of defsRows) { const e = typeof d.expiration === 'string' ? d.expiration.slice(0, 10) : null if (e) expSet.add(e) } const expirations = [...expSet].sort() const selectedExpiration = (requestedExp && expSet.has(requestedExp) ? requestedExp : expirations[0]) ?? requestedExp ?? null if (!selectedExpiration) return [] const byStrike = new Map() for (const d of defsRows) { const exp = typeof d.expiration === 'string' ? d.expiration.slice(0, 10) : null if (exp !== selectedExpiration) continue const strike = typeof d.strike_price === 'number' ? d.strike_price : typeof d.strike === 'number' ? d.strike : Number(d.strike_price ?? d.strike) if (!Number.isFinite(strike)) continue const raw = typeof d.raw_symbol === 'string' && d.raw_symbol.trim() ? d.raw_symbol : null if (!raw) continue const list = byStrike.get(strike) ?? [] if (!list.includes(raw)) list.push(raw) byStrike.set(strike, list) } const strikeRows = [...byStrike.keys()].map((strike) => ({ strike }) as ChainRow) const near = filterStrikesNearSpot(strikeRows, spot, maxStrikes) const symbols: string[] = [] for (const r of near) { for (const sym of byStrike.get(r.strike) ?? []) { if (!symbols.includes(sym)) symbols.push(sym) } } return symbols.slice(0, 100) } export function buildDatabentoChain( symbol: string, defsRows: Record[], quotesRows: Record[], ohlcvRows: Record[], requestedExp: string | null | undefined, maxStrikes: number, ): { rows: ChainRow[]; meta: ChainMeta } { const sym = symbol.toUpperCase() const daily = consolidateDatabentoOhlcv(ohlcvRows) const latestBar = daily[daily.length - 1] const prevBar = daily.length > 1 ? daily[daily.length - 2] : undefined const spot = latestBar ? latestBar.close : null const prevClose = prevBar ? prevBar.close : latestBar ? latestBar.open : null const change = spot != null && prevClose != null ? Number((spot - prevClose).toFixed(2)) : null const changePct = change != null && prevClose && prevClose > 0 ? Number(((change / prevClose) * 100).toFixed(2)) : null const high = latestBar ? latestBar.high : null const low = latestBar ? latestBar.low : null const dayRange = low != null && high != null ? `${low.toFixed(2)}-${high.toFixed(2)}` : null const volume = latestBar ? latestBar.volume : null const quoteMap = new Map>() for (const q of quotesRows) { const rawSym = typeof q.raw_symbol === 'string' ? q.raw_symbol.replace(/\s+/g, '') : typeof q.symbol === 'string' ? q.symbol.replace(/\s+/g, '') : null if (rawSym) quoteMap.set(rawSym, q) } const expSet = new Set() for (const d of defsRows) { const e = typeof d.expiration === 'string' ? d.expiration.slice(0, 10) : null if (e) expSet.add(e) } const expirations = [...expSet].sort() const selectedExpiration = (requestedExp && expSet.has(requestedExp) ? requestedExp : expirations[0]) ?? requestedExp ?? null const byStrike = new Map() for (const d of defsRows) { const exp = typeof d.expiration === 'string' ? d.expiration.slice(0, 10) : null if (selectedExpiration && exp !== selectedExpiration) continue const strike = typeof d.strike_price === 'number' ? d.strike_price : typeof d.strike === 'number' ? d.strike : Number(d.strike_price ?? d.strike) if (!Number.isFinite(strike)) continue const isPut = isDatabentoPut(d) const expStr = exp ?? selectedExpiration ?? '' const rawSym = typeof d.raw_symbol === 'string' && d.raw_symbol.trim() ? d.raw_symbol.replace(/\s+/g, '') : toOcc(sym, expStr, isPut ? 'P' : 'C', strike) const q = quoteMap.get(rawSym) const bid = typeof q?.bid_px === 'number' ? q.bid_px : typeof q?.bid === 'number' ? q.bid : null const ask = typeof q?.ask_px === 'number' ? q.ask_px : typeof q?.ask === 'number' ? q.ask : null const bidSz = typeof q?.bid_sz === 'number' ? q.bid_sz : typeof q?.bid_size === 'number' ? q.bid_size : typeof q?.bidSize === 'number' ? q.bidSize : null const askSz = typeof q?.ask_sz === 'number' ? q.ask_sz : typeof q?.ask_size === 'number' ? q.ask_size : typeof q?.askSize === 'number' ? q.askSize : null const mid = bid != null && ask != null ? Number(((bid + ask) / 2).toFixed(2)) : null const last = typeof q?.last === 'number' ? q.last : mid let row = byStrike.get(strike) if (!row) { const cc = toOcc(sym, expStr, 'C', strike) const pc = toOcc(sym, expStr, 'P', strike) row = { contract: `${cc} / ${pc}`, callContract: cc, putContract: pc, expiration: expStr, strike, callBid: null, callBidSize: null, callAsk: null, callAskSize: null, callLast: null, callChg: null, callVol: null, callOi: null, callIv: null, callDelta: null, callItm: spot != null ? strike < spot : false, putBid: null, putBidSize: null, putAsk: null, putAskSize: null, putLast: null, putChg: null, putVol: null, putOi: null, putIv: null, putDelta: null, putItm: spot != null ? strike > spot : false, } byStrike.set(strike, row) } if (!isPut) { row.callContract = rawSym row.callBid = bid row.callBidSize = bidSz row.callAsk = ask row.callAskSize = askSz row.callLast = last } else { row.putContract = rawSym row.putBid = bid row.putBidSize = bidSz row.putAsk = ask row.putAskSize = askSz row.putLast = last } row.contract = `${row.callContract} / ${row.putContract}` } const filtered = filterStrikesNearSpot([...byStrike.values()], spot, maxStrikes) return computeSummary(filtered, { symbol: sym, provider: 'databento', feedLabel: 'Databento OPRA', spot, change, changePct, dayRange, volume, expirations, selectedExpiration, }) }