tradeblocks-mcp 3.0.0 → 3.0.2
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/{server/chunk-JAAQMESY.js → dist/chunk-XXYOUIZY.js} +699 -111
- package/dist/chunk-XXYOUIZY.js.map +1 -0
- package/dist/iv-solver-worker.js +295 -0
- package/dist/{market-provider-GGLA7ACC.js → market-provider-VDRJUEF2.js} +2 -2
- package/dist/test-exports.js +10 -2
- package/dist/test-exports.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +4 -2
- package/{dist/chunk-CCITWAAI.js → server/chunk-PNKG7RY7.js} +455 -311
- package/server/chunk-PNKG7RY7.js.map +1 -0
- package/server/index.js +18 -6
- package/server/index.js.map +1 -1
- package/server/iv-solver-worker.js +295 -0
- package/server/{market-provider-D5SI47K7.js → market-provider-VOYYVYWT.js} +2 -2
- package/src/index.ts +20 -3
- package/src/market/ingestor/market-ingestor.ts +2 -2
- package/src/test-exports.ts +8 -0
- package/src/utils/iv-solver-pool.ts +396 -0
- package/src/utils/iv-solver-worker.ts +112 -0
- package/src/utils/option-quote-greeks.ts +246 -51
- package/src/utils/providers/thetadata/decode.ts +16 -10
- package/dist/chunk-CCITWAAI.js.map +0 -1
- package/server/chunk-JAAQMESY.js.map +0 -1
- /package/dist/{market-provider-GGLA7ACC.js.map → market-provider-VDRJUEF2.js.map} +0 -0
- /package/server/{market-provider-D5SI47K7.js.map → market-provider-VOYYVYWT.js.map} +0 -0
|
@@ -2,6 +2,11 @@ import { getSofrRateByKey } from "@tradeblocks/lib";
|
|
|
2
2
|
import type { ContractRow } from "./chain-loader.ts";
|
|
3
3
|
import { computeLegGreeks } from "./black-scholes.ts";
|
|
4
4
|
import { computeFractionalDte } from "./option-time.ts";
|
|
5
|
+
import {
|
|
6
|
+
getSharedIvSolverPool,
|
|
7
|
+
type IvSolveColumns,
|
|
8
|
+
type IvSolverPool,
|
|
9
|
+
} from "./iv-solver-pool.ts";
|
|
5
10
|
|
|
6
11
|
export type QuoteGreeksSource = "massive" | "thetadata" | "computed";
|
|
7
12
|
export type QuoteGreeksMode = "auto" | "provider" | "compute";
|
|
@@ -58,6 +63,21 @@ function isFiniteNumber(value: unknown): value is number {
|
|
|
58
63
|
return typeof value === "number" && Number.isFinite(value);
|
|
59
64
|
}
|
|
60
65
|
|
|
66
|
+
// `getSofrRateByKey` does a binary search over the rate table on every call,
|
|
67
|
+
// but within a quote-ingest batch the date is constant (one partition = one
|
|
68
|
+
// trading day) and across batches a date repeats for every row of that day.
|
|
69
|
+
// Memoize the lookup by date key — the function is a pure deterministic map
|
|
70
|
+
// from key → rate, so the cached value is identical to a fresh lookup.
|
|
71
|
+
const sofrRateByDateKey = new Map<string, number>();
|
|
72
|
+
|
|
73
|
+
function memoizedSofrRate(dateKey: string): number {
|
|
74
|
+
const cached = sofrRateByDateKey.get(dateKey);
|
|
75
|
+
if (cached !== undefined) return cached;
|
|
76
|
+
const rate = getSofrRateByKey(dateKey);
|
|
77
|
+
sofrRateByDateKey.set(dateKey, rate);
|
|
78
|
+
return rate;
|
|
79
|
+
}
|
|
80
|
+
|
|
61
81
|
export function hasQuoteGreeks(row: QuoteGreekFields): boolean {
|
|
62
82
|
return isFiniteNumber(row.delta ?? null)
|
|
63
83
|
&& isFiniteNumber(row.gamma ?? null)
|
|
@@ -123,7 +143,7 @@ export function computeQuoteGreeks(params: {
|
|
|
123
143
|
if (!(optionPrice > 0) || !(underlyingPrice > 0) || !(strike > 0)) return null;
|
|
124
144
|
const dte = computeFractionalDte(date, time.slice(0, 5), expiration);
|
|
125
145
|
if (!(dte >= 0)) return null;
|
|
126
|
-
const riskFreeRate =
|
|
146
|
+
const riskFreeRate = memoizedSofrRate(date) / 100;
|
|
127
147
|
const result = computeLegGreeks(
|
|
128
148
|
optionPrice,
|
|
129
149
|
underlyingPrice,
|
|
@@ -152,7 +172,7 @@ export function buildUnderlyingPriceKey(date: string, time: string): string {
|
|
|
152
172
|
return `${date}|${time.slice(0, 5)}`;
|
|
153
173
|
}
|
|
154
174
|
|
|
155
|
-
export
|
|
175
|
+
export interface ApplyQuoteGreeksParams<T extends QuoteGreekFields> {
|
|
156
176
|
rows: T[];
|
|
157
177
|
getDate: (row: T) => string;
|
|
158
178
|
getTime: (row: T) => string;
|
|
@@ -161,19 +181,10 @@ export function applyQuoteGreeks<T extends QuoteGreekFields>(params: {
|
|
|
161
181
|
getUnderlyingPrice: (date: string, time: string) => number | undefined;
|
|
162
182
|
mode?: QuoteGreeksMode;
|
|
163
183
|
defaultProviderSource?: Exclude<QuoteGreeksSource, "computed">;
|
|
164
|
-
}
|
|
165
|
-
const {
|
|
166
|
-
rows,
|
|
167
|
-
getDate,
|
|
168
|
-
getTime,
|
|
169
|
-
getMid,
|
|
170
|
-
getContractMeta,
|
|
171
|
-
getUnderlyingPrice,
|
|
172
|
-
mode = "auto",
|
|
173
|
-
defaultProviderSource,
|
|
174
|
-
} = params;
|
|
184
|
+
}
|
|
175
185
|
|
|
176
|
-
|
|
186
|
+
function emptyStats(): QuoteGreeksStats {
|
|
187
|
+
return {
|
|
177
188
|
rowsVisited: 0,
|
|
178
189
|
existingGreeksRows: 0,
|
|
179
190
|
computedRows: 0,
|
|
@@ -182,61 +193,245 @@ export function applyQuoteGreeks<T extends QuoteGreekFields>(params: {
|
|
|
182
193
|
mathFailedRows: 0,
|
|
183
194
|
unresolvedRows: 0,
|
|
184
195
|
};
|
|
196
|
+
}
|
|
185
197
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
198
|
+
/**
|
|
199
|
+
* Outcome of resolving a single row up to (but not including) the IV solve.
|
|
200
|
+
* Skip kinds are fully accounted in stats and never solved. `kind: "compute"`
|
|
201
|
+
* carries the flat solve inputs directly (no nested object — kept flat so the
|
|
202
|
+
* parallel path can write them straight into typed-array columns). This is the
|
|
203
|
+
* single source of truth for resolution, shared by the inline and parallel
|
|
204
|
+
* apply functions so they cannot diverge.
|
|
205
|
+
*
|
|
206
|
+
* `type` is 0 = call ("C"), 1 = put ("P") — the same encoding the worker uses.
|
|
207
|
+
*/
|
|
208
|
+
type RowResolution =
|
|
209
|
+
| { kind: "existing" }
|
|
210
|
+
| { kind: "missingContract" }
|
|
211
|
+
| { kind: "providerUnresolved" }
|
|
212
|
+
| { kind: "missingUnderlying" }
|
|
213
|
+
| { kind: "mathFailed" }
|
|
214
|
+
| {
|
|
215
|
+
kind: "compute";
|
|
216
|
+
optionPrice: number;
|
|
217
|
+
underlyingPrice: number;
|
|
218
|
+
strike: number;
|
|
219
|
+
dte: number;
|
|
220
|
+
type: 0 | 1;
|
|
221
|
+
riskFreeRate: number;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
function resolveQuoteGreeksRow<T extends QuoteGreekFields>(
|
|
225
|
+
row: T,
|
|
226
|
+
params: ApplyQuoteGreeksParams<T>,
|
|
227
|
+
defaultProviderSource: Exclude<QuoteGreeksSource, "computed"> | undefined,
|
|
228
|
+
mode: QuoteGreeksMode,
|
|
229
|
+
): RowResolution {
|
|
230
|
+
if (mode !== "compute" && hasExistingQuoteGreeks(row)) {
|
|
231
|
+
normalizeExistingQuoteGreeks(row, defaultProviderSource);
|
|
232
|
+
return { kind: "existing" };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const meta = params.getContractMeta(row);
|
|
236
|
+
if (!meta) return { kind: "missingContract" };
|
|
237
|
+
|
|
238
|
+
if (mode === "provider") return { kind: "providerUnresolved" };
|
|
239
|
+
|
|
240
|
+
const date = params.getDate(row);
|
|
241
|
+
const time = params.getTime(row).slice(0, 5);
|
|
242
|
+
const underlyingPrice = params.getUnderlyingPrice(date, time);
|
|
243
|
+
if (!(underlyingPrice != null && underlyingPrice > 0)) {
|
|
244
|
+
return { kind: "missingUnderlying" };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Same guards and rate convention as computeQuoteGreeks — kept in lockstep
|
|
248
|
+
// so the solve inputs are identical whether solved inline or on a worker.
|
|
249
|
+
const optionPrice = params.getMid(row);
|
|
250
|
+
if (!(optionPrice > 0) || !(meta.strike > 0)) return { kind: "mathFailed" };
|
|
251
|
+
const dte = computeFractionalDte(date, time, meta.expiration);
|
|
252
|
+
if (!(dte >= 0)) return { kind: "mathFailed" };
|
|
253
|
+
const riskFreeRate = memoizedSofrRate(date) / 100;
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
kind: "compute",
|
|
257
|
+
optionPrice,
|
|
258
|
+
underlyingPrice,
|
|
259
|
+
strike: meta.strike,
|
|
260
|
+
dte,
|
|
261
|
+
type: meta.contract_type === "call" ? 0 : 1,
|
|
262
|
+
riskFreeRate,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
193
265
|
|
|
194
|
-
|
|
195
|
-
|
|
266
|
+
function writeComputedGreeks<T extends QuoteGreekFields>(
|
|
267
|
+
row: T,
|
|
268
|
+
greeks: { delta: number; gamma: number; theta: number; vega: number; iv: number },
|
|
269
|
+
riskFreeRate: number,
|
|
270
|
+
): void {
|
|
271
|
+
row.delta = greeks.delta;
|
|
272
|
+
row.gamma = greeks.gamma;
|
|
273
|
+
row.theta = greeks.theta;
|
|
274
|
+
row.vega = greeks.vega;
|
|
275
|
+
row.iv = greeks.iv;
|
|
276
|
+
row.greeks_source = "computed";
|
|
277
|
+
row.greeks_revision = OPTION_QUOTE_GREEKS_REVISION;
|
|
278
|
+
row.rate_type = OPTION_QUOTE_GREEKS_RATE_TYPE;
|
|
279
|
+
row.rate_value = riskFreeRate;
|
|
280
|
+
row.gamma_source = OPTION_QUOTE_GREEKS_GAMMA_SOURCE;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function tallySkip(stats: QuoteGreeksStats, kind: RowResolution["kind"]): void {
|
|
284
|
+
switch (kind) {
|
|
285
|
+
case "existing":
|
|
286
|
+
stats.existingGreeksRows++;
|
|
287
|
+
return;
|
|
288
|
+
case "missingContract":
|
|
196
289
|
stats.missingContractRows++;
|
|
197
290
|
stats.unresolvedRows++;
|
|
291
|
+
return;
|
|
292
|
+
case "providerUnresolved":
|
|
293
|
+
stats.unresolvedRows++;
|
|
294
|
+
return;
|
|
295
|
+
case "missingUnderlying":
|
|
296
|
+
stats.missingUnderlyingRows++;
|
|
297
|
+
stats.unresolvedRows++;
|
|
298
|
+
return;
|
|
299
|
+
case "mathFailed":
|
|
300
|
+
stats.mathFailedRows++;
|
|
301
|
+
stats.unresolvedRows++;
|
|
302
|
+
return;
|
|
303
|
+
case "compute":
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function applyQuoteGreeks<T extends QuoteGreekFields>(
|
|
309
|
+
params: ApplyQuoteGreeksParams<T>,
|
|
310
|
+
): QuoteGreeksStats {
|
|
311
|
+
const { rows, mode = "auto", defaultProviderSource } = params;
|
|
312
|
+
const stats = emptyStats();
|
|
313
|
+
|
|
314
|
+
for (const row of rows) {
|
|
315
|
+
stats.rowsVisited++;
|
|
316
|
+
const resolution = resolveQuoteGreeksRow(row, params, defaultProviderSource, mode);
|
|
317
|
+
if (resolution.kind !== "compute") {
|
|
318
|
+
tallySkip(stats, resolution.kind);
|
|
198
319
|
continue;
|
|
199
320
|
}
|
|
200
321
|
|
|
201
|
-
|
|
322
|
+
const result = computeLegGreeks(
|
|
323
|
+
resolution.optionPrice,
|
|
324
|
+
resolution.underlyingPrice,
|
|
325
|
+
resolution.strike,
|
|
326
|
+
resolution.dte,
|
|
327
|
+
resolution.type === 0 ? "C" : "P",
|
|
328
|
+
resolution.riskFreeRate,
|
|
329
|
+
OPTION_QUOTE_GREEKS_DIVIDEND_YIELD,
|
|
330
|
+
);
|
|
331
|
+
if (!hasQuoteGreeks(result)) {
|
|
332
|
+
stats.mathFailedRows++;
|
|
202
333
|
stats.unresolvedRows++;
|
|
203
334
|
continue;
|
|
204
335
|
}
|
|
336
|
+
writeComputedGreeks(
|
|
337
|
+
row,
|
|
338
|
+
{
|
|
339
|
+
delta: result.delta as number,
|
|
340
|
+
gamma: result.gamma as number,
|
|
341
|
+
theta: result.theta as number,
|
|
342
|
+
vega: result.vega as number,
|
|
343
|
+
iv: result.iv as number,
|
|
344
|
+
},
|
|
345
|
+
resolution.riskFreeRate,
|
|
346
|
+
);
|
|
347
|
+
stats.computedRows++;
|
|
348
|
+
}
|
|
205
349
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
350
|
+
return stats;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Parallel sibling of `applyQuoteGreeks`. Resolves every row on the calling
|
|
355
|
+
* thread using the exact same `resolveQuoteGreeksRow` logic, then fans the
|
|
356
|
+
* CPU-bound IV solve out across a worker pool. Greeks and provenance written
|
|
357
|
+
* back are bit-identical to the inline path (the pool runs the same
|
|
358
|
+
* `computeLegGreeks`); only the location of the solve loop changes.
|
|
359
|
+
*
|
|
360
|
+
* The pool degrades to inline automatically for small batches / single-core
|
|
361
|
+
* hosts (see iv-solver-pool.ts), so this is safe to call unconditionally.
|
|
362
|
+
*/
|
|
363
|
+
export async function applyQuoteGreeksParallel<T extends QuoteGreekFields>(
|
|
364
|
+
params: ApplyQuoteGreeksParams<T> & { pool?: IvSolverPool },
|
|
365
|
+
): Promise<QuoteGreeksStats> {
|
|
366
|
+
const { rows, mode = "auto", defaultProviderSource } = params;
|
|
367
|
+
const stats = emptyStats();
|
|
368
|
+
const pool = params.pool ?? getSharedIvSolverPool();
|
|
369
|
+
const n = rows.length;
|
|
370
|
+
|
|
371
|
+
// Resolve directly into the solve columns — no intermediate object array.
|
|
372
|
+
// Sized to the row count (the compute subset is <= n); the unused tail is
|
|
373
|
+
// dropped via `count` before dispatch.
|
|
374
|
+
const optionPrice = new Float64Array(n);
|
|
375
|
+
const underlyingPrice = new Float64Array(n);
|
|
376
|
+
const strike = new Float64Array(n);
|
|
377
|
+
const dte = new Float64Array(n);
|
|
378
|
+
const riskFreeRate = new Float64Array(n);
|
|
379
|
+
const dividendYield = new Float64Array(n);
|
|
380
|
+
const type = new Uint8Array(n);
|
|
381
|
+
// Maps the j-th compute job back to its source row index, so results land on
|
|
382
|
+
// the right row.
|
|
383
|
+
const jobRowIndex = new Int32Array(n);
|
|
384
|
+
|
|
385
|
+
let count = 0;
|
|
386
|
+
for (let i = 0; i < n; i++) {
|
|
387
|
+
stats.rowsVisited++;
|
|
388
|
+
const resolution = resolveQuoteGreeksRow(rows[i], params, defaultProviderSource, mode);
|
|
389
|
+
if (resolution.kind !== "compute") {
|
|
390
|
+
tallySkip(stats, resolution.kind);
|
|
212
391
|
continue;
|
|
213
392
|
}
|
|
393
|
+
optionPrice[count] = resolution.optionPrice;
|
|
394
|
+
underlyingPrice[count] = resolution.underlyingPrice;
|
|
395
|
+
strike[count] = resolution.strike;
|
|
396
|
+
dte[count] = resolution.dte;
|
|
397
|
+
riskFreeRate[count] = resolution.riskFreeRate;
|
|
398
|
+
dividendYield[count] = OPTION_QUOTE_GREEKS_DIVIDEND_YIELD;
|
|
399
|
+
type[count] = resolution.type;
|
|
400
|
+
jobRowIndex[count] = i;
|
|
401
|
+
count++;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (count === 0) return stats;
|
|
214
405
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
406
|
+
const columns: IvSolveColumns = {
|
|
407
|
+
count,
|
|
408
|
+
optionPrice: optionPrice.subarray(0, count),
|
|
409
|
+
underlyingPrice: underlyingPrice.subarray(0, count),
|
|
410
|
+
strike: strike.subarray(0, count),
|
|
411
|
+
dte: dte.subarray(0, count),
|
|
412
|
+
riskFreeRate: riskFreeRate.subarray(0, count),
|
|
413
|
+
dividendYield: dividendYield.subarray(0, count),
|
|
414
|
+
type: type.subarray(0, count),
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
const result = await pool.solveColumns(columns);
|
|
418
|
+
for (let j = 0; j < count; j++) {
|
|
419
|
+
if (result.ok[j] !== 1) {
|
|
225
420
|
stats.mathFailedRows++;
|
|
226
421
|
stats.unresolvedRows++;
|
|
227
422
|
continue;
|
|
228
423
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
424
|
+
writeComputedGreeks(
|
|
425
|
+
rows[jobRowIndex[j]],
|
|
426
|
+
{
|
|
427
|
+
delta: result.delta[j],
|
|
428
|
+
gamma: result.gamma[j],
|
|
429
|
+
theta: result.theta[j],
|
|
430
|
+
vega: result.vega[j],
|
|
431
|
+
iv: result.iv[j],
|
|
432
|
+
},
|
|
433
|
+
riskFreeRate[j],
|
|
434
|
+
);
|
|
240
435
|
stats.computedRows++;
|
|
241
436
|
}
|
|
242
437
|
|
|
@@ -38,6 +38,21 @@ const PRICE_TYPE_FACTORS = [
|
|
|
38
38
|
|
|
39
39
|
const ET_MINUTE_PATTERN = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/;
|
|
40
40
|
|
|
41
|
+
// Constructing an Intl.DateTimeFormat is expensive (it builds an ICU formatter)
|
|
42
|
+
// and the options are constant. Build it once at module load instead of per
|
|
43
|
+
// call — this function runs once per decoded quote row, so a per-row construct
|
|
44
|
+
// dominated the decode cost on dense chains.
|
|
45
|
+
const ET_MINUTE_FORMAT = new Intl.DateTimeFormat("en-CA", {
|
|
46
|
+
timeZone: "America/New_York",
|
|
47
|
+
year: "numeric",
|
|
48
|
+
month: "2-digit",
|
|
49
|
+
day: "2-digit",
|
|
50
|
+
hour: "2-digit",
|
|
51
|
+
minute: "2-digit",
|
|
52
|
+
hour12: false,
|
|
53
|
+
hourCycle: "h23",
|
|
54
|
+
});
|
|
55
|
+
|
|
41
56
|
export function thetaPriceToNumber(price: ThetaPriceLike): number {
|
|
42
57
|
return price.type === 0 ? Number.NaN : price.value * PRICE_TYPE_FACTORS[price.type];
|
|
43
58
|
}
|
|
@@ -45,16 +60,7 @@ export function thetaPriceToNumber(price: ThetaPriceLike): number {
|
|
|
45
60
|
export function thetaTimestampToEtMinute(value: string | number | Date): string {
|
|
46
61
|
if (typeof value === "string" && ET_MINUTE_PATTERN.test(value)) return value;
|
|
47
62
|
const date = value instanceof Date ? value : new Date(value);
|
|
48
|
-
const parts =
|
|
49
|
-
timeZone: "America/New_York",
|
|
50
|
-
year: "numeric",
|
|
51
|
-
month: "2-digit",
|
|
52
|
-
day: "2-digit",
|
|
53
|
-
hour: "2-digit",
|
|
54
|
-
minute: "2-digit",
|
|
55
|
-
hour12: false,
|
|
56
|
-
hourCycle: "h23",
|
|
57
|
-
}).formatToParts(date);
|
|
63
|
+
const parts = ET_MINUTE_FORMAT.formatToParts(date);
|
|
58
64
|
const get = (type: string) => parts.find((part) => part.type === type)?.value ?? "";
|
|
59
65
|
return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}`;
|
|
60
66
|
}
|