@huskly/ibkr-client 1.0.0 → 1.1.0
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 +22 -11
- package/dist/ibkr/ibkrClient.d.ts +11 -3
- package/dist/ibkr/ibkrClient.d.ts.map +1 -1
- package/dist/ibkr/ibkrClient.js +233 -73
- package/dist/ibkr/ibkrClient.js.map +1 -1
- package/dist/ibkr/requestScheduler.d.ts +8 -0
- package/dist/ibkr/requestScheduler.d.ts.map +1 -1
- package/dist/ibkr/requestScheduler.js +33 -6
- package/dist/ibkr/requestScheduler.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -129,9 +129,16 @@ interval. Authentication, entitlement, invalid contract, and ambiguous contract
|
|
|
129
129
|
recovery.
|
|
130
130
|
|
|
131
131
|
- `getOptionExpiries(...)` discovers weekly and monthly maturities across month buckets.
|
|
132
|
-
- `getOptionChain(
|
|
133
|
-
bid
|
|
134
|
-
|
|
132
|
+
- `getOptionChain(symbol, expiry, right?)` is the strict strategy-ready path. It returns only
|
|
133
|
+
exact-expiry contracts that have bid, ask, and delta values. It fails if no contract has all three
|
|
134
|
+
values. Set `right` to `C` or `P` to skip definition and quote requests for the unused side.
|
|
135
|
+
- `getOptionChainSnapshot(symbol, expiry, right)` returns every qualified contract for one exact
|
|
136
|
+
expiry and option side. It preserves canonical OSI symbols and conids. Missing bid, ask, mid,
|
|
137
|
+
delta, volume, open interest, availability, and timestamp values are `null`. The result includes
|
|
138
|
+
qualified, returned, malformed-definition, and missing-field counts. These diagnostics contain no
|
|
139
|
+
account IDs or credentials. If a warmed snapshot stays sparse, the method returns the fields that
|
|
140
|
+
IBKR supplied and keeps the other fields as `null`.
|
|
141
|
+
- `getOptionQuote(...)` resolves and prices one exact contract with the strict market-data shape.
|
|
135
142
|
It serializes the session search with one exact security-definition request. It caches an
|
|
136
143
|
identical exact request and does not load the complete option chain. When an exact ticker has listings in more than one market, option
|
|
137
144
|
discovery selects the one listing with `SMART` option routing. It rejects the result if `SMART`
|
|
@@ -306,12 +313,14 @@ structured response in `BrokerErrorDetail.details`; transport exceptions are ret
|
|
|
306
313
|
### Request pacing and temporary blocks
|
|
307
314
|
|
|
308
315
|
Every authenticated request runs through one priority scheduler per `IbkrClient`. The default
|
|
309
|
-
limits allow at most ten requests globally
|
|
310
|
-
time.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
316
|
+
limits allow at most ten requests globally. Stateful security-definition discovery stays at one
|
|
317
|
+
request at a time. Read-only `iserver/secdef/info` expansion uses a separate conservative limit of
|
|
318
|
+
four concurrent requests. Configure it with `maxSecdefInfoConcurrent`; it cannot exceed the global
|
|
319
|
+
limit. Order preview, status, warning, cancellation, and immediate-trade requests take priority over
|
|
320
|
+
queued discovery. Multi-month derivative discovery also primes each month serially. A session-level
|
|
321
|
+
transaction guard keeps each stateful security-definition search with its dependent strike request.
|
|
322
|
+
For option-chain discovery, independent definition reads run outside this guard after search and
|
|
323
|
+
strikes finish. An exact expiry/strike/right request expands only the requested contracts.
|
|
315
324
|
|
|
316
325
|
A 429 pauses the shared queue behind one `Retry-After`-aware exponential backoff with jitter.
|
|
317
326
|
Individual queued reads do not start independent retry loops. Exhausted throttling throws
|
|
@@ -330,7 +339,9 @@ safe `Retry-After` value when it is available. Callers do not have to parse the
|
|
|
330
339
|
|
|
331
340
|
`IbkrClient` accepts optional scheduler limits and an `onRequestTelemetry` callback. Scheduler
|
|
332
341
|
options also accept `now`, `sleep`, and `random` functions for controlled runtimes and tests.
|
|
333
|
-
Telemetry for each retry contains only a sanitized endpoint category, event, attempt, and delay.
|
|
342
|
+
Telemetry for each retry contains only a sanitized endpoint category, event, attempt, and delay.
|
|
343
|
+
`onOptionDiscoveryTelemetry` reports search, strikes, definitions, and snapshot phases. It includes
|
|
344
|
+
the symbol, month, optional right, duration, definition request count, and snapshot batch count. It
|
|
334
345
|
does not contain account IDs, order IDs, credentials, request payloads, or full URLs. A telemetry
|
|
335
346
|
observer failure does not change request scheduling or request settlement.
|
|
336
347
|
|
|
@@ -362,7 +373,7 @@ const [balances, history, expiries] = await Promise.all([
|
|
|
362
373
|
]);
|
|
363
374
|
const expiry = expiries[0];
|
|
364
375
|
if (!expiry) throw new Error(`No listed expiries for ${symbol} in ${from}..${to}`);
|
|
365
|
-
const chain = await client.getOptionChain(symbol, expiry);
|
|
376
|
+
const chain = await client.getOptionChain(symbol, expiry, "C");
|
|
366
377
|
console.log({ equityRead: Number.isFinite(balances.netLiquidation), historyBars: history.bars.length,
|
|
367
378
|
expiry, contracts: chain.length, first: chain[0]?.symbol });
|
|
368
379
|
NODE
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IbkrOauth1Config } from "./oauthConfig.js";
|
|
2
|
-
import type { AccountBalances, ActiveDerivativeOrder, AuthStatus, BrokerAccountOrders, BrokerClient, BrokerInstrument, BrokerInstrumentSearchProjection, BrokerOrdersOptions, BrokerPosition, BrokerQuote, BrokerQuoteOptions, BrokerQuoteRequest, BrokerTransactionHistory, BrokerErrorDetail, DerivativeContract, DerivativeContractQuery, DerivativeComboExecutionRequest, DerivativeComboReconciliation, DerivativeComboReconciliationRequest, DerivativeComboPreviewRequest, DerivativeComboPreviewResult, DerivativeContingentChildOrderRequest, DerivativeContingentParentOrderRequest, DerivativeDiscoveryClient, DerivativeExecutionClient, DerivativeExecution, DerivativeExecutionQuery, DerivativeMultiOrderResult, DerivativeOrderGraphLookup, DerivativeOrderGraphRequest, DerivativeOrderGraphResult, DerivativeOrderGraphWarningContinuation, DerivativeOrderCancellationResult, DerivativeOrderCancelRequest, DerivativeOrderLifecycle, DerivativeOrderLookup, DerivativeOrderSubmissionResult, DerivativeSingleOrderRequest, DerivativePreviewClient, DerivativeExpiry, DerivativeExpiryQuery, DerivativeQuote, DerivativeReferenceQuote, OptionContract, OptionMarketQuote, OptionQuoteRequest, OptionRight, PriceHistoryContractCandidate, PriceHistoryRequest, PriceHistoryResult, PriceHistoryTelemetry, TradingDiagnostics } from "../types.js";
|
|
2
|
+
import type { AccountBalances, ActiveDerivativeOrder, AuthStatus, BrokerAccountOrders, BrokerClient, BrokerInstrument, BrokerInstrumentSearchProjection, BrokerOrdersOptions, BrokerPosition, BrokerQuote, BrokerQuoteOptions, BrokerQuoteRequest, BrokerTransactionHistory, BrokerErrorDetail, DerivativeContract, DerivativeContractQuery, DerivativeComboExecutionRequest, DerivativeComboReconciliation, DerivativeComboReconciliationRequest, DerivativeComboPreviewRequest, DerivativeComboPreviewResult, DerivativeContingentChildOrderRequest, DerivativeContingentParentOrderRequest, DerivativeDiscoveryClient, DerivativeExecutionClient, DerivativeExecution, DerivativeExecutionQuery, DerivativeMultiOrderResult, DerivativeOrderGraphLookup, DerivativeOrderGraphRequest, DerivativeOrderGraphResult, DerivativeOrderGraphWarningContinuation, DerivativeOrderCancellationResult, DerivativeOrderCancelRequest, DerivativeOrderLifecycle, DerivativeOrderLookup, DerivativeOrderSubmissionResult, DerivativeSingleOrderRequest, DerivativePreviewClient, DerivativeExpiry, DerivativeExpiryQuery, DerivativeQuote, DerivativeReferenceQuote, OptionChainSnapshot, OptionContract, OptionDiscoveryTelemetry, OptionMarketQuote, OptionQuoteRequest, OptionRight, PriceHistoryContractCandidate, PriceHistoryRequest, PriceHistoryResult, PriceHistoryTelemetry, TradingDiagnostics } from "../types.js";
|
|
3
3
|
import { type IbkrRequestSchedulerOptions, type IbkrRequestTelemetry } from "./requestScheduler.js";
|
|
4
4
|
interface IbkrRequestInput {
|
|
5
5
|
path: string;
|
|
@@ -12,6 +12,8 @@ export interface IbkrClientOptions {
|
|
|
12
12
|
onRequestTelemetry?: (event: IbkrRequestTelemetry) => void;
|
|
13
13
|
/** Receive safe contract and request metadata before each price-history request. */
|
|
14
14
|
onPriceHistoryTelemetry?: (event: PriceHistoryTelemetry) => void;
|
|
15
|
+
/** Receive safe timing and request counts for option-discovery phases. */
|
|
16
|
+
onOptionDiscoveryTelemetry?: (event: OptionDiscoveryTelemetry) => void;
|
|
15
17
|
}
|
|
16
18
|
/** Safe HTTP response evidence retained when the raw transport rejects a request. */
|
|
17
19
|
export interface IbkrHttpErrorResponse {
|
|
@@ -67,6 +69,7 @@ export declare class IbkrClient implements BrokerClient, DerivativeDiscoveryClie
|
|
|
67
69
|
private readonly requestNow;
|
|
68
70
|
private secdefPrimingTail;
|
|
69
71
|
private readonly onPriceHistoryTelemetry;
|
|
72
|
+
private readonly onOptionDiscoveryTelemetry;
|
|
70
73
|
private readonly onRequestTelemetry;
|
|
71
74
|
constructor(config: IbkrOauth1Config, options?: IbkrClientOptions);
|
|
72
75
|
/** Obtain the live session token (idempotent — safe to await repeatedly). */
|
|
@@ -243,7 +246,9 @@ export declare class IbkrClient implements BrokerClient, DerivativeDiscoveryClie
|
|
|
243
246
|
/** Discover every listed weekly/monthly expiry in the requested calendar range. */
|
|
244
247
|
getOptionExpiries(symbol: string, right: OptionRight, fromDate: string, toDate: string): Promise<string[]>;
|
|
245
248
|
/** Build one exact-expiry chain with canonical OSI symbols and required pricing/greeks. */
|
|
246
|
-
getOptionChain(symbol: string, expiry: string): Promise<OptionMarketQuote[]>;
|
|
249
|
+
getOptionChain(symbol: string, expiry: string, right?: OptionRight): Promise<OptionMarketQuote[]>;
|
|
250
|
+
/** Return every qualified contract for one exact expiry and side without hiding sparse data. */
|
|
251
|
+
getOptionChainSnapshot(symbol: string, expiry: string, right: OptionRight): Promise<OptionChainSnapshot>;
|
|
247
252
|
/** Fetch one exact option quote; null means the contract is not listed. */
|
|
248
253
|
getOptionQuote(input: OptionQuoteRequest): Promise<OptionMarketQuote | null>;
|
|
249
254
|
/** Resolve a conid back into the canonical OSI-bearing option contract. */
|
|
@@ -259,9 +264,12 @@ export declare class IbkrClient implements BrokerClient, DerivativeDiscoveryClie
|
|
|
259
264
|
private discoverOptions;
|
|
260
265
|
private loadOptionUnderlying;
|
|
261
266
|
private loadOptionContracts;
|
|
262
|
-
private
|
|
267
|
+
private fetchOptionChainSnapshot;
|
|
268
|
+
private fetchNullableOptionQuotes;
|
|
263
269
|
private fetchOptionQuotes;
|
|
264
270
|
private historyInterval;
|
|
271
|
+
private elapsedSince;
|
|
272
|
+
private emitOptionDiscoveryTelemetry;
|
|
265
273
|
private requestPriceHistory;
|
|
266
274
|
private recoverDailyHistory;
|
|
267
275
|
private dailyHistoryWindows;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ibkrClient.d.ts","sourceRoot":"","sources":["../../src/ibkr/ibkrClient.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EACV,eAAe,EACf,qBAAqB,EAGrB,UAAU,EACV,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,gCAAgC,EAGhC,mBAAmB,EACnB,cAAc,EACd,WAAW,EACX,kBAAkB,EAClB,kBAAkB,EAElB,wBAAwB,EACxB,iBAAiB,EAEjB,kBAAkB,EAClB,uBAAuB,EACvB,+BAA+B,EAC/B,6BAA6B,EAC7B,oCAAoC,EACpC,6BAA6B,EAC7B,4BAA4B,EAC5B,qCAAqC,EAErC,sCAAsC,EACtC,yBAAyB,EACzB,yBAAyB,EACzB,mBAAmB,EACnB,wBAAwB,EAGxB,0BAA0B,EAE1B,0BAA0B,EAE1B,2BAA2B,EAC3B,0BAA0B,EAC1B,uCAAuC,EACvC,iCAAiC,EACjC,4BAA4B,EAC5B,wBAAwB,EACxB,qBAAqB,EAErB,+BAA+B,EAC/B,4BAA4B,EAE5B,uBAAuB,EACvB,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EAIX,6BAA6B,EAC7B,mBAAmB,EAEnB,kBAAkB,EAClB,qBAAqB,EACrB,kBAAkB,EACnB,MAAM,aAAa,CAAC;AAmCrB,OAAO,EAIL,KAAK,2BAA2B,EAChC,KAAK,oBAAoB,EAC1B,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"ibkrClient.d.ts","sourceRoot":"","sources":["../../src/ibkr/ibkrClient.ts"],"names":[],"mappings":"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"}
|
package/dist/ibkr/ibkrClient.js
CHANGED
|
@@ -259,10 +259,12 @@ export class IbkrClient {
|
|
|
259
259
|
requestNow;
|
|
260
260
|
secdefPrimingTail = Promise.resolve();
|
|
261
261
|
onPriceHistoryTelemetry;
|
|
262
|
+
onOptionDiscoveryTelemetry;
|
|
262
263
|
onRequestTelemetry;
|
|
263
264
|
constructor(config, options = {}) {
|
|
264
265
|
this.raw = new RawIbkrClientCtor(config);
|
|
265
266
|
this.onPriceHistoryTelemetry = options.onPriceHistoryTelemetry ?? (() => undefined);
|
|
267
|
+
this.onOptionDiscoveryTelemetry = options.onOptionDiscoveryTelemetry ?? (() => undefined);
|
|
266
268
|
this.onRequestTelemetry = options.onRequestTelemetry ?? (() => undefined);
|
|
267
269
|
const schedulerOptions = options.requestScheduler;
|
|
268
270
|
this.requestNow = schedulerOptions?.now ?? (() => this.now());
|
|
@@ -3283,8 +3285,8 @@ export class IbkrClient {
|
|
|
3283
3285
|
const contracts = [];
|
|
3284
3286
|
for (let index = 0; index < months.length; index += OPTION_DISCOVERY_MONTH_CONCURRENCY) {
|
|
3285
3287
|
const batch = months.slice(index, index + OPTION_DISCOVERY_MONTH_CONCURRENCY);
|
|
3286
|
-
const batchContracts =
|
|
3287
|
-
contracts.push(...batchContracts);
|
|
3288
|
+
const batchContracts = await Promise.all(batch.map((month) => this.discoverOptions(normalized, month, right)));
|
|
3289
|
+
contracts.push(...batchContracts.flatMap((result) => result.contracts));
|
|
3288
3290
|
}
|
|
3289
3291
|
return [
|
|
3290
3292
|
...new Set(contracts
|
|
@@ -3293,17 +3295,38 @@ export class IbkrClient {
|
|
|
3293
3295
|
].sort();
|
|
3294
3296
|
}
|
|
3295
3297
|
/** Build one exact-expiry chain with canonical OSI symbols and required pricing/greeks. */
|
|
3296
|
-
async getOptionChain(symbol, expiry) {
|
|
3297
|
-
const
|
|
3298
|
+
async getOptionChain(symbol, expiry, right) {
|
|
3299
|
+
const month = monthCode(expiry);
|
|
3300
|
+
const normalized = symbol.trim().toUpperCase();
|
|
3301
|
+
const discovery = await this.discoverOptions(normalized, month, right);
|
|
3302
|
+
const contracts = discovery.contracts.filter((contract) => contract.expiry === expiry && (right === undefined || contract.right === right));
|
|
3298
3303
|
if (!contracts.length) {
|
|
3299
3304
|
throw new Error(`IBKR returned no option contracts for ${symbol} ${expiry}`);
|
|
3300
3305
|
}
|
|
3301
|
-
const quoted = await this.fetchOptionQuotes(contracts, {
|
|
3306
|
+
const quoted = await this.fetchOptionQuotes(contracts, {
|
|
3307
|
+
allowIncomplete: true,
|
|
3308
|
+
telemetry: { symbol: normalized, month, right: right ?? null },
|
|
3309
|
+
});
|
|
3302
3310
|
if (!quoted.length) {
|
|
3303
3311
|
throw new Error(`IBKR returned no usable option quotes for ${symbol} ${expiry}`);
|
|
3304
3312
|
}
|
|
3305
3313
|
return quoted;
|
|
3306
3314
|
}
|
|
3315
|
+
/** Return every qualified contract for one exact expiry and side without hiding sparse data. */
|
|
3316
|
+
async getOptionChainSnapshot(symbol, expiry, right) {
|
|
3317
|
+
const month = monthCode(expiry);
|
|
3318
|
+
const normalized = symbol.trim().toUpperCase();
|
|
3319
|
+
const discovery = await this.discoverOptions(normalized, month, right);
|
|
3320
|
+
const contracts = discovery.contracts.filter((contract) => contract.expiry === expiry);
|
|
3321
|
+
if (!contracts.length) {
|
|
3322
|
+
throw new Error(`IBKR returned no ${right} option contracts for ${symbol} ${expiry}`);
|
|
3323
|
+
}
|
|
3324
|
+
return this.fetchOptionChainSnapshot(contracts, discovery.malformedDefinitionCount, {
|
|
3325
|
+
symbol: normalized,
|
|
3326
|
+
month,
|
|
3327
|
+
right,
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
3307
3330
|
/** Fetch one exact option quote; null means the contract is not listed. */
|
|
3308
3331
|
async getOptionQuote(input) {
|
|
3309
3332
|
const contract = await this.resolveOptionContract(input);
|
|
@@ -3556,14 +3579,21 @@ export class IbkrClient {
|
|
|
3556
3579
|
}
|
|
3557
3580
|
return result;
|
|
3558
3581
|
}
|
|
3559
|
-
discoverOptions(symbol, month) {
|
|
3582
|
+
discoverOptions(symbol, month, right) {
|
|
3560
3583
|
const normalized = symbol.trim().toUpperCase();
|
|
3561
|
-
const key = `${normalized}:${month}`;
|
|
3584
|
+
const key = `${normalized}:${month}:${right ?? "*"}`;
|
|
3562
3585
|
let pending = this.optionDiscovery.get(key);
|
|
3563
|
-
if (!pending) {
|
|
3564
|
-
|
|
3565
|
-
|
|
3586
|
+
if (!pending && right !== undefined) {
|
|
3587
|
+
const complete = this.optionDiscovery.get(`${normalized}:${month}:*`);
|
|
3588
|
+
if (complete !== undefined) {
|
|
3589
|
+
pending = complete.then((result) => ({
|
|
3590
|
+
contracts: result.contracts.filter((contract) => contract.right === right),
|
|
3591
|
+
malformedDefinitionCount: result.malformedDefinitionCount,
|
|
3592
|
+
}));
|
|
3593
|
+
}
|
|
3566
3594
|
}
|
|
3595
|
+
pending ??= this.loadOptionContracts(normalized, month, right);
|
|
3596
|
+
this.optionDiscovery.set(key, pending);
|
|
3567
3597
|
return pending;
|
|
3568
3598
|
}
|
|
3569
3599
|
async loadOptionUnderlying(symbol) {
|
|
@@ -3611,97 +3641,211 @@ export class IbkrClient {
|
|
|
3611
3641
|
throw new Error(`IBKR lost the selected underlying for ${symbol}`);
|
|
3612
3642
|
return underlying;
|
|
3613
3643
|
}
|
|
3614
|
-
loadOptionContracts(symbol, month) {
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
|
|
3621
|
-
|
|
3644
|
+
async loadOptionContracts(symbol, month, right) {
|
|
3645
|
+
const { underlying, requests } = await this.withSecdefPriming(async () => {
|
|
3646
|
+
const searchStarted = this.requestNow();
|
|
3647
|
+
const selectedUnderlying = await this.loadOptionUnderlying(symbol);
|
|
3648
|
+
this.emitOptionDiscoveryTelemetry({
|
|
3649
|
+
phase: "SEARCH",
|
|
3650
|
+
symbol,
|
|
3651
|
+
month,
|
|
3652
|
+
right: right ?? null,
|
|
3653
|
+
durationMs: this.elapsedSince(searchStarted),
|
|
3654
|
+
definitionRequestCount: 0,
|
|
3655
|
+
snapshotBatchCount: 0,
|
|
3656
|
+
});
|
|
3657
|
+
const strikesStarted = this.requestNow();
|
|
3658
|
+
const strikes = await this.req({
|
|
3659
|
+
path: "iserver/secdef/strikes",
|
|
3660
|
+
params: { conid: String(selectedUnderlying.conid), sectype: "OPT", month },
|
|
3661
|
+
});
|
|
3662
|
+
this.emitOptionDiscoveryTelemetry({
|
|
3663
|
+
phase: "STRIKES",
|
|
3664
|
+
symbol,
|
|
3665
|
+
month,
|
|
3666
|
+
right: right ?? null,
|
|
3667
|
+
durationMs: this.elapsedSince(strikesStarted),
|
|
3668
|
+
definitionRequestCount: 0,
|
|
3669
|
+
snapshotBatchCount: 0,
|
|
3670
|
+
});
|
|
3671
|
+
const callStrikes = strikes.call ?? [];
|
|
3672
|
+
const putStrikes = strikes.put ?? [];
|
|
3673
|
+
if (callStrikes.length === 0 && putStrikes.length === 0) {
|
|
3674
|
+
throw new Error(`IBKR returned empty option strikes for ${symbol} ${month} after secdef/search priming`);
|
|
3675
|
+
}
|
|
3676
|
+
const definitionRequests = [
|
|
3677
|
+
...(right === undefined || right === "C"
|
|
3678
|
+
? callStrikes.map((strike) => ({ strike, right: "C" }))
|
|
3679
|
+
: []),
|
|
3680
|
+
...(right === undefined || right === "P"
|
|
3681
|
+
? putStrikes.map((strike) => ({ strike, right: "P" }))
|
|
3682
|
+
: []),
|
|
3683
|
+
];
|
|
3684
|
+
return { underlying: selectedUnderlying, requests: definitionRequests };
|
|
3622
3685
|
});
|
|
3623
|
-
const
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3686
|
+
const definitionsStarted = this.requestNow();
|
|
3687
|
+
const responses = await Promise.all(requests.map(({ strike, right: requestRight }) => this.req({
|
|
3688
|
+
path: "iserver/secdef/info",
|
|
3689
|
+
params: {
|
|
3690
|
+
conid: String(underlying.conid),
|
|
3691
|
+
sectype: "OPT",
|
|
3692
|
+
month,
|
|
3693
|
+
strike,
|
|
3694
|
+
right: requestRight,
|
|
3695
|
+
},
|
|
3696
|
+
})));
|
|
3697
|
+
this.emitOptionDiscoveryTelemetry({
|
|
3698
|
+
phase: "DEFINITIONS",
|
|
3699
|
+
symbol,
|
|
3700
|
+
month,
|
|
3701
|
+
right: right ?? null,
|
|
3702
|
+
durationMs: this.elapsedSince(definitionsStarted),
|
|
3703
|
+
definitionRequestCount: requests.length,
|
|
3704
|
+
snapshotBatchCount: 0,
|
|
3705
|
+
});
|
|
3706
|
+
if (requests.length === 0)
|
|
3707
|
+
return { contracts: [], malformedDefinitionCount: 0 };
|
|
3630
3708
|
const contracts = [];
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
}
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
3709
|
+
let malformedDefinitionCount = 0;
|
|
3710
|
+
for (const response of responses) {
|
|
3711
|
+
if (!Array.isArray(response)) {
|
|
3712
|
+
throw new Error(`IBKR returned malformed option definitions for ${symbol} ${month}`);
|
|
3713
|
+
}
|
|
3714
|
+
for (const raw of response) {
|
|
3715
|
+
if (!isUnknownRecord(raw)) {
|
|
3716
|
+
malformedDefinitionCount += 1;
|
|
3717
|
+
continue;
|
|
3718
|
+
}
|
|
3719
|
+
let contract;
|
|
3720
|
+
try {
|
|
3721
|
+
contract = normalizeOptionContract({
|
|
3722
|
+
conid: typeof raw["conid"] === "number" ? raw["conid"] : undefined,
|
|
3723
|
+
symbol: typeof raw["symbol"] === "string" ? raw["symbol"] : underlying.symbol,
|
|
3724
|
+
maturityDate: typeof raw["maturityDate"] === "string" ? raw["maturityDate"] : undefined,
|
|
3725
|
+
right: typeof raw["right"] === "string" ? raw["right"] : undefined,
|
|
3726
|
+
strike: typeof raw["strike"] === "string" || typeof raw["strike"] === "number"
|
|
3727
|
+
? raw["strike"]
|
|
3728
|
+
: undefined,
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
catch {
|
|
3732
|
+
malformedDefinitionCount += 1;
|
|
3733
|
+
continue;
|
|
3734
|
+
}
|
|
3650
3735
|
if (contract)
|
|
3651
3736
|
contracts.push(contract);
|
|
3737
|
+
else
|
|
3738
|
+
malformedDefinitionCount += 1;
|
|
3652
3739
|
}
|
|
3653
3740
|
}
|
|
3654
3741
|
const unique = [...new Map(contracts.map((contract) => [contract.conid, contract])).values()];
|
|
3655
3742
|
if (!unique.length) {
|
|
3656
|
-
throw new Error(`IBKR returned no usable option definitions for ${symbol} ${month}`);
|
|
3743
|
+
throw new Error(`IBKR returned no usable option definitions for ${symbol} ${month} (${String(malformedDefinitionCount)} malformed)`);
|
|
3744
|
+
}
|
|
3745
|
+
return { contracts: unique, malformedDefinitionCount };
|
|
3746
|
+
}
|
|
3747
|
+
async fetchOptionChainSnapshot(contracts, malformedDefinitionCount, telemetry) {
|
|
3748
|
+
const fields = [
|
|
3749
|
+
"bid",
|
|
3750
|
+
"ask",
|
|
3751
|
+
"mid",
|
|
3752
|
+
"delta",
|
|
3753
|
+
"volume",
|
|
3754
|
+
"openInterest",
|
|
3755
|
+
"availability",
|
|
3756
|
+
"timestamp",
|
|
3757
|
+
];
|
|
3758
|
+
const missingFieldCounts = Object.fromEntries(fields.map((field) => [field, 0]));
|
|
3759
|
+
const quotes = await this.fetchNullableOptionQuotes(contracts, telemetry);
|
|
3760
|
+
for (const quote of quotes) {
|
|
3761
|
+
for (const field of fields) {
|
|
3762
|
+
if (quote[field] === null)
|
|
3763
|
+
missingFieldCounts[field] += 1;
|
|
3764
|
+
}
|
|
3657
3765
|
}
|
|
3658
|
-
|
|
3766
|
+
const diagnostics = {
|
|
3767
|
+
qualifiedCount: contracts.length,
|
|
3768
|
+
returnedCount: quotes.length,
|
|
3769
|
+
malformedDefinitionCount,
|
|
3770
|
+
missingFieldCounts,
|
|
3771
|
+
};
|
|
3772
|
+
return { quotes, diagnostics };
|
|
3659
3773
|
}
|
|
3660
|
-
async
|
|
3661
|
-
const
|
|
3662
|
-
const
|
|
3663
|
-
const
|
|
3664
|
-
for (const batch of
|
|
3774
|
+
async fetchNullableOptionQuotes(contracts, telemetry) {
|
|
3775
|
+
const quotes = [];
|
|
3776
|
+
const batches = chunks(contracts, OPTION_MARKETDATA_BATCH_SIZE);
|
|
3777
|
+
const snapshotsStarted = this.requestNow();
|
|
3778
|
+
for (const batch of batches) {
|
|
3665
3779
|
const params = {
|
|
3666
3780
|
conids: batch.map((contract) => contract.conid).join(","),
|
|
3667
3781
|
fields: OPTION_QUOTE_FIELDS,
|
|
3668
3782
|
};
|
|
3669
3783
|
await this.req({ path: "iserver/marketdata/snapshot", params });
|
|
3670
3784
|
await this.wait(2000);
|
|
3671
|
-
const
|
|
3785
|
+
const response = await this.req({
|
|
3672
3786
|
path: "iserver/marketdata/snapshot",
|
|
3673
3787
|
params,
|
|
3674
3788
|
});
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3789
|
+
if (!Array.isArray(response)) {
|
|
3790
|
+
throw new Error("IBKR returned malformed option market-data snapshots");
|
|
3791
|
+
}
|
|
3792
|
+
const snapshots = response.filter((snapshot) => isUnknownRecord(snapshot) &&
|
|
3793
|
+
typeof snapshot["conid"] === "number" &&
|
|
3794
|
+
Number.isSafeInteger(snapshot["conid"]) &&
|
|
3795
|
+
snapshot["conid"] > 0);
|
|
3796
|
+
const byConid = new Map(snapshots.map((snapshot) => [snapshot.conid, snapshot]));
|
|
3678
3797
|
for (const contract of batch) {
|
|
3679
3798
|
const snapshot = byConid.get(contract.conid);
|
|
3680
|
-
const bid = snapshot ? this.snapshotNumber(snapshot, "84") :
|
|
3681
|
-
const ask = snapshot ? this.snapshotNumber(snapshot, "86") :
|
|
3682
|
-
const
|
|
3683
|
-
|
|
3684
|
-
if (allowIncomplete) {
|
|
3685
|
-
skipped.push(contract.symbol);
|
|
3686
|
-
continue;
|
|
3687
|
-
}
|
|
3688
|
-
throw new Error(`IBKR returned incomplete option market data for ${contract.symbol} (bid/ask/delta required)`);
|
|
3689
|
-
}
|
|
3690
|
-
const volume = snapshot ? (this.snapshotVolume(snapshot) ?? null) : null;
|
|
3691
|
-
const openInterest = snapshot ? (this.snapshotNumber(snapshot, "7638") ?? null) : null;
|
|
3692
|
-
result.push({
|
|
3799
|
+
const bid = snapshot ? (this.snapshotNumber(snapshot, "84") ?? null) : null;
|
|
3800
|
+
const ask = snapshot ? (this.snapshotNumber(snapshot, "86") ?? null) : null;
|
|
3801
|
+
const rawAvailability = snapshot?.["6509"];
|
|
3802
|
+
quotes.push({
|
|
3693
3803
|
...contract,
|
|
3694
3804
|
bid,
|
|
3695
3805
|
ask,
|
|
3696
|
-
mid: (bid + ask) / 2,
|
|
3697
|
-
delta,
|
|
3698
|
-
volume,
|
|
3699
|
-
openInterest,
|
|
3700
|
-
availability:
|
|
3806
|
+
mid: bid !== null && ask !== null ? (bid + ask) / 2 : null,
|
|
3807
|
+
delta: snapshot ? (this.snapshotNumber(snapshot, "7308") ?? null) : null,
|
|
3808
|
+
volume: snapshot ? (this.snapshotVolume(snapshot) ?? null) : null,
|
|
3809
|
+
openInterest: snapshot ? (this.snapshotNumber(snapshot, "7638") ?? null) : null,
|
|
3810
|
+
availability: typeof rawAvailability === "string" || typeof rawAvailability === "number"
|
|
3811
|
+
? normalizeDerivativeDataAvailability(rawAvailability)
|
|
3812
|
+
: null,
|
|
3701
3813
|
timestamp: snapshot ? this.snapshotTimestamp(snapshot) : null,
|
|
3702
3814
|
});
|
|
3703
3815
|
}
|
|
3704
3816
|
}
|
|
3817
|
+
if (telemetry !== undefined) {
|
|
3818
|
+
this.emitOptionDiscoveryTelemetry({
|
|
3819
|
+
phase: "SNAPSHOTS",
|
|
3820
|
+
...telemetry,
|
|
3821
|
+
durationMs: this.elapsedSince(snapshotsStarted),
|
|
3822
|
+
definitionRequestCount: 0,
|
|
3823
|
+
snapshotBatchCount: batches.length,
|
|
3824
|
+
});
|
|
3825
|
+
}
|
|
3826
|
+
return quotes;
|
|
3827
|
+
}
|
|
3828
|
+
async fetchOptionQuotes(contracts, options = {}) {
|
|
3829
|
+
const { allowIncomplete = false, telemetry } = options;
|
|
3830
|
+
const result = [];
|
|
3831
|
+
const skipped = [];
|
|
3832
|
+
for (const quote of await this.fetchNullableOptionQuotes(contracts, telemetry)) {
|
|
3833
|
+
if (quote.bid === null || quote.ask === null || quote.delta === null) {
|
|
3834
|
+
if (allowIncomplete) {
|
|
3835
|
+
skipped.push(quote.symbol);
|
|
3836
|
+
continue;
|
|
3837
|
+
}
|
|
3838
|
+
throw new Error(`IBKR returned incomplete option market data for ${quote.symbol} (bid/ask/delta required)`);
|
|
3839
|
+
}
|
|
3840
|
+
result.push({
|
|
3841
|
+
...quote,
|
|
3842
|
+
bid: quote.bid,
|
|
3843
|
+
ask: quote.ask,
|
|
3844
|
+
mid: (quote.bid + quote.ask) / 2,
|
|
3845
|
+
delta: quote.delta,
|
|
3846
|
+
availability: quote.availability ?? "unavailable",
|
|
3847
|
+
});
|
|
3848
|
+
}
|
|
3705
3849
|
if (allowIncomplete && skipped.length && skipped.length === contracts.length) {
|
|
3706
3850
|
const symbol = contracts[0]?.underlying ?? "unknown";
|
|
3707
3851
|
const expiry = contracts[0]?.expiry ?? "unknown";
|
|
@@ -3728,6 +3872,21 @@ export class IbkrClient {
|
|
|
3728
3872
|
const endDay = this.utcDayStart(input.endDate);
|
|
3729
3873
|
return { start, end: endDay + DAY_MS - 1, days: (endDay - start) / DAY_MS + 1 };
|
|
3730
3874
|
}
|
|
3875
|
+
elapsedSince(startedAt) {
|
|
3876
|
+
return Math.max(0, this.requestNow() - startedAt);
|
|
3877
|
+
}
|
|
3878
|
+
emitOptionDiscoveryTelemetry(event) {
|
|
3879
|
+
try {
|
|
3880
|
+
const result = this.onOptionDiscoveryTelemetry({
|
|
3881
|
+
event: "OPTION_DISCOVERY_PHASE",
|
|
3882
|
+
...event,
|
|
3883
|
+
});
|
|
3884
|
+
void Promise.resolve(result).catch(() => undefined);
|
|
3885
|
+
}
|
|
3886
|
+
catch {
|
|
3887
|
+
// Telemetry observers cannot change discovery or quote settlement.
|
|
3888
|
+
}
|
|
3889
|
+
}
|
|
3731
3890
|
async requestPriceHistory(contract, requestedSymbol, request) {
|
|
3732
3891
|
this.onPriceHistoryTelemetry({
|
|
3733
3892
|
event: "PRICE_HISTORY_REQUEST",
|
|
@@ -4370,6 +4529,7 @@ export class IbkrClient {
|
|
|
4370
4529
|
return this.requestScheduler.schedule({
|
|
4371
4530
|
endpoint: this.requestEndpoint(input.path),
|
|
4372
4531
|
priority: this.requestPriority(input.path),
|
|
4532
|
+
secdefInfo: input.path === "iserver/secdef/info",
|
|
4373
4533
|
retryable: retryPolicy !== "SINGLE_ATTEMPT",
|
|
4374
4534
|
retryServerErrors: retryPolicy === "PRICE_HISTORY",
|
|
4375
4535
|
}, async () => {
|