import { computeSummary, toOcc, type ChainMeta, type ChainRow } from './math.ts' export function filterStrikesNearSpot( rows: T[], spot: number | null, maxStrikes: number, ): T[] { const sorted = [...rows].sort((a, b) => a.strike - b.strike) if (!maxStrikes || maxStrikes <= 0 || sorted.length <= maxStrikes) return sorted const ref = spot ?? sorted[Math.floor(sorted.length / 2)]!.strike return [...sorted] .sort((a, b) => Math.abs(a.strike - ref) - Math.abs(b.strike - ref) || a.strike - b.strike) .slice(0, maxStrikes) .sort((a, b) => a.strike - b.strike) } export function buildAlpacaChain( symbol: string, alpacaFeed: 'indicative' | 'opra', optRows: Record[], snapRow: Record | undefined, requestedExp: string | null | undefined, maxStrikes: number, allExpirations?: string[], ): { rows: ChainRow[]; meta: ChainMeta } { const sym = symbol.toUpperCase() const spot = typeof snapRow?.last === 'number' ? snapRow.last : typeof snapRow?.price === 'number' ? snapRow.price : typeof snapRow?.prev_close === 'number' ? snapRow.prev_close : typeof snapRow?.prevClose === 'number' ? snapRow.prevClose : null const change = typeof snapRow?.change === 'number' ? snapRow.change : null const changePct = typeof snapRow?.change_pct === 'number' ? snapRow.change_pct : typeof snapRow?.changePct === 'number' ? snapRow.changePct : null const dayHigh = typeof snapRow?.day_high === 'number' ? snapRow.day_high : typeof snapRow?.dayHigh === 'number' ? snapRow.dayHigh : null const dayLow = typeof snapRow?.day_low === 'number' ? snapRow.day_low : typeof snapRow?.dayLow === 'number' ? snapRow.dayLow : null const dayRange = dayLow != null && dayHigh != null ? `${dayLow.toFixed(2)}-${dayHigh.toFixed(2)}` : null const volume = typeof snapRow?.day_volume === 'number' ? snapRow.day_volume : typeof snapRow?.dayVolume === 'number' ? snapRow.dayVolume : null let sessionDate: string | null = typeof snapRow?.day_time === 'string' ? snapRow.day_time.slice(0, 10) : typeof snapRow?.last_time === 'string' ? snapRow.last_time.slice(0, 10) : null const expSet = new Set(allExpirations ?? []) for (const r of optRows) { const e = typeof r.expiration === 'string' ? r.expiration : null if (e) expSet.add(e) const qt = typeof r.quote_time === 'string' ? r.quote_time.slice(0, 10) : null if (qt && (!sessionDate || qt > sessionDate)) sessionDate = qt } const expirations = [...expSet].sort() const selectedExpiration = (requestedExp && expSet.has(requestedExp) ? requestedExp : expirations[0]) ?? requestedExp ?? null const byStrike = new Map() for (const r of optRows) { 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, callBidSize: null, callAsk: null, callAskSize: null, callLast: null, callLastTime: 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, putLastTime: 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 lastTime = typeof r.last_time === 'string' ? r.last_time : null const isCurrentSessionTrade = !sessionDate || (lastTime != null && lastTime.slice(0, 10) >= sessionDate) const rawLast = typeof r.last === 'number' ? r.last : typeof r.lastPrice === 'number' ? r.lastPrice : null const rawVol = typeof r.day_volume === 'number' ? r.day_volume : typeof r.volume === 'number' ? r.volume : null const rawOi = typeof r.open_interest === 'number' ? r.open_interest : typeof r.openInterest === 'number' ? r.openInterest : null const lastPx = isCurrentSessionTrade ? rawLast : null const volNum = isCurrentSessionTrade ? rawVol : null const bidSz = typeof r.bid_size === 'number' ? r.bid_size : typeof r.bidSize === 'number' ? r.bidSize : null const askSz = typeof r.ask_size === 'number' ? r.ask_size : typeof r.askSize === 'number' ? r.askSize : null if (type === 'call') { if (typeof r.symbol === 'string') row.callContract = r.symbol row.callBid = typeof r.bid === 'number' ? r.bid : null row.callBidSize = bidSz row.callAsk = typeof r.ask === 'number' ? r.ask : null row.callAskSize = askSz row.callLast = lastPx row.callLastTime = lastTime row.callVol = volNum row.callOi = rawOi row.callIv = rawIv row.callDelta = typeof r.delta === 'number' ? Number(r.delta.toFixed(3)) : null } else { if (typeof r.symbol === 'string') row.putContract = r.symbol row.putBid = typeof r.bid === 'number' ? r.bid : null row.putBidSize = bidSz row.putAsk = typeof r.ask === 'number' ? r.ask : null row.putAskSize = askSz row.putLast = lastPx row.putLastTime = lastTime row.putVol = volNum row.putOi = rawOi 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: 'alpaca', feedLabel: `Alpaca (${alpacaFeed.toUpperCase()})`, spot, change, changePct, dayRange, volume, expirations, selectedExpiration, }) }