tradeblocks-mcp 3.0.2 → 3.3.1
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.
- package/README.md +85 -80
- package/dist/{chunk-LDKTV7GW.js → chunk-27S67XW3.js} +24 -52
- package/dist/chunk-27S67XW3.js.map +1 -0
- package/dist/{chunk-FGZH632F.js → chunk-2E63THNI.js} +13 -11
- package/dist/chunk-2E63THNI.js.map +1 -0
- package/dist/{chunk-QTTR7AAW.js → chunk-2I3S2ZLT.js} +325 -84
- package/dist/chunk-2I3S2ZLT.js.map +1 -0
- package/dist/{chunk-4BLCXNQ6.js → chunk-4HZLVUOZ.js} +179 -183
- package/dist/chunk-4HZLVUOZ.js.map +1 -0
- package/dist/{chunk-XXYOUIZY.js → chunk-NWNSKALN.js} +63 -80
- package/dist/chunk-NWNSKALN.js.map +1 -0
- package/dist/{chunk-PRAYH3RT.js → chunk-WNTYA7ER.js} +29 -12
- package/dist/chunk-WNTYA7ER.js.map +1 -0
- package/dist/{chunk-BKQ4PM4Y.js → chunk-YZA4RS76.js} +24 -18
- package/dist/chunk-YZA4RS76.js.map +1 -0
- package/dist/{chunk-W2PP3LEH.js → chunk-ZTJXC3LS.js} +17 -8
- package/dist/chunk-ZTJXC3LS.js.map +1 -0
- package/dist/daily-log-processor-MZW2XBZY.js +9 -0
- package/dist/{daily-logs-store-D3QC2XPV.js → daily-logs-store-3C3OTGPK.js} +2 -2
- package/dist/iv-solver-worker.js +9 -1
- package/dist/market-provider-6U33TQUT.js +16 -0
- package/dist/{proto-3HZTCWK4.js → proto-V23GIZG2.js} +2 -2
- package/dist/{sync-PO4IPCYV.js → sync-3UBC37VW.js} +7 -7
- package/dist/test-exports.js +876 -483
- package/dist/test-exports.js.map +1 -1
- package/dist/trade-processor-DKDCWGKD.js +9 -0
- package/dist/{trades-store-LOL3FQSF.js → trades-store-R3NL25T2.js} +2 -2
- package/package.json +2 -6
- package/server/{chunk-CSDVJPBB.js → chunk-536NSFED.js} +29 -12
- package/server/chunk-536NSFED.js.map +1 -0
- package/server/{chunk-WA5AAPCH.js → chunk-A3UKD64A.js} +17 -8
- package/server/chunk-A3UKD64A.js.map +1 -0
- package/server/{chunk-PNKG7RY7.js → chunk-OMUDNJBJ.js} +63 -80
- package/server/chunk-OMUDNJBJ.js.map +1 -0
- package/server/{chunk-72GKJE2U.js → chunk-RJDJHZ5Y.js} +934 -335
- package/server/chunk-RJDJHZ5Y.js.map +1 -0
- package/server/{chunk-NZO6PT64.js → chunk-RM4B2VKS.js} +167 -177
- package/server/chunk-RM4B2VKS.js.map +1 -0
- package/server/{chunk-5EBXHT6C.js → chunk-SY6GDNVI.js} +24 -18
- package/server/chunk-SY6GDNVI.js.map +1 -0
- package/server/{chunk-FBNDMCT5.js → chunk-T66KH2XH.js} +13 -11
- package/server/chunk-T66KH2XH.js.map +1 -0
- package/server/{chunk-NRFXAJF7.js → chunk-W5E7FHC4.js} +24 -52
- package/server/chunk-W5E7FHC4.js.map +1 -0
- package/server/{config-6IZXEFEX.js → config-DK7KOMNL.js} +1 -1
- package/server/config-DK7KOMNL.js.map +1 -0
- package/server/daily-log-processor-B2VPU2FQ.js +10 -0
- package/server/{daily-logs-store-NU7M4L5C.js → daily-logs-store-YFNXTB7J.js} +2 -2
- package/server/http-server.js +2 -5
- package/server/index.js +2497 -2029
- package/server/index.js.map +1 -1
- package/server/iv-solver-worker.js +9 -1
- package/server/market-provider-L56RFHAC.js +17 -0
- package/server/{proto-DWRZJO4E.js → proto-YZ4TLX2N.js} +2 -2
- package/server/{sync-QFI5L7S7.js → sync-CENKDN53.js} +7 -7
- package/server/trade-processor-3PI4LHZI.js +10 -0
- package/server/{trades-store-VY26MWER.js → trades-store-RTTEYMXH.js} +2 -2
- package/src/auth/clients-store.ts +4 -4
- package/src/auth/code-store.ts +1 -1
- package/src/auth/config.ts +11 -12
- package/src/auth/login-page.ts +8 -10
- package/src/auth/provider.ts +35 -30
- package/src/auth/token.ts +17 -15
- package/src/db/backtest-schemas.ts +38 -8
- package/src/db/connection.ts +107 -40
- package/src/db/data-root.ts +22 -0
- package/src/db/index.ts +37 -5
- package/src/db/json-adapters.ts +22 -31
- package/src/db/json-migration.ts +11 -15
- package/src/db/json-store.ts +5 -3
- package/src/db/market-datasets.ts +48 -15
- package/src/db/market-schemas.ts +11 -5
- package/src/db/market-views.ts +119 -32
- package/src/db/parquet-writer.ts +7 -7
- package/src/db/profile-schemas.ts +39 -22
- package/src/db/schemas.ts +2 -2
- package/src/http-server.ts +7 -12
- package/src/index.ts +8 -9
- package/src/market/ingestor/index.ts +1 -4
- package/src/market/ingestor/market-ingestor.ts +140 -67
- package/src/market/ingestor/types.ts +1 -3
- package/src/market/stores/chain-sql.ts +2 -8
- package/src/market/stores/chain-store.ts +1 -4
- package/src/market/stores/duckdb-chain-store.ts +3 -14
- package/src/market/stores/duckdb-enriched-store.ts +3 -13
- package/src/market/stores/duckdb-quote-store.ts +10 -27
- package/src/market/stores/duckdb-spot-store.ts +4 -20
- package/src/market/stores/enriched-sql.ts +3 -9
- package/src/market/stores/enriched-store.ts +2 -2
- package/src/market/stores/parquet-chain-store.ts +5 -22
- package/src/market/stores/parquet-enriched-store.ts +4 -16
- package/src/market/stores/parquet-oi-daily-store.ts +3 -14
- package/src/market/stores/parquet-quote-store.ts +9 -29
- package/src/market/stores/parquet-spot-store.ts +14 -33
- package/src/market/stores/quote-store.ts +2 -10
- package/src/market/stores/spot-sql.ts +3 -15
- package/src/market/stores/spot-store.ts +1 -1
- package/src/market/tickers/defaults.json +6 -6
- package/src/market/tickers/loader.ts +2 -7
- package/src/market/tickers/registry.ts +5 -15
- package/src/market/tickers/schemas.ts +2 -6
- package/src/models/strategy-profile.ts +39 -39
- package/src/sync/block-sync.ts +44 -65
- package/src/sync/index.ts +1 -4
- package/src/sync/metadata.ts +14 -20
- package/src/test-exports.ts +139 -123
- package/src/tools/analysis.ts +180 -159
- package/src/tools/batch-exit-analysis.ts +100 -59
- package/src/tools/blocks/analysis.ts +116 -101
- package/src/tools/blocks/comparison.ts +281 -364
- package/src/tools/blocks/core.ts +208 -253
- package/src/tools/blocks/health.ts +571 -563
- package/src/tools/blocks/index.ts +2 -0
- package/src/tools/blocks/paired-comparison.ts +425 -0
- package/src/tools/blocks/similarity.ts +316 -283
- package/src/tools/edge-decay.ts +124 -197
- package/src/tools/exit-analysis.ts +130 -77
- package/src/tools/greeks-attribution.ts +84 -35
- package/src/tools/guides.ts +4 -6
- package/src/tools/imports.ts +11 -14
- package/src/tools/market-data.ts +1067 -757
- package/src/tools/market-enrichment.ts +3 -3
- package/src/tools/market-fetch.ts +148 -67
- package/src/tools/market-imports.ts +12 -12
- package/src/tools/middleware/sync-middleware.ts +5 -6
- package/src/tools/performance.ts +185 -302
- package/src/tools/profile-analysis.ts +52 -66
- package/src/tools/profiles.ts +106 -69
- package/src/tools/regime-advisor.ts +20 -45
- package/src/tools/replay.ts +81 -77
- package/src/tools/reports/discrepancies.ts +298 -328
- package/src/tools/reports/fields.ts +7 -25
- package/src/tools/reports/helpers.ts +18 -49
- package/src/tools/reports/predictive.ts +27 -70
- package/src/tools/reports/slippage-trends.ts +315 -345
- package/src/tools/reports/slippage.ts +1 -4
- package/src/tools/reports/strategy-matches.ts +399 -441
- package/src/tools/schema.ts +43 -40
- package/src/tools/shared/filters.ts +3 -9
- package/src/tools/snapshot.ts +9 -30
- package/src/tools/sql.ts +15 -14
- package/src/tools/tickers.ts +1 -4
- package/src/utils/batch-exit-analysis.ts +31 -42
- package/src/utils/black-scholes.ts +39 -29
- package/src/utils/block-loader.ts +69 -83
- package/src/utils/calibration-probe.ts +3 -4
- package/src/utils/chain-loader.ts +3 -3
- package/src/utils/csv-discovery.ts +16 -22
- package/src/utils/data-quality.ts +24 -36
- package/src/utils/exit-triggers.ts +91 -96
- package/src/utils/field-timing.ts +94 -79
- package/src/utils/filter-predicates.ts +13 -9
- package/src/utils/flatfile-importer.ts +94 -64
- package/src/utils/greeks-decomposition.ts +152 -100
- package/src/utils/iv-solver-pool.ts +55 -25
- package/src/utils/iv-solver-worker.ts +5 -5
- package/src/utils/market-enricher.ts +528 -497
- package/src/utils/market-importer.ts +31 -12
- package/src/utils/market-provider.ts +21 -23
- package/src/utils/massive-tier.ts +5 -7
- package/src/utils/migrate-option-data-helpers.ts +2 -8
- package/src/utils/option-quote-greeks.ts +25 -31
- package/src/utils/option-time.ts +4 -8
- package/src/utils/output-formatter.ts +1 -4
- package/src/utils/provider-capabilities.ts +1 -4
- package/src/utils/providers/massive.ts +59 -93
- package/src/utils/providers/thetadata/backfill.ts +14 -23
- package/src/utils/providers/thetadata/client.ts +12 -8
- package/src/utils/providers/thetadata/decode.ts +2 -20
- package/src/utils/providers/thetadata/endpoints.ts +32 -40
- package/src/utils/providers/thetadata/join.ts +11 -10
- package/src/utils/providers/thetadata/proto.ts +12 -10
- package/src/utils/providers/thetadata/quote-mid-greeks.ts +5 -5
- package/src/utils/providers/thetadata.ts +11 -10
- package/src/utils/quote-enricher.ts +4 -4
- package/src/utils/quote-parquet-projection.ts +3 -11
- package/src/utils/sample-date-selector.ts +3 -5
- package/src/utils/schema-metadata.ts +102 -70
- package/src/utils/ticker.ts +5 -9
- package/src/utils/trade-replay.ts +77 -68
- package/dist/chunk-4BLCXNQ6.js.map +0 -1
- package/dist/chunk-BKQ4PM4Y.js.map +0 -1
- package/dist/chunk-FGZH632F.js.map +0 -1
- package/dist/chunk-LDKTV7GW.js.map +0 -1
- package/dist/chunk-PRAYH3RT.js.map +0 -1
- package/dist/chunk-QTTR7AAW.js.map +0 -1
- package/dist/chunk-W2PP3LEH.js.map +0 -1
- package/dist/chunk-XXYOUIZY.js.map +0 -1
- package/dist/daily-log-processor-6MWJ23JK.js +0 -9
- package/dist/market-provider-VDRJUEF2.js +0 -16
- package/dist/trade-processor-NHU4VWRX.js +0 -9
- package/manifest.json +0 -141
- package/server/chunk-5EBXHT6C.js.map +0 -1
- package/server/chunk-72GKJE2U.js.map +0 -1
- package/server/chunk-CSDVJPBB.js.map +0 -1
- package/server/chunk-FBNDMCT5.js.map +0 -1
- package/server/chunk-NRFXAJF7.js.map +0 -1
- package/server/chunk-NZO6PT64.js.map +0 -1
- package/server/chunk-PNKG7RY7.js.map +0 -1
- package/server/chunk-WA5AAPCH.js.map +0 -1
- package/server/config-6IZXEFEX.js.map +0 -1
- package/server/daily-log-processor-Y3PVSVBM.js +0 -10
- package/server/market-provider-VOYYVYWT.js +0 -17
- package/server/trade-processor-JWVS37KM.js +0 -10
- /package/dist/{daily-log-processor-6MWJ23JK.js.map → daily-log-processor-MZW2XBZY.js.map} +0 -0
- /package/dist/{daily-logs-store-D3QC2XPV.js.map → daily-logs-store-3C3OTGPK.js.map} +0 -0
- /package/dist/{market-provider-VDRJUEF2.js.map → market-provider-6U33TQUT.js.map} +0 -0
- /package/dist/{proto-3HZTCWK4.js.map → proto-V23GIZG2.js.map} +0 -0
- /package/dist/{sync-PO4IPCYV.js.map → sync-3UBC37VW.js.map} +0 -0
- /package/dist/{trade-processor-NHU4VWRX.js.map → trade-processor-DKDCWGKD.js.map} +0 -0
- /package/dist/{trades-store-LOL3FQSF.js.map → trades-store-R3NL25T2.js.map} +0 -0
- /package/server/{daily-log-processor-Y3PVSVBM.js.map → daily-log-processor-B2VPU2FQ.js.map} +0 -0
- /package/server/{daily-logs-store-NU7M4L5C.js.map → daily-logs-store-YFNXTB7J.js.map} +0 -0
- /package/server/{market-provider-VOYYVYWT.js.map → market-provider-L56RFHAC.js.map} +0 -0
- /package/server/{proto-DWRZJO4E.js.map → proto-YZ4TLX2N.js.map} +0 -0
- /package/server/{sync-QFI5L7S7.js.map → sync-CENKDN53.js.map} +0 -0
- /package/server/{trade-processor-JWVS37KM.js.map → trade-processor-3PI4LHZI.js.map} +0 -0
- /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
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
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 ===
|
|
429
|
-
greeks: false,
|
|
430
|
-
flatFiles: true,
|
|
431
|
-
bulkByRoot: false,
|
|
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:
|
|
437
|
-
index: { from:
|
|
438
|
-
stock: { from:
|
|
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 +
|
|
561
|
-
const endDate = new Date(to +
|
|
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:
|
|
669
|
-
index:
|
|
670
|
-
stock:
|
|
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 ===
|
|
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(
|
|
679
|
-
const { execFile } = await import(
|
|
680
|
-
const { promisify } = await import(
|
|
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(
|
|
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(
|
|
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(
|
|
710
|
-
const { promisify } = await import(
|
|
711
|
-
const { dirname } = await import(
|
|
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:
|
|
718
|
-
daily_bars:
|
|
719
|
-
trades:
|
|
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:
|
|
723
|
-
daily_bars:
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
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(
|
|
801
|
-
const db = await DuckDBInstance.create(
|
|
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(
|
|
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<
|
|
202
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
|
45
|
-
|
|
46
|
-
|
|
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 =
|
|
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 =
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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:
|
|
98
|
-
|
|
99
|
-
|
|
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:
|
|
114
|
-
|
|
115
|
-
|
|
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(
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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(
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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");
|