@huskly/ibkr-client 1.6.0 → 2.0.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 +61 -13
- package/dist/ibkr/ibkrClient.d.ts +36 -3
- package/dist/ibkr/ibkrClient.d.ts.map +1 -1
- package/dist/ibkr/ibkrClient.js +692 -363
- package/dist/ibkr/ibkrClient.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 +52 -8
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/ibkr/ibkrClient.js
CHANGED
|
@@ -99,6 +99,11 @@ function parseRetryAfter(raw, now) {
|
|
|
99
99
|
function isUnknownRecord(input) {
|
|
100
100
|
return typeof input === "object" && input !== null && !Array.isArray(input);
|
|
101
101
|
}
|
|
102
|
+
function isBrokerageSessionInput(input) {
|
|
103
|
+
return (isUnknownRecord(input) &&
|
|
104
|
+
typeof input["compete"] === "boolean" &&
|
|
105
|
+
typeof input["publish"] === "boolean");
|
|
106
|
+
}
|
|
102
107
|
/**
|
|
103
108
|
* Every distinct listing of an exact symbol that carries a section of one asset class (#671).
|
|
104
109
|
*
|
|
@@ -410,6 +415,9 @@ export class IbkrInsufficientHistoryError extends Error {
|
|
|
410
415
|
export class IbkrClient {
|
|
411
416
|
raw;
|
|
412
417
|
initPromise;
|
|
418
|
+
logoutPromise;
|
|
419
|
+
closed = false;
|
|
420
|
+
accountCriticalSectionTail = Promise.resolve();
|
|
413
421
|
accountIdPromise;
|
|
414
422
|
optionDiscovery = new Map();
|
|
415
423
|
optionDefinitionCache;
|
|
@@ -441,125 +449,194 @@ export class IbkrClient {
|
|
|
441
449
|
: { onTelemetry: options.onRequestTelemetry }),
|
|
442
450
|
});
|
|
443
451
|
}
|
|
444
|
-
/**
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
// IBKR is slow right after init; give the session a moment to settle.
|
|
454
|
-
await this.wait(1000);
|
|
455
|
-
})();
|
|
452
|
+
/**
|
|
453
|
+
* Obtain the live session token with the legacy competing-session behavior.
|
|
454
|
+
*
|
|
455
|
+
* @deprecated Use {@link initializeBrokerageSession} with explicit flags.
|
|
456
|
+
*/
|
|
457
|
+
async init() {
|
|
458
|
+
this.assertOpen();
|
|
459
|
+
this.initPromise ??= this.initializeBrokerageSession({ compete: true, publish: true });
|
|
456
460
|
return this.initPromise;
|
|
457
461
|
}
|
|
462
|
+
async initializeBrokerageSession(input) {
|
|
463
|
+
this.assertOpen();
|
|
464
|
+
const rawInput = input;
|
|
465
|
+
if (!isBrokerageSessionInput(rawInput)) {
|
|
466
|
+
throw new TypeError("IBKR brokerage session initialization flags must be exact booleans");
|
|
467
|
+
}
|
|
468
|
+
try {
|
|
469
|
+
await this.raw.init(rawInput.compete, rawInput.publish);
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
throw this.normalizeHttpError(error);
|
|
473
|
+
}
|
|
474
|
+
// IBKR is slow right after initialization; give the session a moment to settle.
|
|
475
|
+
await this.wait(1000);
|
|
476
|
+
}
|
|
477
|
+
async renewBrokerageSession(input) {
|
|
478
|
+
this.assertOpen();
|
|
479
|
+
const rawInput = input;
|
|
480
|
+
if (!isBrokerageSessionInput(rawInput) || rawInput.compete) {
|
|
481
|
+
throw new TypeError("IBKR brokerage session renewal requires compete false and an exact publish boolean");
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
await this.raw.init(false, rawInput.publish);
|
|
485
|
+
}
|
|
486
|
+
catch (error) {
|
|
487
|
+
throw this.normalizeHttpError(error);
|
|
488
|
+
}
|
|
489
|
+
await this.wait(1000);
|
|
490
|
+
}
|
|
491
|
+
async getSessionEvidence() {
|
|
492
|
+
this.assertOpen();
|
|
493
|
+
const [status, rawAccounts] = await Promise.all([
|
|
494
|
+
this.getAuthStatus(),
|
|
495
|
+
this.req({ path: "iserver/accounts" }),
|
|
496
|
+
]);
|
|
497
|
+
const accounts = isUnknownRecord(rawAccounts) ? rawAccounts : {};
|
|
498
|
+
return {
|
|
499
|
+
authenticated: status.authenticated,
|
|
500
|
+
competing: status.competing,
|
|
501
|
+
connected: status.connected,
|
|
502
|
+
accountIds: this.accountIdsOrNull(accounts["accounts"]),
|
|
503
|
+
selectedAccountId: typeof accounts["selectedAccount"] === "string" ? accounts["selectedAccount"] : null,
|
|
504
|
+
isPaper: this.booleanOrNull(accounts["isPaper"]),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
async tickle() {
|
|
508
|
+
this.assertOpen();
|
|
509
|
+
await this.req({ path: "tickle", method: "POST" });
|
|
510
|
+
}
|
|
511
|
+
async logout() {
|
|
512
|
+
this.assertOpen();
|
|
513
|
+
this.logoutPromise ??= this.singleAttemptRequest({
|
|
514
|
+
path: "logout",
|
|
515
|
+
method: "POST",
|
|
516
|
+
}).then(() => undefined);
|
|
517
|
+
return this.logoutPromise;
|
|
518
|
+
}
|
|
519
|
+
close() {
|
|
520
|
+
if (this.closed)
|
|
521
|
+
return Promise.resolve();
|
|
522
|
+
this.closed = true;
|
|
523
|
+
delete this.accountIdPromise;
|
|
524
|
+
this.optionDiscovery.clear();
|
|
525
|
+
this.optionContractResolution.clear();
|
|
526
|
+
this.priceHistoryContractResolution.clear();
|
|
527
|
+
this.derivativeDiscovery.clear();
|
|
528
|
+
return Promise.resolve();
|
|
529
|
+
}
|
|
458
530
|
async getAuthStatus() {
|
|
459
|
-
|
|
531
|
+
this.assertOpen();
|
|
532
|
+
const rawStatus = await this.req({
|
|
460
533
|
path: "iserver/auth/status",
|
|
461
534
|
method: "POST",
|
|
462
535
|
});
|
|
536
|
+
const status = isUnknownRecord(rawStatus) ? rawStatus : {};
|
|
463
537
|
return {
|
|
464
|
-
authenticated: status
|
|
465
|
-
competing: status
|
|
538
|
+
authenticated: this.booleanOrNull(status["authenticated"]),
|
|
539
|
+
competing: this.booleanOrNull(status["competing"]),
|
|
540
|
+
connected: this.booleanOrNull(status["connected"]),
|
|
466
541
|
};
|
|
467
542
|
}
|
|
468
543
|
async getTradingDiagnostics(accountId) {
|
|
544
|
+
this.assertOpen();
|
|
469
545
|
if (!accountId.trim())
|
|
470
546
|
throw new Error("An explicit IBKR account ID is required");
|
|
471
|
-
const [status,
|
|
547
|
+
const [status, rawAccounts] = await Promise.all([
|
|
472
548
|
this.getAuthStatus(),
|
|
473
549
|
this.req({ path: "iserver/accounts" }),
|
|
474
550
|
]);
|
|
475
|
-
|
|
551
|
+
const accounts = isUnknownRecord(rawAccounts) ? rawAccounts : {};
|
|
552
|
+
const accountIds = this.accountIdsOrNull(accounts["accounts"]);
|
|
553
|
+
if (!accountIds?.includes(accountId)) {
|
|
476
554
|
throw new Error(`IBKR account ${accountId} is not available to this session`);
|
|
477
555
|
}
|
|
556
|
+
const rawFeatures = accounts["allowFeatures"];
|
|
557
|
+
const features = isUnknownRecord(rawFeatures) ? rawFeatures : {};
|
|
558
|
+
const rawAssetTypes = features["allowedAssetTypes"];
|
|
478
559
|
return {
|
|
479
560
|
accountId,
|
|
480
|
-
selectedAccountId: accounts
|
|
481
|
-
environment: accounts
|
|
561
|
+
selectedAccountId: typeof accounts["selectedAccount"] === "string" ? accounts["selectedAccount"] : null,
|
|
562
|
+
environment: accounts["isPaper"] === true ? "paper" : accounts["isPaper"] === false ? "live" : null,
|
|
482
563
|
authenticated: status.authenticated,
|
|
564
|
+
connected: status.connected,
|
|
483
565
|
competingSession: status.competing,
|
|
484
|
-
marketDataAvailable:
|
|
485
|
-
advisoryAssetPermissions:
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
566
|
+
marketDataAvailable: this.booleanOrNull(features["showGFIS"]),
|
|
567
|
+
advisoryAssetPermissions: typeof rawAssetTypes === "string"
|
|
568
|
+
? rawAssetTypes
|
|
569
|
+
.split(",")
|
|
570
|
+
.map((value) => value.trim())
|
|
571
|
+
.filter(Boolean)
|
|
572
|
+
: [],
|
|
489
573
|
};
|
|
490
574
|
}
|
|
491
575
|
async previewDerivativeCombo(request) {
|
|
576
|
+
this.assertOpen();
|
|
492
577
|
this.validateComboPreview(request);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
method: "POST",
|
|
506
|
-
data: {
|
|
507
|
-
orders: [this.comboOrderTicket(request)],
|
|
508
|
-
},
|
|
578
|
+
return this.withTradingMutation(request.accountId, "IBKR brokerage session is not safely authenticated for What-If", async (diagnostics) => {
|
|
579
|
+
const conids = request.legs.map(({ contract }) => contract.conid).join(",");
|
|
580
|
+
await this.req({
|
|
581
|
+
path: "iserver/marketdata/snapshot",
|
|
582
|
+
params: { conids, fields: "6509" },
|
|
583
|
+
});
|
|
584
|
+
const response = await this.singleAttemptRequest({
|
|
585
|
+
path: `iserver/account/${request.accountId}/orders/whatif`,
|
|
586
|
+
method: "POST",
|
|
587
|
+
data: { orders: [this.comboOrderTicket(request)] },
|
|
588
|
+
});
|
|
589
|
+
return this.normalizeComboPreview(request.accountId, diagnostics, response);
|
|
509
590
|
});
|
|
510
|
-
return this.normalizeComboPreview(request.accountId, diagnostics, response);
|
|
511
591
|
}
|
|
512
592
|
async submitDerivativeCombo(request) {
|
|
593
|
+
this.assertOpen();
|
|
513
594
|
this.validateComboPreview(request);
|
|
514
595
|
if (!request.clientOrderId.trim() || request.clientOrderId.length > 64) {
|
|
515
596
|
throw new Error("Client order ID must contain 1 to 64 characters");
|
|
516
597
|
}
|
|
517
598
|
const cmeOperatorMetadata = this.cmeOperatorMetadata(request.legs[0].contract.assetClass, request);
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
],
|
|
534
|
-
},
|
|
599
|
+
return this.withTradingMutation(request.accountId, "IBKR brokerage session is not safely authenticated for submission", async () => {
|
|
600
|
+
const response = await this.singleAttemptRequest({
|
|
601
|
+
path: `iserver/account/${request.accountId}/orders`,
|
|
602
|
+
method: "POST",
|
|
603
|
+
data: {
|
|
604
|
+
orders: [
|
|
605
|
+
{
|
|
606
|
+
...this.comboOrderTicket(request),
|
|
607
|
+
cOID: request.clientOrderId,
|
|
608
|
+
...cmeOperatorMetadata,
|
|
609
|
+
},
|
|
610
|
+
],
|
|
611
|
+
},
|
|
612
|
+
});
|
|
613
|
+
return this.normalizeOrderSubmission(response, request.clientOrderId);
|
|
535
614
|
});
|
|
536
|
-
return this.normalizeOrderSubmission(response, request.clientOrderId);
|
|
537
615
|
}
|
|
538
616
|
async submitDerivativeSingleOrder(request) {
|
|
617
|
+
this.assertOpen();
|
|
539
618
|
this.validateSingleOrder(request);
|
|
540
619
|
const cmeOperatorMetadata = this.cmeOperatorMetadata(request.contract.assetClass, request);
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
],
|
|
558
|
-
},
|
|
620
|
+
return this.withTradingMutation(request.accountId, "IBKR brokerage session is not safely authenticated for submission", async () => {
|
|
621
|
+
const response = await this.singleAttemptRequest({
|
|
622
|
+
path: `iserver/account/${request.accountId}/orders`,
|
|
623
|
+
method: "POST",
|
|
624
|
+
data: {
|
|
625
|
+
orders: [
|
|
626
|
+
{
|
|
627
|
+
...this.singleOrderTicket(request),
|
|
628
|
+
...(request.clientOrderId !== undefined ? { cOID: request.clientOrderId } : {}),
|
|
629
|
+
...(request.parentId !== undefined ? { parentId: request.parentId } : {}),
|
|
630
|
+
...cmeOperatorMetadata,
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
return this.normalizeOrderSubmission(response, request.clientOrderId ?? null);
|
|
559
636
|
});
|
|
560
|
-
return this.normalizeOrderSubmission(response, request.clientOrderId ?? null);
|
|
561
637
|
}
|
|
562
638
|
async submitDerivativeContingentOrders(request) {
|
|
639
|
+
this.assertOpen();
|
|
563
640
|
const { accountId, parent, child } = request;
|
|
564
641
|
if (!accountId.trim())
|
|
565
642
|
throw new Error("An explicit IBKR account ID is required");
|
|
@@ -578,64 +655,58 @@ export class IbkrClient {
|
|
|
578
655
|
}
|
|
579
656
|
const parentMetadata = this.cmeOperatorMetadata(parent.contract.assetClass, parent);
|
|
580
657
|
const childMetadata = this.cmeOperatorMetadata(child.contract.assetClass, child);
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
],
|
|
602
|
-
},
|
|
658
|
+
return this.withTradingMutation(accountId, "IBKR brokerage session is not safely authenticated for submission", async () => {
|
|
659
|
+
const response = await this.singleAttemptRequest({
|
|
660
|
+
path: `iserver/account/${accountId}/orders`,
|
|
661
|
+
method: "POST",
|
|
662
|
+
data: {
|
|
663
|
+
orders: [
|
|
664
|
+
{
|
|
665
|
+
...this.singleOrderTicket(parent),
|
|
666
|
+
cOID: parent.clientOrderId,
|
|
667
|
+
...parentMetadata,
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
...this.singleOrderTicket(child),
|
|
671
|
+
parentId: parent.clientOrderId,
|
|
672
|
+
...childMetadata,
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
},
|
|
676
|
+
});
|
|
677
|
+
return this.normalizeMultiOrderSubmission(response, parent.clientOrderId, accountId);
|
|
603
678
|
});
|
|
604
|
-
return this.normalizeMultiOrderSubmission(response, parent.clientOrderId);
|
|
605
679
|
}
|
|
606
680
|
async submitDerivativeOrderGraph(request) {
|
|
681
|
+
this.assertOpen();
|
|
607
682
|
this.validateOrderGraph(request);
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
method: "POST",
|
|
616
|
-
data: { orders: request.nodes.map((node) => this.graphOrderTicket(request, node)) },
|
|
683
|
+
return this.withTradingMutation(request.accountId, "IBKR brokerage session is not safely authenticated for submission", async () => {
|
|
684
|
+
const response = await this.singleAttemptRequest({
|
|
685
|
+
path: `iserver/account/${request.accountId}/orders`,
|
|
686
|
+
method: "POST",
|
|
687
|
+
data: { orders: request.nodes.map((node) => this.graphOrderTicket(request, node)) },
|
|
688
|
+
});
|
|
689
|
+
return this.normalizeOrderGraphSubmission(response, request, []);
|
|
617
690
|
});
|
|
618
|
-
return this.normalizeOrderGraphSubmission(response, request, []);
|
|
619
691
|
}
|
|
620
692
|
async acknowledgeDerivativeOrderGraphWarning(input) {
|
|
693
|
+
this.assertOpen();
|
|
621
694
|
if (input.confirmed !== true)
|
|
622
695
|
throw new Error("Order warning confirmation must be true");
|
|
623
696
|
this.validateOrderGraph(input.continuation.request);
|
|
624
697
|
if (!input.continuation.replyId.trim())
|
|
625
698
|
throw new Error("An exact warning reply ID is required");
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
method: "POST",
|
|
634
|
-
data: { confirmed: true },
|
|
699
|
+
return this.withTradingMutation(input.continuation.request.accountId, "IBKR brokerage session is not safely authenticated for submission", async () => {
|
|
700
|
+
const response = await this.singleAttemptRequest({
|
|
701
|
+
path: `iserver/reply/${encodeURIComponent(input.continuation.replyId)}`,
|
|
702
|
+
method: "POST",
|
|
703
|
+
data: { confirmed: true },
|
|
704
|
+
});
|
|
705
|
+
return this.normalizeOrderGraphSubmission(response, input.continuation.request, input.continuation.members);
|
|
635
706
|
});
|
|
636
|
-
return this.normalizeOrderGraphSubmission(response, input.continuation.request, input.continuation.members);
|
|
637
707
|
}
|
|
638
708
|
async recoverDerivativeOrderGraph(input, request) {
|
|
709
|
+
this.assertOpen();
|
|
639
710
|
this.validateOrderGraph(request);
|
|
640
711
|
if (input.accountId !== request.accountId)
|
|
641
712
|
throw new Error("Graph recovery account does not match request");
|
|
@@ -646,138 +717,140 @@ export class IbkrClient {
|
|
|
646
717
|
if (input.orderId !== undefined && !input.orderId.trim()) {
|
|
647
718
|
throw new Error("An exact broker order ID is required for graph recovery");
|
|
648
719
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
const
|
|
672
|
-
this.
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
const
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const orderId = this.recoveryOrderId(match);
|
|
684
|
-
if (orderId === undefined)
|
|
685
|
-
continue;
|
|
686
|
-
if (usedOrderIds.has(orderId))
|
|
687
|
-
continue;
|
|
688
|
-
usedOrderIds.add(orderId);
|
|
689
|
-
selected.set(node.memberId, match);
|
|
690
|
-
}
|
|
691
|
-
const unresolved = request.nodes.filter((node) => !selected.has(node.memberId));
|
|
692
|
-
const terminalEvidence = await this.findRecoveryGraphTerminalCandidates(input.accountId, request, input.orderId);
|
|
693
|
-
const conflictingKnownActiveTickets = knownActiveOrders.filter((activeOrder) => terminalEvidence.linkedOrders.some((terminalOrder) => this.recoveryOrderId(terminalOrder) === input.orderId &&
|
|
694
|
-
this.terminalOrderTicketConflicts(activeOrder, terminalOrder)));
|
|
695
|
-
this.reconcileSelectedGraphMembers(request, selected, terminalEvidence);
|
|
696
|
-
if (unresolved.length > 0) {
|
|
697
|
-
const assignments = this.assignRecoveryGraphCandidates(unresolved, terminalEvidence.byNode, usedOrderIds);
|
|
698
|
-
for (const node of unresolved) {
|
|
699
|
-
const order = assignments.get(node.memberId);
|
|
700
|
-
if (order === undefined)
|
|
720
|
+
return this.withAccountCriticalSection(async () => {
|
|
721
|
+
await this.prepareBrokerageAccount(input.accountId);
|
|
722
|
+
const response = await this.req({
|
|
723
|
+
path: "iserver/account/orders",
|
|
724
|
+
params: { accountId: input.accountId },
|
|
725
|
+
});
|
|
726
|
+
const flattenedActiveSnapshot = this.resolveFlattenedGraphParentAliases(request, this.flattenCompleteOrderSnapshot(response));
|
|
727
|
+
const activeSnapshotIncomplete = flattenedActiveSnapshot === null;
|
|
728
|
+
const invalidActiveAccountEvidence = flattenedActiveSnapshot?.some(({ order }) => !this.orderHasExactAccount(order, input.accountId)) ?? false;
|
|
729
|
+
const invalidNestedActiveEvidence = flattenedActiveSnapshot?.some(({ order, nestedParent }) => nestedParent !== null &&
|
|
730
|
+
!this.recoveryGraphOrderMayBeAttached(request, order) &&
|
|
731
|
+
this.recoveryGraphOrderIsAttached(request, nestedParent)) ?? false;
|
|
732
|
+
const accountOrders = (flattenedActiveSnapshot ?? [])
|
|
733
|
+
.map(({ order }) => order)
|
|
734
|
+
.filter((order) => this.orderHasExactAccount(order, input.accountId));
|
|
735
|
+
const activeMatchesByMember = new Map();
|
|
736
|
+
const knownActiveOrders = accountOrders.filter((order) => input.orderId !== undefined && this.recoveryOrderId(order) === input.orderId);
|
|
737
|
+
const conflictingKnownActiveOrders = knownActiveOrders.filter((order) => !this.recoveryGraphOrderMayBeAttached(request, order));
|
|
738
|
+
const observedCandidates = activeSnapshotIncomplete || invalidActiveAccountEvidence || invalidNestedActiveEvidence
|
|
739
|
+
? [response]
|
|
740
|
+
: [...conflictingKnownActiveOrders];
|
|
741
|
+
observedCandidates.push(...accountOrders.filter((order) => this.recoveryGraphOrderMayBeAttached(request, order)));
|
|
742
|
+
for (const node of request.nodes) {
|
|
743
|
+
const matches = accountOrders.filter((order) => this.terminalOrderTicketIsValid(order) &&
|
|
744
|
+
this.orderHasValidRecoveryStatus(order) &&
|
|
745
|
+
this.recoveryOrderMatchesGraphNode(request, node, order));
|
|
746
|
+
activeMatchesByMember.set(node.memberId, matches);
|
|
747
|
+
}
|
|
748
|
+
const selected = new Map();
|
|
749
|
+
const usedOrderIds = new Set();
|
|
750
|
+
for (const node of request.nodes) {
|
|
751
|
+
const matches = activeMatchesByMember.get(node.memberId) ?? [];
|
|
752
|
+
const [match] = matches;
|
|
753
|
+
if (matches.length !== 1 || match === undefined)
|
|
701
754
|
continue;
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
755
|
+
const orderId = this.recoveryOrderId(match);
|
|
756
|
+
if (orderId === undefined)
|
|
757
|
+
continue;
|
|
758
|
+
if (usedOrderIds.has(orderId))
|
|
759
|
+
continue;
|
|
760
|
+
usedOrderIds.add(orderId);
|
|
761
|
+
selected.set(node.memberId, match);
|
|
707
762
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
if (!this.recoveryGraphOrderMayBeAttached(request, order))
|
|
726
|
-
continue;
|
|
727
|
-
if (!this.recoveryGraphOrderIsAttached(request, order)) {
|
|
728
|
-
linkedOrderMissingBrokerId = true;
|
|
729
|
-
continue;
|
|
763
|
+
const unresolved = request.nodes.filter((node) => !selected.has(node.memberId));
|
|
764
|
+
const terminalEvidence = await this.findRecoveryGraphTerminalCandidates(input.accountId, request, input.orderId);
|
|
765
|
+
const conflictingKnownActiveTickets = knownActiveOrders.filter((activeOrder) => terminalEvidence.linkedOrders.some((terminalOrder) => this.recoveryOrderId(terminalOrder) === input.orderId &&
|
|
766
|
+
this.terminalOrderTicketConflicts(activeOrder, terminalOrder)));
|
|
767
|
+
this.reconcileSelectedGraphMembers(request, selected, terminalEvidence);
|
|
768
|
+
if (unresolved.length > 0) {
|
|
769
|
+
const assignments = this.assignRecoveryGraphCandidates(unresolved, terminalEvidence.byNode, usedOrderIds);
|
|
770
|
+
for (const node of unresolved) {
|
|
771
|
+
const order = assignments.get(node.memberId);
|
|
772
|
+
if (order === undefined)
|
|
773
|
+
continue;
|
|
774
|
+
selected.set(node.memberId, order);
|
|
775
|
+
const orderId = this.recoveryOrderId(order);
|
|
776
|
+
if (orderId !== undefined)
|
|
777
|
+
usedOrderIds.add(orderId);
|
|
778
|
+
observedCandidates.push(order);
|
|
779
|
+
}
|
|
730
780
|
}
|
|
731
|
-
|
|
732
|
-
if (
|
|
733
|
-
|
|
734
|
-
continue;
|
|
781
|
+
observedCandidates.push(...terminalEvidence.observedResponses);
|
|
782
|
+
if (terminalEvidence.invalidAttachedEvidence) {
|
|
783
|
+
observedCandidates.push({ reason: "Invalid or conflicting terminal broker evidence" });
|
|
735
784
|
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
785
|
+
const selectedCandidates = [...selected.values()];
|
|
786
|
+
const requestedOrderIdMissing = input.orderId !== undefined &&
|
|
787
|
+
!selectedCandidates.some((order) => this.recoveryOrderId(order) === input.orderId);
|
|
788
|
+
const members = request.nodes.map((node) => {
|
|
789
|
+
const order = selected.get(node.memberId);
|
|
790
|
+
return this.graphMemberEvidence(request, node, order);
|
|
791
|
+
});
|
|
792
|
+
const ids = members.flatMap(({ orderId }) => (orderId === null ? [] : [orderId]));
|
|
793
|
+
const hasDistinctOrderIds = new Set(ids).size === request.nodes.length;
|
|
794
|
+
const linkedOrderIds = new Set();
|
|
795
|
+
let linkedOrderMissingBrokerId = false;
|
|
796
|
+
for (const order of accountOrders) {
|
|
797
|
+
if (!this.recoveryGraphOrderMayBeAttached(request, order))
|
|
798
|
+
continue;
|
|
799
|
+
if (!this.recoveryGraphOrderIsAttached(request, order)) {
|
|
800
|
+
linkedOrderMissingBrokerId = true;
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
const orderId = this.recoveryOrderId(order);
|
|
804
|
+
if (orderId === undefined) {
|
|
805
|
+
linkedOrderMissingBrokerId = true;
|
|
806
|
+
continue;
|
|
807
|
+
}
|
|
808
|
+
linkedOrderIds.add(orderId);
|
|
809
|
+
}
|
|
810
|
+
for (const order of terminalEvidence.linkedOrders) {
|
|
811
|
+
const orderId = this.recoveryOrderId(order);
|
|
812
|
+
if (orderId === undefined) {
|
|
813
|
+
linkedOrderMissingBrokerId = true;
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
816
|
+
linkedOrderIds.add(orderId);
|
|
817
|
+
}
|
|
818
|
+
const selectedOrderIds = new Set(selectedCandidates.flatMap((order) => {
|
|
819
|
+
const orderId = this.recoveryOrderId(order);
|
|
820
|
+
return orderId === undefined ? [] : [orderId];
|
|
821
|
+
}));
|
|
822
|
+
if (selected.size !== request.nodes.length ||
|
|
823
|
+
!hasDistinctOrderIds ||
|
|
824
|
+
linkedOrderMissingBrokerId ||
|
|
825
|
+
[...linkedOrderIds].some((orderId) => !selectedOrderIds.has(orderId)) ||
|
|
826
|
+
requestedOrderIdMissing ||
|
|
827
|
+
activeSnapshotIncomplete ||
|
|
828
|
+
invalidActiveAccountEvidence ||
|
|
829
|
+
invalidNestedActiveEvidence ||
|
|
830
|
+
conflictingKnownActiveOrders.length > 0 ||
|
|
831
|
+
conflictingKnownActiveTickets.length > 0 ||
|
|
832
|
+
terminalEvidence.invalidAttachedEvidence ||
|
|
833
|
+
terminalEvidence.terminalSnapshotLookupFailed ||
|
|
834
|
+
members.some(({ status }) => status === "UNKNOWN" || status === "WARNING_PENDING")) {
|
|
835
|
+
return {
|
|
836
|
+
state: "recovery_required",
|
|
837
|
+
rootClientOrderId: request.rootClientOrderId,
|
|
838
|
+
members,
|
|
839
|
+
reasons: [
|
|
840
|
+
"Exact graph recovery found incomplete, duplicated, or ambiguous member evidence",
|
|
841
|
+
],
|
|
842
|
+
warnings: [],
|
|
843
|
+
errors: [],
|
|
844
|
+
unrecognizedResponses: observedCandidates,
|
|
845
|
+
};
|
|
743
846
|
}
|
|
744
|
-
linkedOrderIds.add(orderId);
|
|
745
|
-
}
|
|
746
|
-
const selectedOrderIds = new Set(selectedCandidates.flatMap((order) => {
|
|
747
|
-
const orderId = this.recoveryOrderId(order);
|
|
748
|
-
return orderId === undefined ? [] : [orderId];
|
|
749
|
-
}));
|
|
750
|
-
if (selected.size !== request.nodes.length ||
|
|
751
|
-
!hasDistinctOrderIds ||
|
|
752
|
-
linkedOrderMissingBrokerId ||
|
|
753
|
-
[...linkedOrderIds].some((orderId) => !selectedOrderIds.has(orderId)) ||
|
|
754
|
-
requestedOrderIdMissing ||
|
|
755
|
-
activeSnapshotIncomplete ||
|
|
756
|
-
invalidActiveAccountEvidence ||
|
|
757
|
-
invalidNestedActiveEvidence ||
|
|
758
|
-
conflictingKnownActiveOrders.length > 0 ||
|
|
759
|
-
conflictingKnownActiveTickets.length > 0 ||
|
|
760
|
-
terminalEvidence.invalidAttachedEvidence ||
|
|
761
|
-
terminalEvidence.terminalSnapshotLookupFailed ||
|
|
762
|
-
members.some(({ status }) => status === "UNKNOWN" || status === "WARNING_PENDING")) {
|
|
763
847
|
return {
|
|
764
|
-
state: "
|
|
848
|
+
state: "accepted",
|
|
765
849
|
rootClientOrderId: request.rootClientOrderId,
|
|
766
|
-
members,
|
|
767
|
-
reasons: [
|
|
768
|
-
"Exact graph recovery found incomplete, duplicated, or ambiguous member evidence",
|
|
769
|
-
],
|
|
850
|
+
members: this.attachGraphParentOrderIds(members),
|
|
770
851
|
warnings: [],
|
|
771
|
-
errors: [],
|
|
772
|
-
unrecognizedResponses: observedCandidates,
|
|
773
852
|
};
|
|
774
|
-
}
|
|
775
|
-
return {
|
|
776
|
-
state: "accepted",
|
|
777
|
-
rootClientOrderId: request.rootClientOrderId,
|
|
778
|
-
members: this.attachGraphParentOrderIds(members),
|
|
779
|
-
warnings: [],
|
|
780
|
-
};
|
|
853
|
+
});
|
|
781
854
|
}
|
|
782
855
|
reconcileSelectedGraphMembers(request, selected, terminalEvidence) {
|
|
783
856
|
for (const node of request.nodes) {
|
|
@@ -1293,38 +1366,58 @@ export class IbkrClient {
|
|
|
1293
1366
|
return assignments;
|
|
1294
1367
|
}
|
|
1295
1368
|
async acknowledgeOrderWarning(input) {
|
|
1369
|
+
this.assertOpen();
|
|
1370
|
+
const confirmed = input.confirmed;
|
|
1371
|
+
if (confirmed !== true)
|
|
1372
|
+
throw new Error("Order warning confirmation must be true");
|
|
1373
|
+
if (!input.accountId.trim())
|
|
1374
|
+
throw new Error("An exact account ID is required");
|
|
1296
1375
|
if (!input.replyId.trim())
|
|
1297
1376
|
throw new Error("An exact warning reply ID is required");
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1377
|
+
return this.withTradingMutation(input.accountId, "IBKR brokerage session is not safely authenticated for warning acknowledgement", async () => {
|
|
1378
|
+
const response = await this.singleAttemptRequest({
|
|
1379
|
+
path: `iserver/reply/${encodeURIComponent(input.replyId)}`,
|
|
1380
|
+
method: "POST",
|
|
1381
|
+
data: { confirmed: true },
|
|
1382
|
+
});
|
|
1383
|
+
return this.normalizeOrderSubmission(response, null);
|
|
1302
1384
|
});
|
|
1303
|
-
return this.normalizeOrderSubmission(response, null);
|
|
1304
1385
|
}
|
|
1305
1386
|
async acknowledgeContingentOrderWarning(input) {
|
|
1387
|
+
this.assertOpen();
|
|
1306
1388
|
const confirmed = input.confirmed;
|
|
1307
1389
|
if (confirmed !== true) {
|
|
1308
1390
|
throw new Error("Order warning confirmation must be true");
|
|
1309
1391
|
}
|
|
1392
|
+
if (!input.continuation.accountId.trim()) {
|
|
1393
|
+
throw new Error("An exact account ID is required");
|
|
1394
|
+
}
|
|
1310
1395
|
if (!input.continuation.replyId.trim()) {
|
|
1311
1396
|
throw new Error("An exact warning reply ID is required");
|
|
1312
1397
|
}
|
|
1313
1398
|
if (!input.continuation.parentClientOrderId.trim()) {
|
|
1314
1399
|
throw new Error("An exact parent client order ID is required");
|
|
1315
1400
|
}
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1401
|
+
return this.withTradingMutation(input.continuation.accountId, "IBKR brokerage session is not safely authenticated for warning acknowledgement", async () => {
|
|
1402
|
+
const response = await this.singleAttemptRequest({
|
|
1403
|
+
path: `iserver/reply/${encodeURIComponent(input.continuation.replyId)}`,
|
|
1404
|
+
method: "POST",
|
|
1405
|
+
data: { confirmed: true },
|
|
1406
|
+
});
|
|
1407
|
+
return this.normalizeMultiOrderSubmission(response, input.continuation.parentClientOrderId, input.continuation.accountId);
|
|
1320
1408
|
});
|
|
1321
|
-
return this.normalizeMultiOrderSubmission(response, input.continuation.parentClientOrderId);
|
|
1322
1409
|
}
|
|
1323
1410
|
async getDerivativeOrderStatus(accountId, orderId) {
|
|
1411
|
+
this.assertOpen();
|
|
1324
1412
|
if (!accountId.trim() || !orderId.trim()) {
|
|
1325
1413
|
throw new Error("Exact account and order IDs are required");
|
|
1326
1414
|
}
|
|
1327
|
-
|
|
1415
|
+
return this.withAccountCriticalSection(async () => {
|
|
1416
|
+
await this.prepareBrokerageAccount(accountId);
|
|
1417
|
+
return this.getDerivativeOrderStatusPrepared(accountId, orderId);
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
async getDerivativeOrderStatusPrepared(accountId, orderId) {
|
|
1328
1421
|
const order = await this.req({
|
|
1329
1422
|
path: `iserver/account/order/status/${encodeURIComponent(orderId)}`,
|
|
1330
1423
|
});
|
|
@@ -1341,6 +1434,7 @@ export class IbkrClient {
|
|
|
1341
1434
|
return lifecycle;
|
|
1342
1435
|
}
|
|
1343
1436
|
async findDerivativeOrder(input) {
|
|
1437
|
+
this.assertOpen();
|
|
1344
1438
|
if (!input.accountId.trim())
|
|
1345
1439
|
throw new Error("An exact account ID is required");
|
|
1346
1440
|
const identity = input.orderId ?? input.clientOrderId;
|
|
@@ -1349,78 +1443,84 @@ export class IbkrClient {
|
|
|
1349
1443
|
if (input.orderId !== undefined) {
|
|
1350
1444
|
return this.getDerivativeOrderStatus(input.accountId, input.orderId);
|
|
1351
1445
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1446
|
+
return this.withAccountCriticalSection(async () => {
|
|
1447
|
+
await this.prepareBrokerageAccount(input.accountId);
|
|
1448
|
+
const response = await this.req({
|
|
1449
|
+
path: "iserver/account/orders",
|
|
1450
|
+
params: { accountId: input.accountId },
|
|
1451
|
+
});
|
|
1452
|
+
const order = response.orders?.find((candidate) => {
|
|
1453
|
+
if (!this.orderBelongsToAccount(candidate, input.accountId))
|
|
1454
|
+
return false;
|
|
1455
|
+
return (candidate.cOID ?? candidate.order_ref) === input.clientOrderId;
|
|
1456
|
+
});
|
|
1457
|
+
if (order === undefined)
|
|
1458
|
+
throw new Error(`IBKR order ${identity} was not found`);
|
|
1459
|
+
const orderId = order.order_id ?? order.orderId;
|
|
1460
|
+
if (orderId === undefined) {
|
|
1461
|
+
throw new Error(`IBKR order ${identity} did not include a broker order ID`);
|
|
1462
|
+
}
|
|
1463
|
+
return this.getDerivativeOrderStatusPrepared(input.accountId, String(orderId));
|
|
1361
1464
|
});
|
|
1362
|
-
if (order === undefined)
|
|
1363
|
-
throw new Error(`IBKR order ${identity} was not found`);
|
|
1364
|
-
const orderId = order.order_id ?? order.orderId;
|
|
1365
|
-
if (orderId === undefined) {
|
|
1366
|
-
throw new Error(`IBKR order ${identity} did not include a broker order ID`);
|
|
1367
|
-
}
|
|
1368
|
-
return this.getDerivativeOrderStatus(input.accountId, String(orderId));
|
|
1369
1465
|
}
|
|
1370
1466
|
async listActiveDerivativeOrders(accountId) {
|
|
1467
|
+
this.assertOpen();
|
|
1371
1468
|
if (!accountId.trim())
|
|
1372
1469
|
throw new Error("An exact account ID is required");
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
const invalidAccountEvidence = flattened.find(({ order }) => {
|
|
1383
|
-
const returnedAccounts = [order.account, order.acct];
|
|
1384
|
-
const providedAccounts = returnedAccounts.filter((value) => value !== undefined);
|
|
1385
|
-
return (providedAccounts.length === 0 ||
|
|
1386
|
-
providedAccounts.some((returnedAccount) => typeof returnedAccount !== "string" || returnedAccount !== accountId));
|
|
1387
|
-
});
|
|
1388
|
-
if (invalidAccountEvidence !== undefined) {
|
|
1389
|
-
throw new Error("IBKR active-order response did not provide unambiguous account identity");
|
|
1390
|
-
}
|
|
1391
|
-
const normalized = flattened.map(({ order, nestedParent }) => this.normalizeActiveDerivativeOrder(accountId, order, nestedParent));
|
|
1392
|
-
const byOrderId = new Map();
|
|
1393
|
-
const byClientId = new Map();
|
|
1394
|
-
for (const order of normalized) {
|
|
1395
|
-
if (order.orderId !== null) {
|
|
1396
|
-
const members = byOrderId.get(order.orderId) ?? [];
|
|
1397
|
-
members.push(order);
|
|
1398
|
-
byOrderId.set(order.orderId, members);
|
|
1470
|
+
return this.withAccountCriticalSection(async () => {
|
|
1471
|
+
await this.prepareBrokerageAccount(accountId);
|
|
1472
|
+
const response = await this.req({
|
|
1473
|
+
path: "iserver/account/orders",
|
|
1474
|
+
params: { accountId },
|
|
1475
|
+
});
|
|
1476
|
+
const flattened = this.flattenCompleteOrderSnapshot(response);
|
|
1477
|
+
if (flattened === null) {
|
|
1478
|
+
throw new Error("IBKR active-order snapshot is incomplete");
|
|
1399
1479
|
}
|
|
1400
|
-
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1403
|
-
|
|
1480
|
+
const invalidAccountEvidence = flattened.find(({ order }) => {
|
|
1481
|
+
const returnedAccounts = [order.account, order.acct];
|
|
1482
|
+
const providedAccounts = returnedAccounts.filter((value) => value !== undefined);
|
|
1483
|
+
return (providedAccounts.length === 0 ||
|
|
1484
|
+
providedAccounts.some((returnedAccount) => typeof returnedAccount !== "string" || returnedAccount !== accountId));
|
|
1485
|
+
});
|
|
1486
|
+
if (invalidAccountEvidence !== undefined) {
|
|
1487
|
+
throw new Error("IBKR active-order response did not provide unambiguous account identity");
|
|
1404
1488
|
}
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1489
|
+
const normalized = flattened.map(({ order, nestedParent }) => this.normalizeActiveDerivativeOrder(accountId, order, nestedParent));
|
|
1490
|
+
const byOrderId = new Map();
|
|
1491
|
+
const byClientId = new Map();
|
|
1492
|
+
for (const order of normalized) {
|
|
1493
|
+
if (order.orderId !== null) {
|
|
1494
|
+
const members = byOrderId.get(order.orderId) ?? [];
|
|
1495
|
+
members.push(order);
|
|
1496
|
+
byOrderId.set(order.orderId, members);
|
|
1497
|
+
}
|
|
1498
|
+
if (order.clientOrderId !== null) {
|
|
1499
|
+
const members = byClientId.get(order.clientOrderId) ?? [];
|
|
1500
|
+
members.push(order);
|
|
1501
|
+
byClientId.set(order.clientOrderId, members);
|
|
1502
|
+
}
|
|
1409
1503
|
}
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1504
|
+
for (const order of normalized) {
|
|
1505
|
+
if (order.orderId !== null && (byOrderId.get(order.orderId)?.length ?? 0) > 1) {
|
|
1506
|
+
this.addOrderUncertainty(order, "DUPLICATE_MEMBER");
|
|
1507
|
+
}
|
|
1508
|
+
const parentIdentity = order.parentOrderId ?? order.parentClientOrderId;
|
|
1509
|
+
if (parentIdentity === null)
|
|
1510
|
+
continue;
|
|
1511
|
+
const brokerMatches = byOrderId.get(parentIdentity) ?? [];
|
|
1512
|
+
const clientMatches = byClientId.get(parentIdentity) ?? [];
|
|
1513
|
+
const matches = new Set([...brokerMatches, ...clientMatches]);
|
|
1514
|
+
if (matches.size === 0)
|
|
1515
|
+
this.addOrderUncertainty(order, "MISSING_PARENT");
|
|
1516
|
+
if (matches.size > 1)
|
|
1517
|
+
this.addOrderUncertainty(order, "AMBIGUOUS_PARENT");
|
|
1518
|
+
}
|
|
1519
|
+
return normalized;
|
|
1520
|
+
});
|
|
1422
1521
|
}
|
|
1423
1522
|
async getDerivativeExecutions(input) {
|
|
1523
|
+
this.assertOpen();
|
|
1424
1524
|
if (!input.accountId.trim())
|
|
1425
1525
|
throw new Error("An exact account ID is required");
|
|
1426
1526
|
if (input.days !== undefined &&
|
|
@@ -1433,21 +1533,24 @@ export class IbkrClient {
|
|
|
1433
1533
|
if (input.clientOrderId !== undefined && !input.clientOrderId.trim()) {
|
|
1434
1534
|
throw new Error("Client order ID cannot be empty");
|
|
1435
1535
|
}
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1536
|
+
return this.withAccountCriticalSection(async () => {
|
|
1537
|
+
await this.prepareBrokerageAccount(input.accountId);
|
|
1538
|
+
const response = await this.req({
|
|
1539
|
+
path: "iserver/account/trades",
|
|
1540
|
+
...(input.days === undefined ? {} : { params: { days: input.days } }),
|
|
1541
|
+
});
|
|
1542
|
+
return response
|
|
1543
|
+
.filter((trade) => (trade.account ?? trade.accountCode) === input.accountId)
|
|
1544
|
+
.filter((trade) => input.orderId === undefined || String(trade.order_id) === input.orderId)
|
|
1545
|
+
.filter((trade) => input.clientOrderId === undefined || trade.order_ref === input.clientOrderId)
|
|
1546
|
+
.flatMap((trade) => {
|
|
1547
|
+
const execution = this.normalizeDerivativeExecution(input.accountId, trade);
|
|
1548
|
+
return execution === undefined ? [] : [execution];
|
|
1549
|
+
});
|
|
1448
1550
|
});
|
|
1449
1551
|
}
|
|
1450
1552
|
async reconcileDerivativeComboExecution(request) {
|
|
1553
|
+
this.assertOpen();
|
|
1451
1554
|
this.validateReconciliationRequest(request);
|
|
1452
1555
|
const lifecycle = await this.getDerivativeOrderStatus(request.accountId, request.orderId);
|
|
1453
1556
|
const deadline = this.now() + (request.timeoutMs ?? 30_000);
|
|
@@ -1474,24 +1577,22 @@ export class IbkrClient {
|
|
|
1474
1577
|
}
|
|
1475
1578
|
}
|
|
1476
1579
|
async cancelDerivativeOrder(input) {
|
|
1580
|
+
this.assertOpen();
|
|
1477
1581
|
if (!input.accountId.trim() || !input.orderId.trim()) {
|
|
1478
1582
|
throw new Error("Exact account and order IDs are required");
|
|
1479
1583
|
}
|
|
1480
1584
|
const cmeOperatorMetadata = this.cmeOperatorMetadata(input.assetClass, input);
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1585
|
+
return this.withTradingMutation(input.accountId, "IBKR brokerage session is not safely authenticated for cancellation", async () => {
|
|
1586
|
+
const response = await this.singleAttemptRequest({
|
|
1587
|
+
path: `iserver/account/${input.accountId}/order/${encodeURIComponent(input.orderId)}`,
|
|
1588
|
+
method: "DELETE",
|
|
1589
|
+
...(Object.keys(cmeOperatorMetadata).length > 0 ? { params: cmeOperatorMetadata } : {}),
|
|
1590
|
+
});
|
|
1591
|
+
return this.normalizeOrderCancellation(input, response);
|
|
1486
1592
|
});
|
|
1487
|
-
return {
|
|
1488
|
-
state: "requested",
|
|
1489
|
-
accountId: input.accountId,
|
|
1490
|
-
orderId: input.orderId,
|
|
1491
|
-
message: this.trimmedString(response.msg),
|
|
1492
|
-
};
|
|
1493
1593
|
}
|
|
1494
1594
|
async getAccountId() {
|
|
1595
|
+
this.assertOpen();
|
|
1495
1596
|
this.accountIdPromise ??= (async () => {
|
|
1496
1597
|
const override = process.env["IBKR_ACCOUNT_ID"];
|
|
1497
1598
|
if (override)
|
|
@@ -1505,6 +1606,7 @@ export class IbkrClient {
|
|
|
1505
1606
|
return this.accountIdPromise;
|
|
1506
1607
|
}
|
|
1507
1608
|
async getAccountBalances() {
|
|
1609
|
+
this.assertOpen();
|
|
1508
1610
|
const accountId = await this.getAccountId();
|
|
1509
1611
|
const summary = await this.req({
|
|
1510
1612
|
path: `portfolio/${accountId}/summary`,
|
|
@@ -1545,6 +1647,7 @@ export class IbkrClient {
|
|
|
1545
1647
|
};
|
|
1546
1648
|
}
|
|
1547
1649
|
async getPositions(symbol) {
|
|
1650
|
+
this.assertOpen();
|
|
1548
1651
|
const accountId = await this.getAccountId();
|
|
1549
1652
|
const rows = await this.fetchAllPositions(accountId);
|
|
1550
1653
|
for (const position of rows) {
|
|
@@ -1622,6 +1725,7 @@ export class IbkrClient {
|
|
|
1622
1725
|
};
|
|
1623
1726
|
}
|
|
1624
1727
|
async getQuotes(requests, options = {}) {
|
|
1728
|
+
this.assertOpen();
|
|
1625
1729
|
const unique = new Map();
|
|
1626
1730
|
for (const request of requests) {
|
|
1627
1731
|
if (!request.symbol.trim())
|
|
@@ -1680,6 +1784,7 @@ export class IbkrClient {
|
|
|
1680
1784
|
}
|
|
1681
1785
|
/** Resolve equity/ETF symbols to IBKR contracts via `trsrv/stocks`. */
|
|
1682
1786
|
async searchInstruments(symbol, projection = "symbol-search") {
|
|
1787
|
+
this.assertOpen();
|
|
1683
1788
|
if (projection !== "symbol-search" && projection !== "search") {
|
|
1684
1789
|
throw new Error(`IBKR search currently supports only symbol-search/search projections (got '${projection}').`);
|
|
1685
1790
|
}
|
|
@@ -1693,6 +1798,7 @@ export class IbkrClient {
|
|
|
1693
1798
|
return (response[query] ?? []).flatMap((listing) => this.normalizeStockListing(query, listing));
|
|
1694
1799
|
}
|
|
1695
1800
|
async fetchTransactionHistory(startDate, endDate) {
|
|
1801
|
+
this.assertOpen();
|
|
1696
1802
|
const accountId = await this.getAccountId();
|
|
1697
1803
|
const rows = await this.fetchAllPositions(accountId);
|
|
1698
1804
|
const positionsByConid = new Map(rows
|
|
@@ -1722,25 +1828,28 @@ export class IbkrClient {
|
|
|
1722
1828
|
return [{ accountNumber: accountId, transactions: [...transactionsByKey.values()] }];
|
|
1723
1829
|
}
|
|
1724
1830
|
async fetchOrders(options) {
|
|
1831
|
+
this.assertOpen();
|
|
1725
1832
|
const accountId = await this.getAccountId();
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1833
|
+
return this.withAccountCriticalSection(async () => {
|
|
1834
|
+
await this.prepareBrokerageAccount(accountId);
|
|
1835
|
+
const params = {};
|
|
1836
|
+
if (options.status && options.status.toUpperCase() !== "WORKING") {
|
|
1837
|
+
params["filters"] = this.ibkrStatusFilter(options.status);
|
|
1838
|
+
}
|
|
1839
|
+
const response = await this.req({
|
|
1840
|
+
path: "iserver/account/orders",
|
|
1841
|
+
params,
|
|
1842
|
+
});
|
|
1843
|
+
let orders = (response.orders ?? [])
|
|
1844
|
+
.filter((order) => this.orderBelongsToAccount(order, accountId))
|
|
1845
|
+
.map((order) => this.normalizeOrder(order))
|
|
1846
|
+
.filter((order) => this.orderMatchesStatus(order, options.status))
|
|
1847
|
+
.filter((order) => this.orderInDateRange(order, options.fromEnteredTime, options.toEnteredTime))
|
|
1848
|
+
.sort((left, right) => this.orderTimeMs(right) - this.orderTimeMs(left));
|
|
1849
|
+
if (options.maxResults !== undefined)
|
|
1850
|
+
orders = orders.slice(0, options.maxResults);
|
|
1851
|
+
return [{ accountNumber: accountId, orders }];
|
|
1734
1852
|
});
|
|
1735
|
-
let orders = (response.orders ?? [])
|
|
1736
|
-
.filter((order) => this.orderBelongsToAccount(order, accountId))
|
|
1737
|
-
.map((order) => this.normalizeOrder(order))
|
|
1738
|
-
.filter((order) => this.orderMatchesStatus(order, options.status))
|
|
1739
|
-
.filter((order) => this.orderInDateRange(order, options.fromEnteredTime, options.toEnteredTime))
|
|
1740
|
-
.sort((left, right) => this.orderTimeMs(right) - this.orderTimeMs(left));
|
|
1741
|
-
if (options.maxResults !== undefined)
|
|
1742
|
-
orders = orders.slice(0, options.maxResults);
|
|
1743
|
-
return [{ accountNumber: accountId, orders }];
|
|
1744
1853
|
}
|
|
1745
1854
|
validateComboPreview(request) {
|
|
1746
1855
|
if (!request.accountId.trim())
|
|
@@ -2287,7 +2396,172 @@ export class IbkrClient {
|
|
|
2287
2396
|
],
|
|
2288
2397
|
};
|
|
2289
2398
|
}
|
|
2290
|
-
|
|
2399
|
+
normalizeOrderCancellation(input, response) {
|
|
2400
|
+
const record = isUnknownRecord(response) ? response : null;
|
|
2401
|
+
const message = this.trimmedString(record?.["msg"]);
|
|
2402
|
+
const accountId = this.trimmedString(record?.["account"]);
|
|
2403
|
+
const orderId = this.cancellationOrderId(record?.["order_id"]);
|
|
2404
|
+
const errorParts = record === null ? [] : this.cancellationErrorParts(record);
|
|
2405
|
+
const evidence = {
|
|
2406
|
+
message,
|
|
2407
|
+
accountId,
|
|
2408
|
+
orderId,
|
|
2409
|
+
error: errorParts.length > 0 ? errorParts.join("; ").slice(0, 4_096) : null,
|
|
2410
|
+
response: this.sanitizeJsonEvidence(response),
|
|
2411
|
+
};
|
|
2412
|
+
const accountProvided = record !== null && "account" in record;
|
|
2413
|
+
const orderProvided = record !== null && "order_id" in record;
|
|
2414
|
+
const conidProvided = record !== null && "conid" in record;
|
|
2415
|
+
const conid = record?.["conid"];
|
|
2416
|
+
const unknownFields = record === null
|
|
2417
|
+
? []
|
|
2418
|
+
: Object.keys(record).filter((key) => key !== "msg" && key !== "account" && key !== "order_id" && key !== "conid");
|
|
2419
|
+
let reason = null;
|
|
2420
|
+
if (record === null)
|
|
2421
|
+
reason = "IBKR returned a malformed cancellation response";
|
|
2422
|
+
else if (errorParts.length > 0)
|
|
2423
|
+
reason = "IBKR returned cancellation error evidence";
|
|
2424
|
+
else if (unknownFields.length > 0)
|
|
2425
|
+
reason = "IBKR returned undocumented cancellation fields";
|
|
2426
|
+
else if (message !== "Request was submitted")
|
|
2427
|
+
reason = "IBKR did not confirm the cancellation request";
|
|
2428
|
+
else if (accountProvided && accountId === null)
|
|
2429
|
+
reason = "IBKR returned malformed cancellation account evidence";
|
|
2430
|
+
else if (orderProvided && orderId === null)
|
|
2431
|
+
reason = "IBKR returned malformed cancellation order evidence";
|
|
2432
|
+
else if (conidProvided &&
|
|
2433
|
+
(typeof conid !== "number" || !Number.isSafeInteger(conid) || conid <= 0))
|
|
2434
|
+
reason = "IBKR returned malformed cancellation conid evidence";
|
|
2435
|
+
else if (accountId !== null && accountId !== input.accountId)
|
|
2436
|
+
reason = "IBKR cancellation account evidence conflicts with the request";
|
|
2437
|
+
else if (orderId !== null && orderId !== input.orderId)
|
|
2438
|
+
reason = "IBKR cancellation order evidence conflicts with the request";
|
|
2439
|
+
if (reason !== null || message === null) {
|
|
2440
|
+
return {
|
|
2441
|
+
state: "recovery_required",
|
|
2442
|
+
accountId: input.accountId,
|
|
2443
|
+
orderId: input.orderId,
|
|
2444
|
+
reason: reason ?? "IBKR did not confirm the cancellation request",
|
|
2445
|
+
evidence,
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
return {
|
|
2449
|
+
state: "requested",
|
|
2450
|
+
accountId: input.accountId,
|
|
2451
|
+
orderId: input.orderId,
|
|
2452
|
+
message,
|
|
2453
|
+
};
|
|
2454
|
+
}
|
|
2455
|
+
sanitizeJsonEvidence(value) {
|
|
2456
|
+
const seen = new WeakMap();
|
|
2457
|
+
let remainingEntries = 500;
|
|
2458
|
+
const dictionary = () => Object.create(null);
|
|
2459
|
+
const markerKey = (source, result, label) => {
|
|
2460
|
+
let key = label;
|
|
2461
|
+
while (Object.prototype.hasOwnProperty.call(source, key) ||
|
|
2462
|
+
Object.prototype.hasOwnProperty.call(result, key)) {
|
|
2463
|
+
key += "#";
|
|
2464
|
+
}
|
|
2465
|
+
return key;
|
|
2466
|
+
};
|
|
2467
|
+
const sanitize = (item, depth, path) => {
|
|
2468
|
+
if (item === null || typeof item === "boolean" || typeof item === "string")
|
|
2469
|
+
return item;
|
|
2470
|
+
if (typeof item === "number") {
|
|
2471
|
+
return Number.isFinite(item) ? item : `[non-json number: ${String(item)}]`;
|
|
2472
|
+
}
|
|
2473
|
+
if (typeof item !== "object")
|
|
2474
|
+
return `[non-json ${typeof item}]`;
|
|
2475
|
+
const priorPath = seen.get(item);
|
|
2476
|
+
if (priorPath !== undefined)
|
|
2477
|
+
return `[reference: ${priorPath}]`;
|
|
2478
|
+
seen.set(item, path);
|
|
2479
|
+
if (depth >= 8) {
|
|
2480
|
+
const result = dictionary();
|
|
2481
|
+
result[markerKey(item, result, "[truncated: depth]")] = true;
|
|
2482
|
+
return result;
|
|
2483
|
+
}
|
|
2484
|
+
if (Array.isArray(item)) {
|
|
2485
|
+
const result = [];
|
|
2486
|
+
for (const [index, member] of item.entries()) {
|
|
2487
|
+
if (remainingEntries <= 0) {
|
|
2488
|
+
result.push("[truncated: entry count]");
|
|
2489
|
+
break;
|
|
2490
|
+
}
|
|
2491
|
+
remainingEntries -= 1;
|
|
2492
|
+
result.push(sanitize(member, depth + 1, `${path}[${String(index)}]`));
|
|
2493
|
+
}
|
|
2494
|
+
return result;
|
|
2495
|
+
}
|
|
2496
|
+
const result = dictionary();
|
|
2497
|
+
for (const ownKey of Reflect.ownKeys(item)) {
|
|
2498
|
+
if (remainingEntries <= 0) {
|
|
2499
|
+
result[markerKey(item, result, "[truncated: entry count]")] = true;
|
|
2500
|
+
break;
|
|
2501
|
+
}
|
|
2502
|
+
remainingEntries -= 1;
|
|
2503
|
+
const key = typeof ownKey === "string"
|
|
2504
|
+
? ownKey
|
|
2505
|
+
: markerKey(item, result, `[non-json symbol key: ${ownKey.description ?? ""}]`);
|
|
2506
|
+
let member;
|
|
2507
|
+
try {
|
|
2508
|
+
member = Reflect.get(item, ownKey);
|
|
2509
|
+
}
|
|
2510
|
+
catch {
|
|
2511
|
+
member = "[unreadable property]";
|
|
2512
|
+
}
|
|
2513
|
+
result[key] = sanitize(member, depth + 1, `${path}.${JSON.stringify(key)}`);
|
|
2514
|
+
}
|
|
2515
|
+
return result;
|
|
2516
|
+
};
|
|
2517
|
+
const sanitized = sanitize(value, 0, "$");
|
|
2518
|
+
const encoded = JSON.stringify(sanitized);
|
|
2519
|
+
if (encoded.length <= 8_192)
|
|
2520
|
+
return sanitized;
|
|
2521
|
+
const fallback = dictionary();
|
|
2522
|
+
fallback["[truncated: evidence size]"] = true;
|
|
2523
|
+
fallback["originalSerializedLength"] = encoded.length;
|
|
2524
|
+
let low = 0;
|
|
2525
|
+
let high = encoded.length;
|
|
2526
|
+
while (low < high) {
|
|
2527
|
+
const middle = Math.ceil((low + high) / 2);
|
|
2528
|
+
fallback["preview"] = encoded.slice(0, middle);
|
|
2529
|
+
if (JSON.stringify(fallback).length <= 8_192)
|
|
2530
|
+
low = middle;
|
|
2531
|
+
else
|
|
2532
|
+
high = middle - 1;
|
|
2533
|
+
}
|
|
2534
|
+
fallback["preview"] = encoded.slice(0, low);
|
|
2535
|
+
return fallback;
|
|
2536
|
+
}
|
|
2537
|
+
cancellationOrderId(value) {
|
|
2538
|
+
if (typeof value === "string")
|
|
2539
|
+
return this.trimmedString(value);
|
|
2540
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0
|
|
2541
|
+
? String(value)
|
|
2542
|
+
: null;
|
|
2543
|
+
}
|
|
2544
|
+
cancellationErrorParts(record) {
|
|
2545
|
+
const parts = [];
|
|
2546
|
+
for (const key of ["error", "message", "text"]) {
|
|
2547
|
+
if (!(key in record))
|
|
2548
|
+
continue;
|
|
2549
|
+
const text = this.trimmedString(record[key]);
|
|
2550
|
+
parts.push(text === null ? `${key}: present` : `${key}: ${text}`);
|
|
2551
|
+
}
|
|
2552
|
+
for (const key of ["statusCode", "code"]) {
|
|
2553
|
+
if (!(key in record))
|
|
2554
|
+
continue;
|
|
2555
|
+
const value = record[key];
|
|
2556
|
+
parts.push(typeof value === "string" || typeof value === "number"
|
|
2557
|
+
? `${key}: ${String(value)}`
|
|
2558
|
+
: `${key}: present`);
|
|
2559
|
+
}
|
|
2560
|
+
if (record["success"] === false)
|
|
2561
|
+
parts.push("success: false");
|
|
2562
|
+
return parts;
|
|
2563
|
+
}
|
|
2564
|
+
normalizeMultiOrderSubmission(response, parentClientOrderId, accountId) {
|
|
2291
2565
|
const decoded = this.decodeOrderSubmission(response);
|
|
2292
2566
|
const hasDistinctBrokerOrderIds = decoded.orders.length === 2 &&
|
|
2293
2567
|
new Set(decoded.orders.map(({ orderId }) => orderId)).size === 2;
|
|
@@ -2314,7 +2588,7 @@ export class IbkrClient {
|
|
|
2314
2588
|
return {
|
|
2315
2589
|
state: "warning",
|
|
2316
2590
|
warnings: decoded.warnings,
|
|
2317
|
-
continuation: { replyId: warning.replyId, parentClientOrderId },
|
|
2591
|
+
continuation: { accountId, replyId: warning.replyId, parentClientOrderId },
|
|
2318
2592
|
};
|
|
2319
2593
|
}
|
|
2320
2594
|
if (hasErrors &&
|
|
@@ -3147,15 +3421,57 @@ export class IbkrClient {
|
|
|
3147
3421
|
const parsed = Number(match[0].replace(/,/g, ""));
|
|
3148
3422
|
return Number.isFinite(parsed) ? parsed : null;
|
|
3149
3423
|
}
|
|
3424
|
+
withAccountCriticalSection(operation) {
|
|
3425
|
+
this.assertOpen();
|
|
3426
|
+
const result = this.accountCriticalSectionTail.then(async () => {
|
|
3427
|
+
this.assertOpen();
|
|
3428
|
+
return operation();
|
|
3429
|
+
});
|
|
3430
|
+
this.accountCriticalSectionTail = result.then(() => undefined, () => undefined);
|
|
3431
|
+
return result;
|
|
3432
|
+
}
|
|
3433
|
+
withTradingMutation(accountId, unsafeMessage, operation) {
|
|
3434
|
+
return this.withAccountCriticalSection(async () => {
|
|
3435
|
+
const diagnostics = await this.getTradingDiagnostics(accountId);
|
|
3436
|
+
if (!this.isSafeForTradingMutation(diagnostics))
|
|
3437
|
+
throw new Error(unsafeMessage);
|
|
3438
|
+
await this.prepareBrokerageAccount(accountId);
|
|
3439
|
+
return operation(diagnostics);
|
|
3440
|
+
});
|
|
3441
|
+
}
|
|
3442
|
+
isSafeForTradingMutation(diagnostics) {
|
|
3443
|
+
return (diagnostics.authenticated === true &&
|
|
3444
|
+
diagnostics.connected === true &&
|
|
3445
|
+
diagnostics.competingSession === false &&
|
|
3446
|
+
diagnostics.environment !== null);
|
|
3447
|
+
}
|
|
3448
|
+
assertOpen() {
|
|
3449
|
+
if (this.closed)
|
|
3450
|
+
throw this.closedError();
|
|
3451
|
+
}
|
|
3452
|
+
closedError() {
|
|
3453
|
+
return new Error("This IBKR client is closed");
|
|
3454
|
+
}
|
|
3455
|
+
booleanOrNull(value) {
|
|
3456
|
+
return typeof value === "boolean" ? value : null;
|
|
3457
|
+
}
|
|
3458
|
+
accountIdsOrNull(value) {
|
|
3459
|
+
return Array.isArray(value) &&
|
|
3460
|
+
Array.from(value).every((accountId) => typeof accountId === "string")
|
|
3461
|
+
? value
|
|
3462
|
+
: null;
|
|
3463
|
+
}
|
|
3150
3464
|
async prepareBrokerageAccount(accountId) {
|
|
3151
|
-
const
|
|
3465
|
+
const rawBrokerageAccounts = await this.req({
|
|
3152
3466
|
path: "iserver/accounts",
|
|
3153
3467
|
});
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
if (
|
|
3468
|
+
const brokerageAccounts = isUnknownRecord(rawBrokerageAccounts) ? rawBrokerageAccounts : {};
|
|
3469
|
+
const accountIds = this.accountIdsOrNull(brokerageAccounts["accounts"]);
|
|
3470
|
+
if (!accountIds?.includes(accountId)) {
|
|
3157
3471
|
throw new Error(`IBKR account ${accountId} is not available for trading/order queries.`);
|
|
3158
3472
|
}
|
|
3473
|
+
if (brokerageAccounts["selectedAccount"] === accountId)
|
|
3474
|
+
return;
|
|
3159
3475
|
const switchedAccount = await this.singleAttemptRequest({
|
|
3160
3476
|
path: "iserver/account",
|
|
3161
3477
|
method: "POST",
|
|
@@ -3294,6 +3610,7 @@ export class IbkrClient {
|
|
|
3294
3610
|
}
|
|
3295
3611
|
/** Return complete daily history with the exact validated IBKR request context. */
|
|
3296
3612
|
async getPriceHistory(input) {
|
|
3613
|
+
this.assertOpen();
|
|
3297
3614
|
const requestedSymbol = input.symbol.trim().toUpperCase();
|
|
3298
3615
|
if (!requestedSymbol) {
|
|
3299
3616
|
throw new IbkrPriceHistoryContractError("Price history requires a symbol", "CONTRACT_INVALID");
|
|
@@ -3444,6 +3761,7 @@ export class IbkrClient {
|
|
|
3444
3761
|
}
|
|
3445
3762
|
/** Discover listed derivative series over an inclusive calendar range. */
|
|
3446
3763
|
async getDerivativeExpiries(query) {
|
|
3764
|
+
this.assertOpen();
|
|
3447
3765
|
const contracts = [];
|
|
3448
3766
|
for (const month of monthCodes(query.from, query.to)) {
|
|
3449
3767
|
contracts.push(...(await this.discoverDerivativeMonth(query.underlying, query.assetClass, month, query.exchange, query.right)));
|
|
@@ -3477,6 +3795,7 @@ export class IbkrClient {
|
|
|
3477
3795
|
}
|
|
3478
3796
|
/** Discover contracts for one exact expiration, preserving class and venue identity. */
|
|
3479
3797
|
async getDerivativeContracts(query) {
|
|
3798
|
+
this.assertOpen();
|
|
3480
3799
|
const tradingClass = query.tradingClass?.trim().toUpperCase();
|
|
3481
3800
|
return (await this.discoverDerivativeMonth(query.underlying, query.assetClass, monthCode(query.expiration), query.exchange, query.right, query.strike)).filter((contract) => contract.expiration === query.expiration &&
|
|
3482
3801
|
(query.right === undefined || contract.right === query.right) &&
|
|
@@ -3485,6 +3804,7 @@ export class IbkrClient {
|
|
|
3485
3804
|
}
|
|
3486
3805
|
/** Resolve exactly one contract and reject missing or ambiguous semantic identity. */
|
|
3487
3806
|
async resolveDerivativeContract(query) {
|
|
3807
|
+
this.assertOpen();
|
|
3488
3808
|
const contracts = await this.getDerivativeContracts(query);
|
|
3489
3809
|
if (!contracts.length) {
|
|
3490
3810
|
throw new Error(`IBKR returned no exact ${query.assetClass} contract for ${query.underlying} ${query.expiration} ${query.right}${String(query.strike)}`);
|
|
@@ -3500,6 +3820,7 @@ export class IbkrClient {
|
|
|
3500
3820
|
}
|
|
3501
3821
|
/** Return an exact-expiration derivative chain with explicit data availability. */
|
|
3502
3822
|
async getDerivativeChain(query) {
|
|
3823
|
+
this.assertOpen();
|
|
3503
3824
|
const contracts = await this.getDerivativeContracts(query);
|
|
3504
3825
|
if (!contracts.length) {
|
|
3505
3826
|
throw new Error(`IBKR returned no ${query.assetClass} contracts for ${query.underlying} ${query.expiration}`);
|
|
@@ -3512,6 +3833,7 @@ export class IbkrClient {
|
|
|
3512
3833
|
}
|
|
3513
3834
|
/** Quote the broker-linked underlying (for example, the Sep NQ future behind QN3). */
|
|
3514
3835
|
async getDerivativeReferenceQuote(contract) {
|
|
3836
|
+
this.assertOpen();
|
|
3515
3837
|
const detailResponse = await this.req({
|
|
3516
3838
|
path: "trsrv/secdef",
|
|
3517
3839
|
params: { conids: String(contract.conid) },
|
|
@@ -3558,6 +3880,7 @@ export class IbkrClient {
|
|
|
3558
3880
|
}
|
|
3559
3881
|
/** Discover every listed weekly/monthly expiry in the requested calendar range. */
|
|
3560
3882
|
async getOptionExpiries(symbol, right, fromDate, toDate, options = {}) {
|
|
3883
|
+
this.assertOpen();
|
|
3561
3884
|
const normalized = symbol.trim().toUpperCase();
|
|
3562
3885
|
const months = monthCodes(fromDate, toDate);
|
|
3563
3886
|
const contracts = [];
|
|
@@ -3573,6 +3896,7 @@ export class IbkrClient {
|
|
|
3573
3896
|
}
|
|
3574
3897
|
/** Build one exact-expiry chain with canonical OSI symbols and required pricing/greeks. */
|
|
3575
3898
|
async getOptionChain(symbol, expiry, right, options = {}) {
|
|
3899
|
+
this.assertOpen();
|
|
3576
3900
|
const month = monthCode(expiry);
|
|
3577
3901
|
const normalized = symbol.trim().toUpperCase();
|
|
3578
3902
|
const discovery = await this.discoverOptions(normalized, month, right, options);
|
|
@@ -3592,6 +3916,7 @@ export class IbkrClient {
|
|
|
3592
3916
|
}
|
|
3593
3917
|
/** Return every qualified contract for one exact expiry and side without hiding sparse data. */
|
|
3594
3918
|
async getOptionChainSnapshot(symbol, expiry, right, options = {}) {
|
|
3919
|
+
this.assertOpen();
|
|
3595
3920
|
const month = monthCode(expiry);
|
|
3596
3921
|
const normalized = symbol.trim().toUpperCase();
|
|
3597
3922
|
const discovery = await this.discoverOptions(normalized, month, right, options);
|
|
@@ -3603,6 +3928,7 @@ export class IbkrClient {
|
|
|
3603
3928
|
}
|
|
3604
3929
|
/** Fetch one exact option quote; null means the contract is not listed. */
|
|
3605
3930
|
async getOptionQuote(input) {
|
|
3931
|
+
this.assertOpen();
|
|
3606
3932
|
const contract = await this.resolveOptionContract(input);
|
|
3607
3933
|
if (!contract)
|
|
3608
3934
|
return null;
|
|
@@ -3610,6 +3936,7 @@ export class IbkrClient {
|
|
|
3610
3936
|
}
|
|
3611
3937
|
/** Resolve a conid back into the canonical OSI-bearing option contract. */
|
|
3612
3938
|
async getOptionContract(conid) {
|
|
3939
|
+
this.assertOpen();
|
|
3613
3940
|
const response = await this.req({
|
|
3614
3941
|
path: "trsrv/secdef",
|
|
3615
3942
|
params: { conids: String(conid) },
|
|
@@ -4984,6 +5311,8 @@ export class IbkrClient {
|
|
|
4984
5311
|
return this.scheduledRequest(input, "SINGLE_ATTEMPT");
|
|
4985
5312
|
}
|
|
4986
5313
|
scheduledRequest(input, retryPolicy, signal, onTerminalFailure) {
|
|
5314
|
+
if (this.closed)
|
|
5315
|
+
return Promise.reject(this.closedError());
|
|
4987
5316
|
return this.requestScheduler.schedule({
|
|
4988
5317
|
endpoint: this.requestEndpoint(input.path),
|
|
4989
5318
|
priority: this.requestPriority(input.path),
|