From 7996d006775ea6ab6f0ad77a01026aa787ea024f Mon Sep 17 00:00:00 2001 From: dirtydishes Date: Mon, 29 Dec 2025 19:09:15 -0500 Subject: [PATCH] Expand synthetic data across S&P 500 Add S&P 500 symbol list and expand synthetic options/equities feeds with varied contracts, sizes, and bursts to keep alerts firing. --- packages/types/src/index.ts | 1 + packages/types/src/sp500.ts | 507 ++++++++++++++++++ .../ingest-equities/src/adapters/synthetic.ts | 51 +- .../ingest-options/src/adapters/synthetic.ts | 110 +++- 4 files changed, 643 insertions(+), 26 deletions(-) create mode 100644 packages/types/src/sp500.ts diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 1784004..08ba2d2 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -1 +1,2 @@ export * from "./events"; +export * from "./sp500"; diff --git a/packages/types/src/sp500.ts b/packages/types/src/sp500.ts new file mode 100644 index 0000000..72066f0 --- /dev/null +++ b/packages/types/src/sp500.ts @@ -0,0 +1,507 @@ +export const SP500_SYMBOLS = [ + "MMM", + "AOS", + "ABT", + "ABBV", + "ACN", + "ADBE", + "AMD", + "AES", + "AFL", + "A", + "APD", + "ABNB", + "AKAM", + "ALB", + "ARE", + "ALGN", + "ALLE", + "LNT", + "ALL", + "GOOGL", + "GOOG", + "MO", + "AMZN", + "AMCR", + "AEE", + "AEP", + "AXP", + "AIG", + "AMT", + "AWK", + "AMP", + "AME", + "AMGN", + "APH", + "ADI", + "AON", + "APA", + "APO", + "AAPL", + "AMAT", + "APTV", + "ACGL", + "ADM", + "ANET", + "AJG", + "AIZ", + "T", + "ATO", + "ADSK", + "ADP", + "AZO", + "AVB", + "AVY", + "AXON", + "BKR", + "BALL", + "BAC", + "BAX", + "BDX", + "BRK.B", + "BBY", + "TECH", + "BIIB", + "BLK", + "BX", + "XYZ", + "BK", + "BA", + "BKNG", + "BSX", + "BMY", + "AVGO", + "BR", + "BRO", + "BF.B", + "BLDR", + "BG", + "BXP", + "CHRW", + "CDNS", + "CZR", + "CPT", + "CPB", + "COF", + "CAH", + "KMX", + "CCL", + "CARR", + "CAT", + "CBOE", + "CBRE", + "CDW", + "COR", + "CNC", + "CNP", + "CF", + "CRL", + "SCHW", + "CHTR", + "CVX", + "CMG", + "CB", + "CHD", + "CI", + "CINF", + "CTAS", + "CSCO", + "C", + "CFG", + "CLX", + "CME", + "CMS", + "KO", + "CTSH", + "COIN", + "CL", + "CMCSA", + "CAG", + "COP", + "ED", + "STZ", + "CEG", + "COO", + "CPRT", + "GLW", + "CPAY", + "CTVA", + "CSGP", + "COST", + "CTRA", + "CRWD", + "CCI", + "CSX", + "CMI", + "CVS", + "DHR", + "DRI", + "DDOG", + "DVA", + "DAY", + "DECK", + "DE", + "DELL", + "DAL", + "DVN", + "DXCM", + "FANG", + "DLR", + "DG", + "DLTR", + "D", + "DPZ", + "DASH", + "DOV", + "DOW", + "DHI", + "DTE", + "DUK", + "DD", + "EMN", + "ETN", + "EBAY", + "ECL", + "EIX", + "EW", + "EA", + "ELV", + "EMR", + "ENPH", + "ETR", + "EOG", + "EPAM", + "EQT", + "EFX", + "EQIX", + "EQR", + "ERIE", + "ESS", + "EL", + "EG", + "EVRG", + "ES", + "EXC", + "EXE", + "EXPE", + "EXPD", + "EXR", + "XOM", + "FFIV", + "FDS", + "FICO", + "FAST", + "FRT", + "FDX", + "FIS", + "FITB", + "FSLR", + "FE", + "FI", + "F", + "FTNT", + "FTV", + "FOXA", + "FOX", + "BEN", + "FCX", + "GRMN", + "IT", + "GE", + "GEHC", + "GEV", + "GEN", + "GNRC", + "GD", + "GIS", + "GM", + "GPC", + "GILD", + "GPN", + "GL", + "GDDY", + "GS", + "HAL", + "HIG", + "HAS", + "HCA", + "DOC", + "HSIC", + "HSY", + "HPE", + "HLT", + "HOLX", + "HD", + "HON", + "HRL", + "HST", + "HWM", + "HPQ", + "HUBB", + "HUM", + "HBAN", + "HII", + "IBM", + "IEX", + "IDXX", + "ITW", + "INCY", + "IR", + "PODD", + "INTC", + "ICE", + "IFF", + "IP", + "IPG", + "INTU", + "ISRG", + "IVZ", + "INVH", + "IQV", + "IRM", + "JBHT", + "JBL", + "JKHY", + "J", + "JNJ", + "JCI", + "JPM", + "K", + "KVUE", + "KDP", + "KEY", + "KEYS", + "KMB", + "KIM", + "KMI", + "KKR", + "KLAC", + "KHC", + "KR", + "LHX", + "LH", + "LRCX", + "LW", + "LVS", + "LDOS", + "LEN", + "LII", + "LLY", + "LIN", + "LYV", + "LKQ", + "LMT", + "L", + "LOW", + "LULU", + "LYB", + "MTB", + "MPC", + "MKTX", + "MAR", + "MMC", + "MLM", + "MAS", + "MA", + "MTCH", + "MKC", + "MCD", + "MCK", + "MDT", + "MRK", + "META", + "MET", + "MTD", + "MGM", + "MCHP", + "MU", + "MSFT", + "MAA", + "MRNA", + "MHK", + "MOH", + "TAP", + "MDLZ", + "MPWR", + "MNST", + "MCO", + "MS", + "MOS", + "MSI", + "MSCI", + "NDAQ", + "NTAP", + "NFLX", + "NEM", + "NWSA", + "NWS", + "NEE", + "NKE", + "NI", + "NDSN", + "NSC", + "NTRS", + "NOC", + "NCLH", + "NRG", + "NUE", + "NVDA", + "NVR", + "NXPI", + "ORLY", + "OXY", + "ODFL", + "OMC", + "ON", + "OKE", + "ORCL", + "OTIS", + "PCAR", + "PKG", + "PLTR", + "PANW", + "PSKY", + "PH", + "PAYX", + "PAYC", + "PYPL", + "PNR", + "PEP", + "PFE", + "PCG", + "PM", + "PSX", + "PNW", + "PNC", + "POOL", + "PPG", + "PPL", + "PFG", + "PG", + "PGR", + "PLD", + "PRU", + "PEG", + "PTC", + "PSA", + "PHM", + "PWR", + "QCOM", + "DGX", + "RL", + "RJF", + "RTX", + "O", + "REG", + "REGN", + "RF", + "RSG", + "RMD", + "RVTY", + "ROK", + "ROL", + "ROP", + "ROST", + "RCL", + "SPGI", + "CRM", + "SBAC", + "SLB", + "STX", + "SRE", + "NOW", + "SHW", + "SPG", + "SWKS", + "SJM", + "SW", + "SNA", + "SOLV", + "SO", + "LUV", + "SWK", + "SBUX", + "STT", + "STLD", + "STE", + "SYK", + "SMCI", + "SYF", + "SNPS", + "SYY", + "TMUS", + "TROW", + "TTWO", + "TPR", + "TRGP", + "TGT", + "TEL", + "TDY", + "TER", + "TSLA", + "TXN", + "TPL", + "TXT", + "TMO", + "TJX", + "TKO", + "TTD", + "TSCO", + "TT", + "TDG", + "TRV", + "TRMB", + "TFC", + "TYL", + "TSN", + "USB", + "UBER", + "UDR", + "ULTA", + "UNP", + "UAL", + "UPS", + "URI", + "UNH", + "UHS", + "VLO", + "VTR", + "VLTO", + "VRSN", + "VRSK", + "VZ", + "VRTX", + "VTRS", + "VICI", + "V", + "VST", + "VMC", + "WRB", + "GWW", + "WAB", + "WBA", + "WMT", + "DIS", + "WBD", + "WM", + "WAT", + "WEC", + "WFC", + "WELL", + "WST", + "WDC", + "WY", + "WSM", + "WMB", + "WTW", + "WDAY", + "WYNN", + "XEL", + "XYL", + "YUM", + "ZBRA", + "ZBH", + "ZTS", +] as const; + +export type Sp500Symbol = typeof SP500_SYMBOLS[number]; diff --git a/services/ingest-equities/src/adapters/synthetic.ts b/services/ingest-equities/src/adapters/synthetic.ts index 2b79d36..930e5e2 100644 --- a/services/ingest-equities/src/adapters/synthetic.ts +++ b/services/ingest-equities/src/adapters/synthetic.ts @@ -1,22 +1,40 @@ -import type { EquityPrint } from "@islandflow/types"; +import { SP500_SYMBOLS, type EquityPrint } from "@islandflow/types"; import type { EquityIngestAdapter, EquityIngestHandlers } from "./types"; type SyntheticEquitiesAdapterConfig = { emitIntervalMs: number; }; -const buildSyntheticPrint = (seq: number, now: number): EquityPrint => { +const EXCHANGES = ["NYSE", "NASDAQ", "ARCA", "BATS", "IEX", "TEST"]; + +const hashSymbol = (value: string): number => { + let hash = 0; + for (let i = 0; i < value.length; i += 1) { + hash = (hash * 31 + value.charCodeAt(i)) >>> 0; + } + return hash; +}; + +const buildSyntheticPrint = ( + seq: number, + now: number, + symbol: string, + price: number, + size: number, + exchange: string, + offExchangeFlag: boolean +): EquityPrint => { return { source_ts: now, ingest_ts: now, seq, - trace_id: `ingest-equities-${seq}`, + trace_id: `synthetic-equities-${seq}`, ts: now, - underlying_id: "SPY", - price: 450.1, - size: 100, - exchange: "TEST", - offExchangeFlag: false + underlying_id: symbol, + price, + size, + exchange, + offExchangeFlag }; }; @@ -35,10 +53,21 @@ export const createSyntheticEquitiesAdapter = ( return; } - seq += 1; const now = Date.now(); - const print = buildSyntheticPrint(seq, now); - void handlers.onTrade(print); + const batchSize = 3; + + for (let i = 0; i < batchSize; i += 1) { + seq += 1; + const symbol = SP500_SYMBOLS[(seq + i) % SP500_SYMBOLS.length]; + const symbolHash = hashSymbol(symbol); + const basePrice = 25 + (symbolHash % 475); + const price = Number((basePrice + ((seq % 40) - 20) * 0.05).toFixed(2)); + const size = 10 + (seq % 600); + const exchange = EXCHANGES[(seq + symbolHash) % EXCHANGES.length]; + const offExchangeFlag = (seq + i) % 6 === 0; + const print = buildSyntheticPrint(seq, now + i * 4, symbol, price, size, exchange, offExchangeFlag); + void handlers.onTrade(print); + } }; timer = setInterval(emit, config.emitIntervalMs); diff --git a/services/ingest-options/src/adapters/synthetic.ts b/services/ingest-options/src/adapters/synthetic.ts index 280a4c4..2b26294 100644 --- a/services/ingest-options/src/adapters/synthetic.ts +++ b/services/ingest-options/src/adapters/synthetic.ts @@ -1,22 +1,72 @@ -import type { OptionPrint } from "@islandflow/types"; +import { SP500_SYMBOLS, type OptionPrint } from "@islandflow/types"; import type { OptionIngestAdapter, OptionIngestHandlers } from "./types"; type SyntheticOptionsAdapterConfig = { emitIntervalMs: number; }; -const buildSyntheticPrint = (seq: number, now: number): OptionPrint => { +type Burst = { + contractId: string; + basePrice: number; + baseSize: number; + exchange: string; + conditions?: string[]; + burstSize: number; +}; + +const MS_PER_DAY = 24 * 60 * 60 * 1000; +const EXPIRY_OFFSETS = [7, 14, 28, 45, 60, 90]; +const EXCHANGES = ["CBOE", "PHLX", "ISE", "ARCA", "BOX", "MIAX"]; +const CONDITIONS = ["SWEEP", "ISO", "FILL", "TEST"]; + +const pick = (items: T[], seed: number): T => { + return items[Math.abs(seed) % items.length]; +}; + +const hashSymbol = (value: string): number => { + let hash = 0; + for (let i = 0; i < value.length; i += 1) { + hash = (hash * 31 + value.charCodeAt(i)) >>> 0; + } + return hash; +}; + +const formatStrike = (strike: number): string => { + const fixed = strike.toFixed(3); + return fixed.replace(/\.?0+$/, ""); +}; + +const formatExpiry = (now: number, offsetDays: number): string => { + const expiryDate = new Date(now + offsetDays * MS_PER_DAY); + return expiryDate.toISOString().slice(0, 10); +}; + +const buildBurst = (burstIndex: number, now: number): Burst => { + const symbol = SP500_SYMBOLS[burstIndex % SP500_SYMBOLS.length]; + const symbolHash = hashSymbol(symbol); + const basePrice = 30 + (symbolHash % 470); + const expiryOffset = pick(EXPIRY_OFFSETS, symbolHash + burstIndex); + const expiry = formatExpiry(now, expiryOffset); + const strikeStep = basePrice >= 200 ? 10 : 5; + const strikeOffset = ((burstIndex % 7) - 3) * strikeStep; + const strike = Math.max(1, Math.round(basePrice / strikeStep) * strikeStep + strikeOffset); + const right = burstIndex % 2 === 0 ? "C" : "P"; + const contractId = `${symbol}-${expiry}-${formatStrike(strike)}-${right}`; + const exchange = pick(EXCHANGES, burstIndex + symbolHash); + const isBlock = burstIndex % 4 === 0; + const burstSize = isBlock ? 4 : burstIndex % 3 === 0 ? 2 : 1; + const baseSize = isBlock ? 1200 + (symbolHash % 1800) : 5 + (symbolHash % 180); + const distance = Math.abs(strike - basePrice); + const basePricePer = isBlock ? 12 + distance / strikeStep : 0.5 + distance / 30; + const conditions = isBlock ? [pick(CONDITIONS, burstIndex)] : undefined; + return { - source_ts: now, - ingest_ts: now, - seq, - trace_id: `ingest-options-${seq}`, - ts: now, - option_contract_id: "SPY-2025-01-17-450-C", - price: 1.25, - size: 10, - exchange: "TEST", - conditions: ["TEST"] + contractId, + basePrice: basePricePer, + baseSize, + exchange, + conditions, + burstSize }; }; @@ -27,6 +77,9 @@ export const createSyntheticOptionsAdapter = ( name: "synthetic", start: (handlers: OptionIngestHandlers) => { let seq = 0; + let burstIndex = 0; + let currentBurst: Burst | null = null; + let remaining = 0; let timer: ReturnType | null = null; let stopped = false; @@ -35,10 +88,37 @@ export const createSyntheticOptionsAdapter = ( return; } - seq += 1; const now = Date.now(); - const print = buildSyntheticPrint(seq, now); - void handlers.onTrade(print); + if (!currentBurst || remaining <= 0) { + burstIndex += 1; + currentBurst = buildBurst(burstIndex, now); + remaining = currentBurst.burstSize; + } + + const burst = currentBurst; + const printsToEmit = remaining; + + for (let i = 0; i < printsToEmit; i += 1) { + seq += 1; + const priceJitter = (i % 3) - 1; + const sizeJitter = (i % 4) - 1; + const print: OptionPrint = { + source_ts: now + i * 5, + ingest_ts: now + i * 5, + seq, + trace_id: `synthetic-options-${seq}`, + ts: now + i * 5, + option_contract_id: burst.contractId, + price: Math.max(0.05, Number((burst.basePrice * (1 + priceJitter * 0.02)).toFixed(2))), + size: Math.max(1, Math.round(burst.baseSize * (1 + sizeJitter * 0.05))), + exchange: burst.exchange, + conditions: burst.conditions + }; + + void handlers.onTrade(print); + } + + remaining = 0; }; timer = setInterval(emit, config.emitIntervalMs);