tradeblocks-mcp 3.0.2 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/README.md +82 -68
  2. package/dist/{chunk-LDKTV7GW.js → chunk-27S67XW3.js} +24 -52
  3. package/dist/chunk-27S67XW3.js.map +1 -0
  4. package/dist/{chunk-FGZH632F.js → chunk-2E63THNI.js} +13 -11
  5. package/dist/chunk-2E63THNI.js.map +1 -0
  6. package/dist/{chunk-BKQ4PM4Y.js → chunk-6S37CXUA.js} +11 -13
  7. package/dist/chunk-6S37CXUA.js.map +1 -0
  8. package/dist/{chunk-PRAYH3RT.js → chunk-AP7IHUUR.js} +29 -12
  9. package/dist/chunk-AP7IHUUR.js.map +1 -0
  10. package/dist/{chunk-QTTR7AAW.js → chunk-C6LL746C.js} +67 -84
  11. package/dist/{chunk-QTTR7AAW.js.map → chunk-C6LL746C.js.map} +1 -1
  12. package/dist/{chunk-W2PP3LEH.js → chunk-SEUZYQGQ.js} +17 -8
  13. package/dist/chunk-SEUZYQGQ.js.map +1 -0
  14. package/dist/{chunk-4BLCXNQ6.js → chunk-UBUC5A66.js} +105 -114
  15. package/dist/chunk-UBUC5A66.js.map +1 -0
  16. package/dist/{chunk-XXYOUIZY.js → chunk-VDU25Z6X.js} +63 -80
  17. package/dist/chunk-VDU25Z6X.js.map +1 -0
  18. package/dist/daily-log-processor-BY3ISY6K.js +9 -0
  19. package/dist/{daily-logs-store-D3QC2XPV.js → daily-logs-store-3C3OTGPK.js} +2 -2
  20. package/dist/iv-solver-worker.js +9 -1
  21. package/dist/market-provider-H3ARIVZ4.js +16 -0
  22. package/dist/{proto-3HZTCWK4.js → proto-V23GIZG2.js} +2 -2
  23. package/dist/{sync-PO4IPCYV.js → sync-EVLKB3ZJ.js} +7 -7
  24. package/dist/test-exports.js +358 -254
  25. package/dist/test-exports.js.map +1 -1
  26. package/dist/trade-processor-EYA3I3XB.js +9 -0
  27. package/dist/{trades-store-LOL3FQSF.js → trades-store-R3NL25T2.js} +2 -2
  28. package/manifest.json +3 -11
  29. package/package.json +1 -1
  30. package/server/{chunk-NZO6PT64.js → chunk-4P7D7YZP.js} +102 -113
  31. package/server/chunk-4P7D7YZP.js.map +1 -0
  32. package/server/{chunk-72GKJE2U.js → chunk-BOPHW5M6.js} +521 -235
  33. package/server/chunk-BOPHW5M6.js.map +1 -0
  34. package/server/{chunk-WA5AAPCH.js → chunk-GH2552SE.js} +17 -8
  35. package/server/chunk-GH2552SE.js.map +1 -0
  36. package/server/{chunk-5EBXHT6C.js → chunk-OBYKFW2B.js} +11 -13
  37. package/server/chunk-OBYKFW2B.js.map +1 -0
  38. package/server/{chunk-FBNDMCT5.js → chunk-T66KH2XH.js} +13 -11
  39. package/server/chunk-T66KH2XH.js.map +1 -0
  40. package/server/{chunk-NRFXAJF7.js → chunk-W5E7FHC4.js} +24 -52
  41. package/server/chunk-W5E7FHC4.js.map +1 -0
  42. package/server/{chunk-CSDVJPBB.js → chunk-YUCOAJ4Z.js} +29 -12
  43. package/server/chunk-YUCOAJ4Z.js.map +1 -0
  44. package/server/{chunk-PNKG7RY7.js → chunk-ZBJCF4ZG.js} +63 -80
  45. package/server/chunk-ZBJCF4ZG.js.map +1 -0
  46. package/server/{config-6IZXEFEX.js → config-DK7KOMNL.js} +1 -1
  47. package/server/config-DK7KOMNL.js.map +1 -0
  48. package/server/daily-log-processor-ENEUT22A.js +10 -0
  49. package/server/{daily-logs-store-NU7M4L5C.js → daily-logs-store-YFNXTB7J.js} +2 -2
  50. package/server/http-server.js +2 -5
  51. package/server/index.js +1771 -1538
  52. package/server/index.js.map +1 -1
  53. package/server/iv-solver-worker.js +9 -1
  54. package/server/market-provider-B437HKLW.js +17 -0
  55. package/server/{proto-DWRZJO4E.js → proto-YZ4TLX2N.js} +2 -2
  56. package/server/{sync-QFI5L7S7.js → sync-V25UQJA3.js} +7 -7
  57. package/server/trade-processor-L3PIQ5TG.js +10 -0
  58. package/server/{trades-store-VY26MWER.js → trades-store-RTTEYMXH.js} +2 -2
  59. package/src/auth/clients-store.ts +4 -4
  60. package/src/auth/code-store.ts +1 -1
  61. package/src/auth/config.ts +11 -12
  62. package/src/auth/login-page.ts +8 -10
  63. package/src/auth/provider.ts +35 -30
  64. package/src/auth/token.ts +17 -15
  65. package/src/db/backtest-schemas.ts +12 -4
  66. package/src/db/connection.ts +107 -40
  67. package/src/db/index.ts +37 -5
  68. package/src/db/json-adapters.ts +22 -31
  69. package/src/db/json-migration.ts +11 -15
  70. package/src/db/json-store.ts +5 -3
  71. package/src/db/market-datasets.ts +48 -15
  72. package/src/db/market-schemas.ts +11 -5
  73. package/src/db/market-views.ts +119 -32
  74. package/src/db/parquet-writer.ts +7 -7
  75. package/src/db/profile-schemas.ts +39 -22
  76. package/src/db/schemas.ts +2 -2
  77. package/src/http-server.ts +7 -12
  78. package/src/index.ts +8 -9
  79. package/src/market/ingestor/index.ts +1 -4
  80. package/src/market/ingestor/market-ingestor.ts +140 -67
  81. package/src/market/ingestor/types.ts +1 -3
  82. package/src/market/stores/chain-sql.ts +2 -8
  83. package/src/market/stores/chain-store.ts +1 -4
  84. package/src/market/stores/duckdb-chain-store.ts +3 -14
  85. package/src/market/stores/duckdb-enriched-store.ts +3 -13
  86. package/src/market/stores/duckdb-quote-store.ts +10 -27
  87. package/src/market/stores/duckdb-spot-store.ts +4 -20
  88. package/src/market/stores/enriched-sql.ts +3 -9
  89. package/src/market/stores/enriched-store.ts +2 -2
  90. package/src/market/stores/parquet-chain-store.ts +5 -22
  91. package/src/market/stores/parquet-enriched-store.ts +4 -16
  92. package/src/market/stores/parquet-oi-daily-store.ts +3 -14
  93. package/src/market/stores/parquet-quote-store.ts +9 -29
  94. package/src/market/stores/parquet-spot-store.ts +14 -33
  95. package/src/market/stores/quote-store.ts +2 -10
  96. package/src/market/stores/spot-sql.ts +3 -15
  97. package/src/market/stores/spot-store.ts +1 -1
  98. package/src/market/tickers/defaults.json +6 -6
  99. package/src/market/tickers/loader.ts +2 -7
  100. package/src/market/tickers/registry.ts +5 -15
  101. package/src/market/tickers/schemas.ts +2 -6
  102. package/src/models/strategy-profile.ts +39 -39
  103. package/src/sync/block-sync.ts +44 -65
  104. package/src/sync/index.ts +1 -4
  105. package/src/sync/metadata.ts +14 -20
  106. package/src/test-exports.ts +127 -123
  107. package/src/tools/analysis.ts +180 -159
  108. package/src/tools/batch-exit-analysis.ts +100 -59
  109. package/src/tools/blocks/analysis.ts +116 -101
  110. package/src/tools/blocks/comparison.ts +281 -364
  111. package/src/tools/blocks/core.ts +208 -253
  112. package/src/tools/blocks/health.ts +571 -563
  113. package/src/tools/blocks/similarity.ts +316 -283
  114. package/src/tools/edge-decay.ts +124 -197
  115. package/src/tools/exit-analysis.ts +130 -77
  116. package/src/tools/greeks-attribution.ts +84 -35
  117. package/src/tools/guides.ts +4 -6
  118. package/src/tools/imports.ts +9 -12
  119. package/src/tools/market-data.ts +1067 -757
  120. package/src/tools/market-enrichment.ts +3 -3
  121. package/src/tools/market-fetch.ts +148 -67
  122. package/src/tools/market-imports.ts +12 -12
  123. package/src/tools/middleware/sync-middleware.ts +5 -6
  124. package/src/tools/performance.ts +185 -302
  125. package/src/tools/profile-analysis.ts +52 -66
  126. package/src/tools/profiles.ts +106 -69
  127. package/src/tools/regime-advisor.ts +20 -45
  128. package/src/tools/replay.ts +81 -77
  129. package/src/tools/reports/discrepancies.ts +298 -328
  130. package/src/tools/reports/fields.ts +7 -25
  131. package/src/tools/reports/helpers.ts +18 -49
  132. package/src/tools/reports/predictive.ts +27 -70
  133. package/src/tools/reports/slippage-trends.ts +315 -345
  134. package/src/tools/reports/slippage.ts +1 -4
  135. package/src/tools/reports/strategy-matches.ts +399 -441
  136. package/src/tools/schema.ts +43 -40
  137. package/src/tools/shared/filters.ts +3 -9
  138. package/src/tools/snapshot.ts +9 -30
  139. package/src/tools/sql.ts +15 -14
  140. package/src/tools/tickers.ts +1 -4
  141. package/src/utils/batch-exit-analysis.ts +31 -42
  142. package/src/utils/black-scholes.ts +39 -29
  143. package/src/utils/block-loader.ts +68 -82
  144. package/src/utils/calibration-probe.ts +1 -2
  145. package/src/utils/chain-loader.ts +3 -3
  146. package/src/utils/csv-discovery.ts +16 -22
  147. package/src/utils/data-quality.ts +24 -36
  148. package/src/utils/exit-triggers.ts +91 -96
  149. package/src/utils/field-timing.ts +94 -79
  150. package/src/utils/filter-predicates.ts +13 -9
  151. package/src/utils/flatfile-importer.ts +94 -64
  152. package/src/utils/greeks-decomposition.ts +152 -100
  153. package/src/utils/iv-solver-pool.ts +55 -25
  154. package/src/utils/iv-solver-worker.ts +5 -5
  155. package/src/utils/market-enricher.ts +528 -497
  156. package/src/utils/market-importer.ts +31 -12
  157. package/src/utils/market-provider.ts +21 -23
  158. package/src/utils/massive-tier.ts +5 -7
  159. package/src/utils/migrate-option-data-helpers.ts +2 -8
  160. package/src/utils/option-quote-greeks.ts +25 -31
  161. package/src/utils/option-time.ts +4 -8
  162. package/src/utils/output-formatter.ts +1 -4
  163. package/src/utils/provider-capabilities.ts +1 -4
  164. package/src/utils/providers/massive.ts +59 -93
  165. package/src/utils/providers/thetadata/backfill.ts +14 -23
  166. package/src/utils/providers/thetadata/client.ts +12 -8
  167. package/src/utils/providers/thetadata/decode.ts +2 -20
  168. package/src/utils/providers/thetadata/endpoints.ts +32 -40
  169. package/src/utils/providers/thetadata/join.ts +11 -10
  170. package/src/utils/providers/thetadata/proto.ts +12 -10
  171. package/src/utils/providers/thetadata/quote-mid-greeks.ts +5 -5
  172. package/src/utils/providers/thetadata.ts +11 -10
  173. package/src/utils/quote-enricher.ts +4 -4
  174. package/src/utils/quote-parquet-projection.ts +3 -11
  175. package/src/utils/sample-date-selector.ts +3 -5
  176. package/src/utils/schema-metadata.ts +102 -70
  177. package/src/utils/ticker.ts +5 -9
  178. package/src/utils/trade-replay.ts +77 -68
  179. package/dist/chunk-4BLCXNQ6.js.map +0 -1
  180. package/dist/chunk-BKQ4PM4Y.js.map +0 -1
  181. package/dist/chunk-FGZH632F.js.map +0 -1
  182. package/dist/chunk-LDKTV7GW.js.map +0 -1
  183. package/dist/chunk-PRAYH3RT.js.map +0 -1
  184. package/dist/chunk-W2PP3LEH.js.map +0 -1
  185. package/dist/chunk-XXYOUIZY.js.map +0 -1
  186. package/dist/daily-log-processor-6MWJ23JK.js +0 -9
  187. package/dist/market-provider-VDRJUEF2.js +0 -16
  188. package/dist/trade-processor-NHU4VWRX.js +0 -9
  189. package/server/chunk-5EBXHT6C.js.map +0 -1
  190. package/server/chunk-72GKJE2U.js.map +0 -1
  191. package/server/chunk-CSDVJPBB.js.map +0 -1
  192. package/server/chunk-FBNDMCT5.js.map +0 -1
  193. package/server/chunk-NRFXAJF7.js.map +0 -1
  194. package/server/chunk-NZO6PT64.js.map +0 -1
  195. package/server/chunk-PNKG7RY7.js.map +0 -1
  196. package/server/chunk-WA5AAPCH.js.map +0 -1
  197. package/server/config-6IZXEFEX.js.map +0 -1
  198. package/server/daily-log-processor-Y3PVSVBM.js +0 -10
  199. package/server/market-provider-VOYYVYWT.js +0 -17
  200. package/server/trade-processor-JWVS37KM.js +0 -10
  201. /package/dist/{daily-log-processor-6MWJ23JK.js.map → daily-log-processor-BY3ISY6K.js.map} +0 -0
  202. /package/dist/{daily-logs-store-D3QC2XPV.js.map → daily-logs-store-3C3OTGPK.js.map} +0 -0
  203. /package/dist/{market-provider-VDRJUEF2.js.map → market-provider-H3ARIVZ4.js.map} +0 -0
  204. /package/dist/{proto-3HZTCWK4.js.map → proto-V23GIZG2.js.map} +0 -0
  205. /package/dist/{sync-PO4IPCYV.js.map → sync-EVLKB3ZJ.js.map} +0 -0
  206. /package/dist/{trade-processor-NHU4VWRX.js.map → trade-processor-EYA3I3XB.js.map} +0 -0
  207. /package/dist/{trades-store-LOL3FQSF.js.map → trades-store-R3NL25T2.js.map} +0 -0
  208. /package/server/{daily-log-processor-Y3PVSVBM.js.map → daily-log-processor-ENEUT22A.js.map} +0 -0
  209. /package/server/{daily-logs-store-NU7M4L5C.js.map → daily-logs-store-YFNXTB7J.js.map} +0 -0
  210. /package/server/{market-provider-VOYYVYWT.js.map → market-provider-B437HKLW.js.map} +0 -0
  211. /package/server/{proto-DWRZJO4E.js.map → proto-YZ4TLX2N.js.map} +0 -0
  212. /package/server/{sync-QFI5L7S7.js.map → sync-V25UQJA3.js.map} +0 -0
  213. /package/server/{trade-processor-JWVS37KM.js.map → trade-processor-L3PIQ5TG.js.map} +0 -0
  214. /package/server/{trades-store-VY26MWER.js.map → trades-store-RTTEYMXH.js.map} +0 -0
@@ -238,10 +238,12 @@ export function nanosToETMinuteKey(nanosTimestamp: number): string {
238
238
 
239
239
  function etOffsetMinutesForDate(dateStr: string): number {
240
240
  const probe = new Date(`${dateStr}T12:00:00Z`);
241
- const offsetToken = probe.toLocaleString("en-US", {
242
- timeZone: "America/New_York",
243
- timeZoneName: "shortOffset",
244
- }).match(/GMT([+-]\d{1,2})(?::(\d{2}))?/);
241
+ const offsetToken = probe
242
+ .toLocaleString("en-US", {
243
+ timeZone: "America/New_York",
244
+ timeZoneName: "shortOffset",
245
+ })
246
+ .match(/GMT([+-]\d{1,2})(?::(\d{2}))?/);
245
247
  if (!offsetToken) {
246
248
  throw new Error(`Unable to resolve ET offset for ${dateStr}`);
247
249
  }
@@ -265,9 +267,7 @@ function etDateTimeToUtcIso(dateStr: string, timeStr: string): string {
265
267
  function getApiKey(): string {
266
268
  const key = process.env.MASSIVE_API_KEY;
267
269
  if (!key) {
268
- throw new Error(
269
- "Set MASSIVE_API_KEY environment variable to use Massive.com data import"
270
- );
270
+ throw new Error("Set MASSIVE_API_KEY environment variable to use Massive.com data import");
271
271
  }
272
272
  return key;
273
273
  }
@@ -275,7 +275,7 @@ function getApiKey(): string {
275
275
  async function fetchWithRetry(
276
276
  url: string,
277
277
  headers: Record<string, string>,
278
- maxRetries = 2
278
+ maxRetries = 2,
279
279
  ): Promise<Response> {
280
280
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
281
281
  const response = await fetch(url, {
@@ -285,9 +285,7 @@ async function fetchWithRetry(
285
285
 
286
286
  if (response.status === 429) {
287
287
  if (attempt === maxRetries) {
288
- throw new Error(
289
- "Massive.com rate limit exceeded — try again in a few minutes"
290
- );
288
+ throw new Error("Massive.com rate limit exceeded — try again in a few minutes");
291
289
  }
292
290
  const retryAfter = response.headers.get("Retry-After");
293
291
  const backoffMs = retryAfter
@@ -306,9 +304,7 @@ async function fetchWithRetry(
306
304
  // Snapshot Helpers
307
305
  // ===========================================================================
308
306
 
309
- const INDEX_TICKERS = new Set([
310
- "SPX", "NDX", "RUT", "DJX", "VIX", "VIX9D", "VIX3M", "OEX", "XSP",
311
- ]);
307
+ const INDEX_TICKERS = new Set(["SPX", "NDX", "RUT", "DJX", "VIX", "VIX9D", "VIX3M", "OEX", "XSP"]);
312
308
 
313
309
  function detectSnapshotAssetClass(ticker: string): AssetClass {
314
310
  return INDEX_TICKERS.has(ticker.toUpperCase()) ? "index" : "stock";
@@ -333,18 +329,14 @@ function computeDTE(expirationDate: string): number {
333
329
  const todayDay = parseInt(todayDayS, 10);
334
330
 
335
331
  const dte =
336
- (Date.UTC(expYear, expMonth - 1, expDay) -
337
- Date.UTC(todayYear, todayMonth - 1, todayDay)) /
332
+ (Date.UTC(expYear, expMonth - 1, expDay) - Date.UTC(todayYear, todayMonth - 1, todayDay)) /
338
333
  86_400_000;
339
334
 
340
335
  return dte <= 0 ? 0.001 : dte;
341
336
  }
342
337
 
343
- function mapContract(
344
- contract: z.infer<typeof MassiveSnapshotContractSchema>,
345
- ): OptionContract {
346
- const hasApiGreeks =
347
- contract.greeks != null && contract.greeks.delta != null;
338
+ function mapContract(contract: z.infer<typeof MassiveSnapshotContractSchema>): OptionContract {
339
+ const hasApiGreeks = contract.greeks != null && contract.greeks.delta != null;
348
340
 
349
341
  let delta: number | null = null;
350
342
  let gamma: number | null = null;
@@ -361,8 +353,7 @@ function mapContract(
361
353
  iv = contract.implied_volatility;
362
354
  greeksSource = "massive";
363
355
  } else {
364
- const optionPrice =
365
- contract.last_trade?.price ?? contract.last_quote.midpoint;
356
+ const optionPrice = contract.last_trade?.price ?? contract.last_quote.midpoint;
366
357
  const underlyingPrice = contract.underlying_asset.price;
367
358
  const strike = contract.details.strike_price;
368
359
  const dte = computeDTE(contract.details.expiration_date);
@@ -425,31 +416,24 @@ export class MassiveProvider implements MarketDataProvider {
425
416
  const tier = resolveMassiveDataTier();
426
417
  return {
427
418
  tradeBars: true,
428
- quotes: tier === 'quotes',
429
- greeks: false, // Massive does not provide greeks — we compute via BSM
430
- flatFiles: true, // S3 flat files available via rclone
431
- bulkByRoot: false, // Massive is per-ticker, not bulk-by-root
419
+ quotes: tier === "quotes",
420
+ greeks: false, // Massive does not provide greeks — we compute via BSM
421
+ flatFiles: true, // S3 flat files available via rclone
422
+ bulkByRoot: false, // Massive is per-ticker, not bulk-by-root
432
423
  perTicker: true,
433
424
  minuteBars: true,
434
425
  dailyBars: true,
435
426
  dataAvailability: {
436
- option: { from: '2014-01-02' },
437
- index: { from: '2023-02-14' },
438
- stock: { from: '2014-01-02' },
427
+ option: { from: "2014-01-02" },
428
+ index: { from: "2023-02-14" },
429
+ stock: { from: "2014-01-02" },
439
430
  },
440
431
  };
441
432
  }
442
433
 
443
434
  async fetchBars(options: FetchBarsOptions): Promise<BarRow[]> {
444
435
  const apiKey = getApiKey();
445
- const {
446
- ticker,
447
- from,
448
- to,
449
- timespan = "day",
450
- multiplier = 1,
451
- assetClass = "stock",
452
- } = options;
436
+ const { ticker, from, to, timespan = "day", multiplier = 1, assetClass = "stock" } = options;
453
437
 
454
438
  const apiTicker = toMassiveTicker(ticker, assetClass);
455
439
  const storageTicker = fromMassiveTicker(apiTicker);
@@ -466,22 +450,18 @@ export class MassiveProvider implements MarketDataProvider {
466
450
  pageCount++;
467
451
  if (pageCount > MASSIVE_MAX_PAGES) {
468
452
  throw new Error(
469
- `Pagination safety limit reached (${MASSIVE_MAX_PAGES} pages) — possible API issue`
453
+ `Pagination safety limit reached (${MASSIVE_MAX_PAGES} pages) — possible API issue`,
470
454
  );
471
455
  }
472
456
 
473
457
  const response = await fetchWithRetry(url, headers);
474
458
 
475
459
  if (response.status === 401) {
476
- throw new Error(
477
- "MASSIVE_API_KEY rejected by Massive.com — check your key"
478
- );
460
+ throw new Error("MASSIVE_API_KEY rejected by Massive.com — check your key");
479
461
  }
480
462
 
481
463
  if (!response.ok) {
482
- throw new Error(
483
- `Massive.com API error: HTTP ${response.status} ${response.statusText}`
484
- );
464
+ throw new Error(`Massive.com API error: HTTP ${response.status} ${response.statusText}`);
485
465
  }
486
466
 
487
467
  const json = await response.json();
@@ -516,9 +496,7 @@ export class MassiveProvider implements MarketDataProvider {
516
496
  const nextUrlObj = new URL(data.next_url);
517
497
  const cursor = nextUrlObj.searchParams.get("cursor") ?? data.next_url;
518
498
  if (seenCursors.has(cursor)) {
519
- throw new Error(
520
- `Pagination loop detected — cursor repeated: ${cursor.slice(0, 50)}...`
521
- );
499
+ throw new Error(`Pagination loop detected — cursor repeated: ${cursor.slice(0, 50)}...`);
522
500
  }
523
501
  seenCursors.add(cursor);
524
502
  url = data.next_url;
@@ -557,8 +535,8 @@ export class MassiveProvider implements MarketDataProvider {
557
535
  const result = new Map<string, MinuteQuote>();
558
536
 
559
537
  // Iterate trading days in the range
560
- const startDate = new Date(from + 'T00:00:00');
561
- const endDate = new Date(to + 'T00:00:00');
538
+ const startDate = new Date(from + "T00:00:00");
539
+ const endDate = new Date(to + "T00:00:00");
562
540
  const encodedTicker = encodeURIComponent(apiTicker);
563
541
 
564
542
  for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
@@ -663,28 +641,28 @@ export class MassiveProvider implements MarketDataProvider {
663
641
  }
664
642
 
665
643
  async downloadFlatFile(date: string, assetClass: string): Promise<string | null> {
666
- const [year, month] = date.split('-');
644
+ const [year, month] = date.split("-");
667
645
  const assetPathMap: Record<string, string> = {
668
- option: 'us_options_opra/minute_aggs_v1',
669
- index: 'us_indices/minute_aggs_v1',
670
- stock: 'us_stocks_sip/minute_aggs_v1',
646
+ option: "us_options_opra/minute_aggs_v1",
647
+ index: "us_indices/minute_aggs_v1",
648
+ stock: "us_stocks_sip/minute_aggs_v1",
671
649
  };
672
650
  const assetPath = assetPathMap[assetClass] ?? assetPathMap.stock;
673
651
  const s3Path = `s3massive:flatfiles/${assetPath}/${year}/${month}/${date}.csv.gz`;
674
652
  // Separate tmp dirs per asset class to avoid file collisions
675
- const tmpDir = assetClass === 'index' ? '/tmp/massive-flat-index' : '/tmp/massive-flat';
653
+ const tmpDir = assetClass === "index" ? "/tmp/massive-flat-index" : "/tmp/massive-flat";
676
654
  const localPath = `${tmpDir}/${date}.csv.gz`;
677
655
 
678
- const { existsSync, mkdirSync } = await import('fs');
679
- const { execFile } = await import('child_process');
680
- const { promisify } = await import('util');
656
+ const { existsSync, mkdirSync } = await import("fs");
657
+ const { execFile } = await import("child_process");
658
+ const { promisify } = await import("util");
681
659
  const execFileAsync = promisify(execFile);
682
660
 
683
661
  mkdirSync(tmpDir, { recursive: true });
684
662
  if (existsSync(localPath)) return localPath;
685
663
 
686
664
  try {
687
- await execFileAsync('rclone', ['copy', s3Path, `${tmpDir}/`], { timeout: 120_000 });
665
+ await execFileAsync("rclone", ["copy", s3Path, `${tmpDir}/`], { timeout: 120_000 });
688
666
  } catch {
689
667
  return null;
690
668
  }
@@ -695,7 +673,7 @@ export class MassiveProvider implements MarketDataProvider {
695
673
  const { date, dataset, assetClass, tickers, outputPath } = options;
696
674
 
697
675
  // Skip if output Parquet already exists
698
- const { existsSync: exists, mkdirSync: mkdir } = await import('fs');
676
+ const { existsSync: exists, mkdirSync: mkdir } = await import("fs");
699
677
  if (exists(outputPath)) {
700
678
  return { rowCount: 0, skipped: true };
701
679
  }
@@ -706,21 +684,21 @@ export class MassiveProvider implements MarketDataProvider {
706
684
  return { rowCount: 0, skipped: true };
707
685
  }
708
686
 
709
- const { execFile } = await import('child_process');
710
- const { promisify } = await import('util');
711
- const { dirname } = await import('path');
687
+ const { execFile } = await import("child_process");
688
+ const { promisify } = await import("util");
689
+ const { dirname } = await import("path");
712
690
  const execFileAsync = promisify(execFile);
713
691
 
714
692
  // S3 path mapping
715
693
  const s3PathMap: Record<string, Record<string, string>> = {
716
694
  option: {
717
- minute_bars: 'us_options_opra/minute_aggs_v1',
718
- daily_bars: 'us_options_opra/day_aggs_v1',
719
- trades: 'us_options_opra/trades_v1',
695
+ minute_bars: "us_options_opra/minute_aggs_v1",
696
+ daily_bars: "us_options_opra/day_aggs_v1",
697
+ trades: "us_options_opra/trades_v1",
720
698
  },
721
699
  index: {
722
- minute_bars: 'us_indices/minute_aggs_v1',
723
- daily_bars: 'us_indices/day_aggs_v1',
700
+ minute_bars: "us_indices/minute_aggs_v1",
701
+ daily_bars: "us_indices/day_aggs_v1",
724
702
  },
725
703
  };
726
704
 
@@ -729,7 +707,7 @@ export class MassiveProvider implements MarketDataProvider {
729
707
  throw new Error(`Unsupported asset class/dataset combination: ${assetClass}/${dataset}`);
730
708
  }
731
709
 
732
- const [year, month] = date.split('-');
710
+ const [year, month] = date.split("-");
733
711
  const s3Path = `s3massive:flatfiles/${s3Subpath}/${year}/${month}/${date}.csv.gz`;
734
712
  const tmpDir = `/tmp/massive-bulk-${assetClass}-${dataset}`;
735
713
  const localCsv = `${tmpDir}/${date}.csv.gz`;
@@ -740,21 +718,19 @@ export class MassiveProvider implements MarketDataProvider {
740
718
  try {
741
719
  // Download CSV.gz from S3 via rclone (if not already cached)
742
720
  if (!exists(localCsv)) {
743
- await execFileAsync('rclone', ['copy', s3Path, `${tmpDir}/`], { timeout: 300_000 });
721
+ await execFileAsync("rclone", ["copy", s3Path, `${tmpDir}/`], { timeout: 300_000 });
744
722
  if (!exists(localCsv)) {
745
723
  throw new Error(`rclone download failed — file not found: ${localCsv}`);
746
724
  }
747
725
  }
748
726
 
749
727
  // Build ticker filter WHERE clause
750
- const prefix = assetClass === 'option' ? 'O:' : 'I:';
751
- const tickerConditions = tickers
752
- .map(t => `ticker LIKE '${prefix}${t}%'`)
753
- .join(' OR ');
728
+ const prefix = assetClass === "option" ? "O:" : "I:";
729
+ const tickerConditions = tickers.map((t) => `ticker LIKE '${prefix}${t}%'`).join(" OR ");
754
730
  const whereClause = `WHERE ${tickerConditions}`;
755
731
 
756
732
  // CSV column definitions differ by asset class
757
- const isOption = assetClass === 'option';
733
+ const isOption = assetClass === "option";
758
734
  const csvColumns = isOption
759
735
  ? "columns = {'ticker': 'VARCHAR', 'volume': 'BIGINT', 'open': 'DOUBLE', 'close': 'DOUBLE', 'high': 'DOUBLE', 'low': 'DOUBLE', 'window_start': 'BIGINT', 'transactions': 'BIGINT'}"
760
736
  : "columns = {'ticker': 'VARCHAR', 'open': 'DOUBLE', 'close': 'DOUBLE', 'high': 'DOUBLE', 'low': 'DOUBLE', 'window_start': 'BIGINT'}";
@@ -797,8 +773,8 @@ export class MassiveProvider implements MarketDataProvider {
797
773
  `;
798
774
 
799
775
  // Use in-memory DuckDB to run the conversion
800
- const { DuckDBInstance } = await import('@duckdb/node-api');
801
- const db = await DuckDBInstance.create(':memory:');
776
+ const { DuckDBInstance } = await import("@duckdb/node-api");
777
+ const db = await DuckDBInstance.create(":memory:");
802
778
  const conn = await db.connect();
803
779
 
804
780
  try {
@@ -806,9 +782,7 @@ export class MassiveProvider implements MarketDataProvider {
806
782
  await conn.run(sql);
807
783
 
808
784
  // Get row count from the written Parquet
809
- const countResult = await conn.runAndReadAll(
810
- `SELECT count(*) AS cnt FROM '${outputPath}'`
811
- );
785
+ const countResult = await conn.runAndReadAll(`SELECT count(*) AS cnt FROM '${outputPath}'`);
812
786
  const rowCount = Number(countResult.getRows()[0][0]);
813
787
  return { rowCount, skipped: false };
814
788
  } finally {
@@ -817,7 +791,7 @@ export class MassiveProvider implements MarketDataProvider {
817
791
  } finally {
818
792
  // Clean up temp CSV (keep output Parquet)
819
793
  try {
820
- const { unlinkSync } = await import('fs');
794
+ const { unlinkSync } = await import("fs");
821
795
  if (exists(localCsv)) unlinkSync(localCsv);
822
796
  } catch {
823
797
  // Ignore cleanup errors
@@ -870,15 +844,11 @@ export class MassiveProvider implements MarketDataProvider {
870
844
  const response = await fetchWithRetry(url, headers);
871
845
 
872
846
  if (response.status === 401) {
873
- throw new Error(
874
- "MASSIVE_API_KEY rejected by Massive.com — check your key",
875
- );
847
+ throw new Error("MASSIVE_API_KEY rejected by Massive.com — check your key");
876
848
  }
877
849
 
878
850
  if (!response.ok) {
879
- throw new Error(
880
- `Massive.com API error: HTTP ${response.status} ${response.statusText}`,
881
- );
851
+ throw new Error(`Massive.com API error: HTTP ${response.status} ${response.statusText}`);
882
852
  }
883
853
 
884
854
  const json = await response.json();
@@ -895,9 +865,7 @@ export class MassiveProvider implements MarketDataProvider {
895
865
 
896
866
  if (data.results.length > 0 && underlyingPrice === 0) {
897
867
  underlyingPrice = data.results[0].underlying_asset.price;
898
- underlyingTicker = fromMassiveTicker(
899
- data.results[0].underlying_asset.ticker,
900
- );
868
+ underlyingTicker = fromMassiveTicker(data.results[0].underlying_asset.ticker);
901
869
  }
902
870
 
903
871
  for (const contract of data.results) {
@@ -908,9 +876,7 @@ export class MassiveProvider implements MarketDataProvider {
908
876
  const nextUrlObj = new URL(data.next_url);
909
877
  const cursor = nextUrlObj.searchParams.get("cursor") ?? data.next_url;
910
878
  if (seenCursors.has(cursor)) {
911
- throw new Error(
912
- `Pagination loop detected — cursor repeated: ${cursor.slice(0, 50)}...`,
913
- );
879
+ throw new Error(`Pagination loop detected — cursor repeated: ${cursor.slice(0, 50)}...`);
914
880
  }
915
881
  seenCursors.add(cursor);
916
882
  url = data.next_url;
@@ -78,20 +78,10 @@ export interface BackfillRewriteSelectInput {
78
78
  }
79
79
 
80
80
  export function backfillManifestPath(dataRoot: string, runId: string): string {
81
- return join(
82
- dataRoot,
83
- "market",
84
- "_manifests",
85
- "thetadata-mdds-backfill",
86
- `${runId}.ndjson`,
87
- );
81
+ return join(dataRoot, "market", "_manifests", "thetadata-mdds-backfill", `${runId}.ndjson`);
88
82
  }
89
83
 
90
- export function backfillPartitionPath(
91
- dataRoot: string,
92
- underlying: string,
93
- date: string,
94
- ): string {
84
+ export function backfillPartitionPath(dataRoot: string, underlying: string, date: string): string {
95
85
  return join(
96
86
  dataRoot,
97
87
  "market",
@@ -198,9 +188,12 @@ export function groupBackfillTickersByGreekBand(
198
188
  date: string,
199
189
  ): BackfillGreekBandGroup[] {
200
190
  const validatedDate = validateDate(date);
201
- const byKey = new Map<string, BackfillGreekBandGroup & {
202
- contractByTicker: Map<string, BackfillParsedOccTicker>;
203
- }>();
191
+ const byKey = new Map<
192
+ string,
193
+ BackfillGreekBandGroup & {
194
+ contractByTicker: Map<string, BackfillParsedOccTicker>;
195
+ }
196
+ >();
204
197
 
205
198
  for (const ticker of tickers) {
206
199
  const contract = parseBackfillOccTicker(ticker);
@@ -227,7 +220,7 @@ export function groupBackfillTickersByGreekBand(
227
220
  expiration: group.expiration,
228
221
  date: group.date,
229
222
  contracts: [...group.contractByTicker.values()].sort((left, right) =>
230
- left.ticker.localeCompare(right.ticker)
223
+ left.ticker.localeCompare(right.ticker),
231
224
  ),
232
225
  }))
233
226
  .sort((left, right) => left.key.localeCompare(right.key));
@@ -256,9 +249,7 @@ export function collectBackfillConcreteFallbacks(
256
249
  if (coveredTimes.size === 0 && !input.fallbackUncoveredContracts) {
257
250
  continue;
258
251
  }
259
- const missingTimes = [...expectedTimes]
260
- .filter((time) => !coveredTimes.has(time))
261
- .sort();
252
+ const missingTimes = [...expectedTimes].filter((time) => !coveredTimes.has(time)).sort();
262
253
  if (missingTimes.length > 0) {
263
254
  fallbacks.push({ contract, missingTimes });
264
255
  }
@@ -346,10 +337,10 @@ function sanitizeCount(name: string, value: number): number {
346
337
 
347
338
  function validateManifestStatus(value: BackfillManifestStatus): BackfillManifestStatus {
348
339
  if (
349
- value === "prepared"
350
- || value === "committed"
351
- || value === "failed"
352
- || value === "committed_manifest_failed"
340
+ value === "prepared" ||
341
+ value === "committed" ||
342
+ value === "failed" ||
343
+ value === "committed_manifest_failed"
353
344
  ) {
354
345
  return value;
355
346
  }
@@ -41,9 +41,11 @@ export function buildThetaQueryInfo(sessionId: string, env: NodeJS.ProcessEnv =
41
41
  }
42
42
 
43
43
  export function isRetryableGrpcCode(code: number | undefined): boolean {
44
- return code === grpc.status.UNAVAILABLE
45
- || code === grpc.status.DEADLINE_EXCEEDED
46
- || code === grpc.status.RESOURCE_EXHAUSTED;
44
+ return (
45
+ code === grpc.status.UNAVAILABLE ||
46
+ code === grpc.status.DEADLINE_EXCEEDED ||
47
+ code === grpc.status.RESOURCE_EXHAUSTED
48
+ );
47
49
  }
48
50
 
49
51
  interface MddsStreamCall<T = unknown> {
@@ -104,7 +106,7 @@ export class ThetaMddsClient {
104
106
  const credentials = resolveThetaCredentials(this.env);
105
107
  const auth = await authenticateThetaData(credentials, this.fetchImpl);
106
108
  const config = getThetaMddsConfig(this.env);
107
- const pkg = await this.loadGrpcPackage() as {
109
+ const pkg = (await this.loadGrpcPackage()) as {
108
110
  BetaEndpoints?: {
109
111
  BetaThetaTerminal?: new (
110
112
  target: string,
@@ -124,7 +126,8 @@ export class ThetaMddsClient {
124
126
  throw new Error("ThetaMddsClient connection was closed before it completed");
125
127
  }
126
128
  this.sessionId = auth.sessionId;
127
- this.concurrencyLimit = config.maxConcurrency ?? thetaConcurrencyForTier(auth.optionsSubscription);
129
+ this.concurrencyLimit =
130
+ config.maxConcurrency ?? thetaConcurrencyForTier(auth.optionsSubscription);
128
131
  this.stub = stub;
129
132
  }
130
133
 
@@ -199,9 +202,10 @@ export class ThetaMddsClient {
199
202
  });
200
203
  } catch (error) {
201
204
  lastError = error;
202
- const code = typeof error === "object" && error && "code" in error
203
- ? Number((error as { code: unknown }).code)
204
- : undefined;
205
+ const code =
206
+ typeof error === "object" && error && "code" in error
207
+ ? Number((error as { code: unknown }).code)
208
+ : undefined;
205
209
  if (!isRetryableGrpcCode(code) || attempt === config.maxAttempts) throw error;
206
210
  this.release();
207
211
  releasePermit = false;
@@ -14,26 +14,8 @@ export interface ThetaPriceLike {
14
14
  }
15
15
 
16
16
  const PRICE_TYPE_FACTORS = [
17
- 0,
18
- 1e-9,
19
- 1e-8,
20
- 1e-7,
21
- 1e-6,
22
- 1e-5,
23
- 1e-4,
24
- 1e-3,
25
- 1e-2,
26
- 1e-1,
27
- 1,
28
- 10,
29
- 100,
30
- 1000,
31
- 10000,
32
- 100000,
33
- 1000000,
34
- 10000000,
35
- 100000000,
36
- 1000000000,
17
+ 0, 1e-9, 1e-8, 1e-7, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2, 1e-1, 1, 10, 100, 1000, 10000, 100000, 1000000,
18
+ 10000000, 100000000, 1000000000,
37
19
  ] as const;
38
20
 
39
21
  const ET_MINUTE_PATTERN = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/;
@@ -94,9 +94,10 @@ export function normalizeThetaFirstOrderGreekRow(
94
94
  theta: asNumber(row.theta),
95
95
  vega: asNumber(row.vega),
96
96
  iv: asNumber(row.implied_vol ?? row.implied_volatility),
97
- underlyingTimestamp: row.underlying_timestamp == null
98
- ? null
99
- : thetaTimestampToEtMinute(asText(row.underlying_timestamp)),
97
+ underlyingTimestamp:
98
+ row.underlying_timestamp == null
99
+ ? null
100
+ : thetaTimestampToEtMinute(asText(row.underlying_timestamp)),
100
101
  underlyingPrice: asNumber(row.underlying_price),
101
102
  };
102
103
  }
@@ -110,9 +111,10 @@ export function normalizeThetaImpliedVolatilityRow(
110
111
  midIv: asNumber(row.implied_vol ?? row.midpoint_implied_vol),
111
112
  askIv: asNumber(row.ask_implied_vol),
112
113
  ivError: asNumber(row.iv_error),
113
- underlyingTimestamp: row.underlying_timestamp == null
114
- ? null
115
- : thetaTimestampToEtMinute(asText(row.underlying_timestamp)),
114
+ underlyingTimestamp:
115
+ row.underlying_timestamp == null
116
+ ? null
117
+ : thetaTimestampToEtMinute(asText(row.underlying_timestamp)),
116
118
  underlyingPrice: asNumber(row.underlying_price),
117
119
  };
118
120
  }
@@ -176,9 +178,7 @@ function parseThetaOhlcDateAndMsOfDay(
176
178
 
177
179
  // ThetaData stock OHLC rows: wire format returns a single `timestamp` string
178
180
  // ("YYYY-MM-DD HH:MM" ET) alongside open/high/low/close/volume.
179
- export function normalizeThetaStockOhlcRow(
180
- row: Record<string, ThetaCellValue>,
181
- ): ThetaStockOhlcRow {
181
+ export function normalizeThetaStockOhlcRow(row: Record<string, ThetaCellValue>): ThetaStockOhlcRow {
182
182
  const { date, msOfDay } = parseThetaOhlcDateAndMsOfDay(row, "stock OHLC row");
183
183
  return {
184
184
  date,
@@ -193,9 +193,7 @@ export function normalizeThetaStockOhlcRow(
193
193
 
194
194
  // ThetaData EOD wire format uses `last_trade` for the trading-day date
195
195
  // (mirrors the index variant). Accept `date` too for any provider variant.
196
- export function normalizeThetaStockEodRow(
197
- row: Record<string, ThetaCellValue>,
198
- ): ThetaStockEodRow {
196
+ export function normalizeThetaStockEodRow(row: Record<string, ThetaCellValue>): ThetaStockEodRow {
199
197
  return {
200
198
  date: normalizeThetaDate(row.last_trade ?? row.date, "stock EOD row"),
201
199
  open: requiredNumber(row.open, "stock EOD row", "open"),
@@ -206,9 +204,7 @@ export function normalizeThetaStockEodRow(
206
204
  };
207
205
  }
208
206
 
209
- export function normalizeThetaIndexOhlcRow(
210
- row: Record<string, ThetaCellValue>,
211
- ): ThetaIndexOhlcRow {
207
+ export function normalizeThetaIndexOhlcRow(row: Record<string, ThetaCellValue>): ThetaIndexOhlcRow {
212
208
  const { date, msOfDay } = parseThetaOhlcDateAndMsOfDay(row, "index OHLC row");
213
209
  return {
214
210
  date,
@@ -221,9 +217,7 @@ export function normalizeThetaIndexOhlcRow(
221
217
  };
222
218
  }
223
219
 
224
- export function normalizeThetaIndexEodRow(
225
- row: Record<string, ThetaCellValue>,
226
- ): ThetaIndexEodRow {
220
+ export function normalizeThetaIndexEodRow(row: Record<string, ThetaCellValue>): ThetaIndexEodRow {
227
221
  return {
228
222
  date: normalizeThetaDate(row.last_trade ?? row.date, "index EOD row"),
229
223
  open: requiredNumber(row.open, "index EOD row", "open"),
@@ -352,11 +346,12 @@ export async function stockHistoryOhlc(
352
346
  // ThetaData stock OHLC streams can include null-OHLC rows on auction/pre-open
353
347
  // ticks. Drop those before normalizing so downstream BarRow stays numeric.
354
348
  return decodeThetaRows(chunks)
355
- .filter((row) =>
356
- Number.isFinite(row.open as number) &&
357
- Number.isFinite(row.high as number) &&
358
- Number.isFinite(row.low as number) &&
359
- Number.isFinite(row.close as number),
349
+ .filter(
350
+ (row) =>
351
+ Number.isFinite(row.open as number) &&
352
+ Number.isFinite(row.high as number) &&
353
+ Number.isFinite(row.low as number) &&
354
+ Number.isFinite(row.close as number),
360
355
  )
361
356
  .map(normalizeThetaStockOhlcRow);
362
357
  }
@@ -414,11 +409,12 @@ export async function indexHistoryOhlc(
414
409
  // 09:30 print). Drop those before normalizing so the strict number contract
415
410
  // on downstream BarRow still holds.
416
411
  return decodeThetaRows(chunks)
417
- .filter((row) =>
418
- Number.isFinite(row.open as number) &&
419
- Number.isFinite(row.high as number) &&
420
- Number.isFinite(row.low as number) &&
421
- Number.isFinite(row.close as number),
412
+ .filter(
413
+ (row) =>
414
+ Number.isFinite(row.open as number) &&
415
+ Number.isFinite(row.high as number) &&
416
+ Number.isFinite(row.low as number) &&
417
+ Number.isFinite(row.close as number),
422
418
  )
423
419
  .map(normalizeThetaIndexOhlcRow);
424
420
  }
@@ -538,9 +534,8 @@ export async function optionHistoryGreeksFirstOrderBand(
538
534
  ): Promise<ThetaFirstOrderGreekRow[]> {
539
535
  const symbol = validateSymbol(params.symbol);
540
536
  // Accept "*" wildcard to fetch all expirations in one call.
541
- const expiration = params.expiration === "*"
542
- ? "*"
543
- : validateHyphenDate(params.expiration, "expiration");
537
+ const expiration =
538
+ params.expiration === "*" ? "*" : validateHyphenDate(params.expiration, "expiration");
544
539
  const date = validateHyphenDate(params.date, "date");
545
540
  const strikeRange = optionalPositiveInteger(params.strikeRange, "strike_range");
546
541
  const chunks = await client.callStream<ThetaResponseData>(
@@ -593,9 +588,8 @@ export async function optionHistoryImpliedVolatilityBand(
593
588
  // Allow "*" wildcard like optionHistoryQuoteBand — server returns all active
594
589
  // expirations in one call. Each row carries its own expiration. Subject to
595
590
  // the same 1-concurrent-stream-per-session cap as wildcard quote calls.
596
- const expiration = params.expiration === "*"
597
- ? "*"
598
- : validateHyphenDate(params.expiration, "expiration");
591
+ const expiration =
592
+ params.expiration === "*" ? "*" : validateHyphenDate(params.expiration, "expiration");
599
593
  const date = validateHyphenDate(params.date, "date");
600
594
  const strikeRange = optionalPositiveInteger(params.strikeRange, "strike_range");
601
595
  const chunks = await client.callStream<ThetaResponseData>(
@@ -637,9 +631,8 @@ export async function optionHistoryQuoteBand(
637
631
  // Allow "*" wildcard — MDDS server returns all active expirations in one call
638
632
  // (each row carries its own expiration). Empirically ~1.6x faster than
639
633
  // iterating per-expiration; only one wildcard stream allowed per session.
640
- const expiration = params.expiration === "*"
641
- ? "*"
642
- : validateHyphenDate(params.expiration, "expiration");
634
+ const expiration =
635
+ params.expiration === "*" ? "*" : validateHyphenDate(params.expiration, "expiration");
643
636
  const date = validateHyphenDate(params.date, "date");
644
637
  const strikeRange = optionalPositiveInteger(params.strikeRange, "strike_range");
645
638
  const chunks = await client.callStream<ThetaResponseData>(
@@ -686,9 +679,8 @@ export async function optionHistoryOpenInterest(
686
679
  },
687
680
  ): Promise<ThetaOpenInterestRow[]> {
688
681
  const symbol = validateSymbol(params.symbol);
689
- const expiration = params.expiration === "*"
690
- ? "*"
691
- : validateHyphenDate(params.expiration, "expiration");
682
+ const expiration =
683
+ params.expiration === "*" ? "*" : validateHyphenDate(params.expiration, "expiration");
692
684
  const startDate = validateHyphenDate(params.startDate, "date");
693
685
  const endDate = validateHyphenDate(params.endDate, "date");
694
686
  const strikeRange = optionalPositiveInteger(params.strikeRange, "strike_range");