@huskly/ibkr-client 1.6.0 → 2.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 +71 -13
- package/dist/ibkr/ibkrApiTypes.d.ts +19 -2
- package/dist/ibkr/ibkrApiTypes.d.ts.map +1 -1
- package/dist/ibkr/ibkrClient.d.ts +74 -3
- package/dist/ibkr/ibkrClient.d.ts.map +1 -1
- package/dist/ibkr/ibkrClient.js +790 -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 +124 -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`,
|
|
@@ -1544,7 +1646,106 @@ export class IbkrClient {
|
|
|
1544
1646
|
},
|
|
1545
1647
|
};
|
|
1546
1648
|
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Read one settled-cash observation of the account from the same account
|
|
1651
|
+
* summary endpoint {@link getAccountBalances} uses.
|
|
1652
|
+
*
|
|
1653
|
+
* The observation names the account, states the currency IBKR gave for each
|
|
1654
|
+
* figure, and carries one client-minted timestamp. A figure that is absent,
|
|
1655
|
+
* null, non-finite, or not convertible reads `null`, and a missing currency
|
|
1656
|
+
* reads `null`. A currency is never inferred and never defaulted to `"USD"`.
|
|
1657
|
+
* One missing field never makes this method throw.
|
|
1658
|
+
*
|
|
1659
|
+
* Settled cash is reported as evidence only, from the live `settledcashbydate`
|
|
1660
|
+
* string field. This package refuses nothing and infers nothing: the CONSUMER,
|
|
1661
|
+
* not this package, decides which dates count as settled, and a date after the
|
|
1662
|
+
* observation date is not settled cash.
|
|
1663
|
+
*/
|
|
1664
|
+
async getAccountSettlementEvidence() {
|
|
1665
|
+
this.assertOpen();
|
|
1666
|
+
const accountId = await this.getAccountId();
|
|
1667
|
+
const summary = await this.req({
|
|
1668
|
+
path: `portfolio/${accountId}/summary`,
|
|
1669
|
+
});
|
|
1670
|
+
return {
|
|
1671
|
+
accountId,
|
|
1672
|
+
observedAtEpochMillis: this.now(),
|
|
1673
|
+
settledCashByDate: this.settledCashByDate(summary),
|
|
1674
|
+
settledCashByDateRaw: this.settledCashByDateRaw(summary),
|
|
1675
|
+
availableFunds: this.settlementFigure(summary, "availablefunds"),
|
|
1676
|
+
totalCashValue: this.settlementFigure(summary, "totalcashvalue"),
|
|
1677
|
+
accruedCash: this.settlementFigure(summary, "accruedcash"),
|
|
1678
|
+
excessLiquidity: this.settlementFigure(summary, "excessliquidity"),
|
|
1679
|
+
buyingPower: this.settlementFigure(summary, "buyingpower"),
|
|
1680
|
+
netLiquidation: this.settlementFigure(summary, "netliquidation"),
|
|
1681
|
+
accountType: this.summaryFieldValue(summary, "accounttype"),
|
|
1682
|
+
tradingType: this.summaryFieldValue(summary, "tradingtype-s"),
|
|
1683
|
+
presentSummaryFieldNames: this.presentSummaryFieldNames(summary),
|
|
1684
|
+
};
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* The exact `settledcashbydate` `value` string, unparsed. `null` when the key
|
|
1688
|
+
* is absent, is not an object, or carries no usable string.
|
|
1689
|
+
*/
|
|
1690
|
+
settledCashByDateRaw(summary) {
|
|
1691
|
+
return this.summaryFieldValue(summary, "settledcashbydate");
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Parse the `settledcashbydate` `value` string into one entry for each
|
|
1695
|
+
* `YYYYMMDD:amount` pair, in the order the broker wrote them. Several pairs
|
|
1696
|
+
* may share one string, separated by `;` or `,`. A comma is always a pair
|
|
1697
|
+
* separator here, never a thousands separator. A pair that does not carry
|
|
1698
|
+
* an eight-digit date and a usable amount is skipped, never guessed; the raw
|
|
1699
|
+
* string still reports it. This is evidence only: which dates count as
|
|
1700
|
+
* settled is the consumer's decision, and a date after the observation date
|
|
1701
|
+
* is not settled cash.
|
|
1702
|
+
*/
|
|
1703
|
+
settledCashByDate(summary) {
|
|
1704
|
+
const raw = this.settledCashByDateRaw(summary);
|
|
1705
|
+
if (raw === null)
|
|
1706
|
+
return [];
|
|
1707
|
+
const out = [];
|
|
1708
|
+
for (const pair of raw.split(/[;,]/)) {
|
|
1709
|
+
const separator = pair.indexOf(":");
|
|
1710
|
+
if (separator < 0)
|
|
1711
|
+
continue;
|
|
1712
|
+
const settlementDate = pair.slice(0, separator).trim();
|
|
1713
|
+
if (!/^\d{8}$/.test(settlementDate))
|
|
1714
|
+
continue;
|
|
1715
|
+
const amount = toNullableNumber(pair.slice(separator + 1).trim());
|
|
1716
|
+
if (amount === null)
|
|
1717
|
+
continue;
|
|
1718
|
+
out.push({ settlementDate, amount });
|
|
1719
|
+
}
|
|
1720
|
+
return out;
|
|
1721
|
+
}
|
|
1722
|
+
/** The `value` string of one summary key, or `null` when it is unusable. */
|
|
1723
|
+
summaryFieldValue(summary, key) {
|
|
1724
|
+
const field = summary[key];
|
|
1725
|
+
if (field === null || typeof field !== "object")
|
|
1726
|
+
return null;
|
|
1727
|
+
return this.trimmedString(field.value);
|
|
1728
|
+
}
|
|
1729
|
+
/** Narrow one summary key into an amount and the currency IBKR stated for it. */
|
|
1730
|
+
settlementFigure(summary, key) {
|
|
1731
|
+
const field = summary[key];
|
|
1732
|
+
if (field === null || typeof field !== "object")
|
|
1733
|
+
return { amount: null, currency: null };
|
|
1734
|
+
const record = field;
|
|
1735
|
+
return {
|
|
1736
|
+
amount: toNullableNumber(record.amount),
|
|
1737
|
+
currency: this.trimmedString(record.currency),
|
|
1738
|
+
};
|
|
1739
|
+
}
|
|
1740
|
+
/** The sorted key names present in the summary response. Names only, never values. */
|
|
1741
|
+
presentSummaryFieldNames(summary) {
|
|
1742
|
+
const record = summary;
|
|
1743
|
+
if (record === null || typeof record !== "object")
|
|
1744
|
+
return [];
|
|
1745
|
+
return Object.keys(record).sort();
|
|
1746
|
+
}
|
|
1547
1747
|
async getPositions(symbol) {
|
|
1748
|
+
this.assertOpen();
|
|
1548
1749
|
const accountId = await this.getAccountId();
|
|
1549
1750
|
const rows = await this.fetchAllPositions(accountId);
|
|
1550
1751
|
for (const position of rows) {
|
|
@@ -1622,6 +1823,7 @@ export class IbkrClient {
|
|
|
1622
1823
|
};
|
|
1623
1824
|
}
|
|
1624
1825
|
async getQuotes(requests, options = {}) {
|
|
1826
|
+
this.assertOpen();
|
|
1625
1827
|
const unique = new Map();
|
|
1626
1828
|
for (const request of requests) {
|
|
1627
1829
|
if (!request.symbol.trim())
|
|
@@ -1680,6 +1882,7 @@ export class IbkrClient {
|
|
|
1680
1882
|
}
|
|
1681
1883
|
/** Resolve equity/ETF symbols to IBKR contracts via `trsrv/stocks`. */
|
|
1682
1884
|
async searchInstruments(symbol, projection = "symbol-search") {
|
|
1885
|
+
this.assertOpen();
|
|
1683
1886
|
if (projection !== "symbol-search" && projection !== "search") {
|
|
1684
1887
|
throw new Error(`IBKR search currently supports only symbol-search/search projections (got '${projection}').`);
|
|
1685
1888
|
}
|
|
@@ -1693,6 +1896,7 @@ export class IbkrClient {
|
|
|
1693
1896
|
return (response[query] ?? []).flatMap((listing) => this.normalizeStockListing(query, listing));
|
|
1694
1897
|
}
|
|
1695
1898
|
async fetchTransactionHistory(startDate, endDate) {
|
|
1899
|
+
this.assertOpen();
|
|
1696
1900
|
const accountId = await this.getAccountId();
|
|
1697
1901
|
const rows = await this.fetchAllPositions(accountId);
|
|
1698
1902
|
const positionsByConid = new Map(rows
|
|
@@ -1722,25 +1926,28 @@ export class IbkrClient {
|
|
|
1722
1926
|
return [{ accountNumber: accountId, transactions: [...transactionsByKey.values()] }];
|
|
1723
1927
|
}
|
|
1724
1928
|
async fetchOrders(options) {
|
|
1929
|
+
this.assertOpen();
|
|
1725
1930
|
const accountId = await this.getAccountId();
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1931
|
+
return this.withAccountCriticalSection(async () => {
|
|
1932
|
+
await this.prepareBrokerageAccount(accountId);
|
|
1933
|
+
const params = {};
|
|
1934
|
+
if (options.status && options.status.toUpperCase() !== "WORKING") {
|
|
1935
|
+
params["filters"] = this.ibkrStatusFilter(options.status);
|
|
1936
|
+
}
|
|
1937
|
+
const response = await this.req({
|
|
1938
|
+
path: "iserver/account/orders",
|
|
1939
|
+
params,
|
|
1940
|
+
});
|
|
1941
|
+
let orders = (response.orders ?? [])
|
|
1942
|
+
.filter((order) => this.orderBelongsToAccount(order, accountId))
|
|
1943
|
+
.map((order) => this.normalizeOrder(order))
|
|
1944
|
+
.filter((order) => this.orderMatchesStatus(order, options.status))
|
|
1945
|
+
.filter((order) => this.orderInDateRange(order, options.fromEnteredTime, options.toEnteredTime))
|
|
1946
|
+
.sort((left, right) => this.orderTimeMs(right) - this.orderTimeMs(left));
|
|
1947
|
+
if (options.maxResults !== undefined)
|
|
1948
|
+
orders = orders.slice(0, options.maxResults);
|
|
1949
|
+
return [{ accountNumber: accountId, orders }];
|
|
1734
1950
|
});
|
|
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
1951
|
}
|
|
1745
1952
|
validateComboPreview(request) {
|
|
1746
1953
|
if (!request.accountId.trim())
|
|
@@ -2287,7 +2494,172 @@ export class IbkrClient {
|
|
|
2287
2494
|
],
|
|
2288
2495
|
};
|
|
2289
2496
|
}
|
|
2290
|
-
|
|
2497
|
+
normalizeOrderCancellation(input, response) {
|
|
2498
|
+
const record = isUnknownRecord(response) ? response : null;
|
|
2499
|
+
const message = this.trimmedString(record?.["msg"]);
|
|
2500
|
+
const accountId = this.trimmedString(record?.["account"]);
|
|
2501
|
+
const orderId = this.cancellationOrderId(record?.["order_id"]);
|
|
2502
|
+
const errorParts = record === null ? [] : this.cancellationErrorParts(record);
|
|
2503
|
+
const evidence = {
|
|
2504
|
+
message,
|
|
2505
|
+
accountId,
|
|
2506
|
+
orderId,
|
|
2507
|
+
error: errorParts.length > 0 ? errorParts.join("; ").slice(0, 4_096) : null,
|
|
2508
|
+
response: this.sanitizeJsonEvidence(response),
|
|
2509
|
+
};
|
|
2510
|
+
const accountProvided = record !== null && "account" in record;
|
|
2511
|
+
const orderProvided = record !== null && "order_id" in record;
|
|
2512
|
+
const conidProvided = record !== null && "conid" in record;
|
|
2513
|
+
const conid = record?.["conid"];
|
|
2514
|
+
const unknownFields = record === null
|
|
2515
|
+
? []
|
|
2516
|
+
: Object.keys(record).filter((key) => key !== "msg" && key !== "account" && key !== "order_id" && key !== "conid");
|
|
2517
|
+
let reason = null;
|
|
2518
|
+
if (record === null)
|
|
2519
|
+
reason = "IBKR returned a malformed cancellation response";
|
|
2520
|
+
else if (errorParts.length > 0)
|
|
2521
|
+
reason = "IBKR returned cancellation error evidence";
|
|
2522
|
+
else if (unknownFields.length > 0)
|
|
2523
|
+
reason = "IBKR returned undocumented cancellation fields";
|
|
2524
|
+
else if (message !== "Request was submitted")
|
|
2525
|
+
reason = "IBKR did not confirm the cancellation request";
|
|
2526
|
+
else if (accountProvided && accountId === null)
|
|
2527
|
+
reason = "IBKR returned malformed cancellation account evidence";
|
|
2528
|
+
else if (orderProvided && orderId === null)
|
|
2529
|
+
reason = "IBKR returned malformed cancellation order evidence";
|
|
2530
|
+
else if (conidProvided &&
|
|
2531
|
+
(typeof conid !== "number" || !Number.isSafeInteger(conid) || conid <= 0))
|
|
2532
|
+
reason = "IBKR returned malformed cancellation conid evidence";
|
|
2533
|
+
else if (accountId !== null && accountId !== input.accountId)
|
|
2534
|
+
reason = "IBKR cancellation account evidence conflicts with the request";
|
|
2535
|
+
else if (orderId !== null && orderId !== input.orderId)
|
|
2536
|
+
reason = "IBKR cancellation order evidence conflicts with the request";
|
|
2537
|
+
if (reason !== null || message === null) {
|
|
2538
|
+
return {
|
|
2539
|
+
state: "recovery_required",
|
|
2540
|
+
accountId: input.accountId,
|
|
2541
|
+
orderId: input.orderId,
|
|
2542
|
+
reason: reason ?? "IBKR did not confirm the cancellation request",
|
|
2543
|
+
evidence,
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
return {
|
|
2547
|
+
state: "requested",
|
|
2548
|
+
accountId: input.accountId,
|
|
2549
|
+
orderId: input.orderId,
|
|
2550
|
+
message,
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
sanitizeJsonEvidence(value) {
|
|
2554
|
+
const seen = new WeakMap();
|
|
2555
|
+
let remainingEntries = 500;
|
|
2556
|
+
const dictionary = () => Object.create(null);
|
|
2557
|
+
const markerKey = (source, result, label) => {
|
|
2558
|
+
let key = label;
|
|
2559
|
+
while (Object.prototype.hasOwnProperty.call(source, key) ||
|
|
2560
|
+
Object.prototype.hasOwnProperty.call(result, key)) {
|
|
2561
|
+
key += "#";
|
|
2562
|
+
}
|
|
2563
|
+
return key;
|
|
2564
|
+
};
|
|
2565
|
+
const sanitize = (item, depth, path) => {
|
|
2566
|
+
if (item === null || typeof item === "boolean" || typeof item === "string")
|
|
2567
|
+
return item;
|
|
2568
|
+
if (typeof item === "number") {
|
|
2569
|
+
return Number.isFinite(item) ? item : `[non-json number: ${String(item)}]`;
|
|
2570
|
+
}
|
|
2571
|
+
if (typeof item !== "object")
|
|
2572
|
+
return `[non-json ${typeof item}]`;
|
|
2573
|
+
const priorPath = seen.get(item);
|
|
2574
|
+
if (priorPath !== undefined)
|
|
2575
|
+
return `[reference: ${priorPath}]`;
|
|
2576
|
+
seen.set(item, path);
|
|
2577
|
+
if (depth >= 8) {
|
|
2578
|
+
const result = dictionary();
|
|
2579
|
+
result[markerKey(item, result, "[truncated: depth]")] = true;
|
|
2580
|
+
return result;
|
|
2581
|
+
}
|
|
2582
|
+
if (Array.isArray(item)) {
|
|
2583
|
+
const result = [];
|
|
2584
|
+
for (const [index, member] of item.entries()) {
|
|
2585
|
+
if (remainingEntries <= 0) {
|
|
2586
|
+
result.push("[truncated: entry count]");
|
|
2587
|
+
break;
|
|
2588
|
+
}
|
|
2589
|
+
remainingEntries -= 1;
|
|
2590
|
+
result.push(sanitize(member, depth + 1, `${path}[${String(index)}]`));
|
|
2591
|
+
}
|
|
2592
|
+
return result;
|
|
2593
|
+
}
|
|
2594
|
+
const result = dictionary();
|
|
2595
|
+
for (const ownKey of Reflect.ownKeys(item)) {
|
|
2596
|
+
if (remainingEntries <= 0) {
|
|
2597
|
+
result[markerKey(item, result, "[truncated: entry count]")] = true;
|
|
2598
|
+
break;
|
|
2599
|
+
}
|
|
2600
|
+
remainingEntries -= 1;
|
|
2601
|
+
const key = typeof ownKey === "string"
|
|
2602
|
+
? ownKey
|
|
2603
|
+
: markerKey(item, result, `[non-json symbol key: ${ownKey.description ?? ""}]`);
|
|
2604
|
+
let member;
|
|
2605
|
+
try {
|
|
2606
|
+
member = Reflect.get(item, ownKey);
|
|
2607
|
+
}
|
|
2608
|
+
catch {
|
|
2609
|
+
member = "[unreadable property]";
|
|
2610
|
+
}
|
|
2611
|
+
result[key] = sanitize(member, depth + 1, `${path}.${JSON.stringify(key)}`);
|
|
2612
|
+
}
|
|
2613
|
+
return result;
|
|
2614
|
+
};
|
|
2615
|
+
const sanitized = sanitize(value, 0, "$");
|
|
2616
|
+
const encoded = JSON.stringify(sanitized);
|
|
2617
|
+
if (encoded.length <= 8_192)
|
|
2618
|
+
return sanitized;
|
|
2619
|
+
const fallback = dictionary();
|
|
2620
|
+
fallback["[truncated: evidence size]"] = true;
|
|
2621
|
+
fallback["originalSerializedLength"] = encoded.length;
|
|
2622
|
+
let low = 0;
|
|
2623
|
+
let high = encoded.length;
|
|
2624
|
+
while (low < high) {
|
|
2625
|
+
const middle = Math.ceil((low + high) / 2);
|
|
2626
|
+
fallback["preview"] = encoded.slice(0, middle);
|
|
2627
|
+
if (JSON.stringify(fallback).length <= 8_192)
|
|
2628
|
+
low = middle;
|
|
2629
|
+
else
|
|
2630
|
+
high = middle - 1;
|
|
2631
|
+
}
|
|
2632
|
+
fallback["preview"] = encoded.slice(0, low);
|
|
2633
|
+
return fallback;
|
|
2634
|
+
}
|
|
2635
|
+
cancellationOrderId(value) {
|
|
2636
|
+
if (typeof value === "string")
|
|
2637
|
+
return this.trimmedString(value);
|
|
2638
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0
|
|
2639
|
+
? String(value)
|
|
2640
|
+
: null;
|
|
2641
|
+
}
|
|
2642
|
+
cancellationErrorParts(record) {
|
|
2643
|
+
const parts = [];
|
|
2644
|
+
for (const key of ["error", "message", "text"]) {
|
|
2645
|
+
if (!(key in record))
|
|
2646
|
+
continue;
|
|
2647
|
+
const text = this.trimmedString(record[key]);
|
|
2648
|
+
parts.push(text === null ? `${key}: present` : `${key}: ${text}`);
|
|
2649
|
+
}
|
|
2650
|
+
for (const key of ["statusCode", "code"]) {
|
|
2651
|
+
if (!(key in record))
|
|
2652
|
+
continue;
|
|
2653
|
+
const value = record[key];
|
|
2654
|
+
parts.push(typeof value === "string" || typeof value === "number"
|
|
2655
|
+
? `${key}: ${String(value)}`
|
|
2656
|
+
: `${key}: present`);
|
|
2657
|
+
}
|
|
2658
|
+
if (record["success"] === false)
|
|
2659
|
+
parts.push("success: false");
|
|
2660
|
+
return parts;
|
|
2661
|
+
}
|
|
2662
|
+
normalizeMultiOrderSubmission(response, parentClientOrderId, accountId) {
|
|
2291
2663
|
const decoded = this.decodeOrderSubmission(response);
|
|
2292
2664
|
const hasDistinctBrokerOrderIds = decoded.orders.length === 2 &&
|
|
2293
2665
|
new Set(decoded.orders.map(({ orderId }) => orderId)).size === 2;
|
|
@@ -2314,7 +2686,7 @@ export class IbkrClient {
|
|
|
2314
2686
|
return {
|
|
2315
2687
|
state: "warning",
|
|
2316
2688
|
warnings: decoded.warnings,
|
|
2317
|
-
continuation: { replyId: warning.replyId, parentClientOrderId },
|
|
2689
|
+
continuation: { accountId, replyId: warning.replyId, parentClientOrderId },
|
|
2318
2690
|
};
|
|
2319
2691
|
}
|
|
2320
2692
|
if (hasErrors &&
|
|
@@ -3147,15 +3519,57 @@ export class IbkrClient {
|
|
|
3147
3519
|
const parsed = Number(match[0].replace(/,/g, ""));
|
|
3148
3520
|
return Number.isFinite(parsed) ? parsed : null;
|
|
3149
3521
|
}
|
|
3522
|
+
withAccountCriticalSection(operation) {
|
|
3523
|
+
this.assertOpen();
|
|
3524
|
+
const result = this.accountCriticalSectionTail.then(async () => {
|
|
3525
|
+
this.assertOpen();
|
|
3526
|
+
return operation();
|
|
3527
|
+
});
|
|
3528
|
+
this.accountCriticalSectionTail = result.then(() => undefined, () => undefined);
|
|
3529
|
+
return result;
|
|
3530
|
+
}
|
|
3531
|
+
withTradingMutation(accountId, unsafeMessage, operation) {
|
|
3532
|
+
return this.withAccountCriticalSection(async () => {
|
|
3533
|
+
const diagnostics = await this.getTradingDiagnostics(accountId);
|
|
3534
|
+
if (!this.isSafeForTradingMutation(diagnostics))
|
|
3535
|
+
throw new Error(unsafeMessage);
|
|
3536
|
+
await this.prepareBrokerageAccount(accountId);
|
|
3537
|
+
return operation(diagnostics);
|
|
3538
|
+
});
|
|
3539
|
+
}
|
|
3540
|
+
isSafeForTradingMutation(diagnostics) {
|
|
3541
|
+
return (diagnostics.authenticated === true &&
|
|
3542
|
+
diagnostics.connected === true &&
|
|
3543
|
+
diagnostics.competingSession === false &&
|
|
3544
|
+
diagnostics.environment !== null);
|
|
3545
|
+
}
|
|
3546
|
+
assertOpen() {
|
|
3547
|
+
if (this.closed)
|
|
3548
|
+
throw this.closedError();
|
|
3549
|
+
}
|
|
3550
|
+
closedError() {
|
|
3551
|
+
return new Error("This IBKR client is closed");
|
|
3552
|
+
}
|
|
3553
|
+
booleanOrNull(value) {
|
|
3554
|
+
return typeof value === "boolean" ? value : null;
|
|
3555
|
+
}
|
|
3556
|
+
accountIdsOrNull(value) {
|
|
3557
|
+
return Array.isArray(value) &&
|
|
3558
|
+
Array.from(value).every((accountId) => typeof accountId === "string")
|
|
3559
|
+
? value
|
|
3560
|
+
: null;
|
|
3561
|
+
}
|
|
3150
3562
|
async prepareBrokerageAccount(accountId) {
|
|
3151
|
-
const
|
|
3563
|
+
const rawBrokerageAccounts = await this.req({
|
|
3152
3564
|
path: "iserver/accounts",
|
|
3153
3565
|
});
|
|
3154
|
-
|
|
3155
|
-
|
|
3156
|
-
if (
|
|
3566
|
+
const brokerageAccounts = isUnknownRecord(rawBrokerageAccounts) ? rawBrokerageAccounts : {};
|
|
3567
|
+
const accountIds = this.accountIdsOrNull(brokerageAccounts["accounts"]);
|
|
3568
|
+
if (!accountIds?.includes(accountId)) {
|
|
3157
3569
|
throw new Error(`IBKR account ${accountId} is not available for trading/order queries.`);
|
|
3158
3570
|
}
|
|
3571
|
+
if (brokerageAccounts["selectedAccount"] === accountId)
|
|
3572
|
+
return;
|
|
3159
3573
|
const switchedAccount = await this.singleAttemptRequest({
|
|
3160
3574
|
path: "iserver/account",
|
|
3161
3575
|
method: "POST",
|
|
@@ -3294,6 +3708,7 @@ export class IbkrClient {
|
|
|
3294
3708
|
}
|
|
3295
3709
|
/** Return complete daily history with the exact validated IBKR request context. */
|
|
3296
3710
|
async getPriceHistory(input) {
|
|
3711
|
+
this.assertOpen();
|
|
3297
3712
|
const requestedSymbol = input.symbol.trim().toUpperCase();
|
|
3298
3713
|
if (!requestedSymbol) {
|
|
3299
3714
|
throw new IbkrPriceHistoryContractError("Price history requires a symbol", "CONTRACT_INVALID");
|
|
@@ -3444,6 +3859,7 @@ export class IbkrClient {
|
|
|
3444
3859
|
}
|
|
3445
3860
|
/** Discover listed derivative series over an inclusive calendar range. */
|
|
3446
3861
|
async getDerivativeExpiries(query) {
|
|
3862
|
+
this.assertOpen();
|
|
3447
3863
|
const contracts = [];
|
|
3448
3864
|
for (const month of monthCodes(query.from, query.to)) {
|
|
3449
3865
|
contracts.push(...(await this.discoverDerivativeMonth(query.underlying, query.assetClass, month, query.exchange, query.right)));
|
|
@@ -3477,6 +3893,7 @@ export class IbkrClient {
|
|
|
3477
3893
|
}
|
|
3478
3894
|
/** Discover contracts for one exact expiration, preserving class and venue identity. */
|
|
3479
3895
|
async getDerivativeContracts(query) {
|
|
3896
|
+
this.assertOpen();
|
|
3480
3897
|
const tradingClass = query.tradingClass?.trim().toUpperCase();
|
|
3481
3898
|
return (await this.discoverDerivativeMonth(query.underlying, query.assetClass, monthCode(query.expiration), query.exchange, query.right, query.strike)).filter((contract) => contract.expiration === query.expiration &&
|
|
3482
3899
|
(query.right === undefined || contract.right === query.right) &&
|
|
@@ -3485,6 +3902,7 @@ export class IbkrClient {
|
|
|
3485
3902
|
}
|
|
3486
3903
|
/** Resolve exactly one contract and reject missing or ambiguous semantic identity. */
|
|
3487
3904
|
async resolveDerivativeContract(query) {
|
|
3905
|
+
this.assertOpen();
|
|
3488
3906
|
const contracts = await this.getDerivativeContracts(query);
|
|
3489
3907
|
if (!contracts.length) {
|
|
3490
3908
|
throw new Error(`IBKR returned no exact ${query.assetClass} contract for ${query.underlying} ${query.expiration} ${query.right}${String(query.strike)}`);
|
|
@@ -3500,6 +3918,7 @@ export class IbkrClient {
|
|
|
3500
3918
|
}
|
|
3501
3919
|
/** Return an exact-expiration derivative chain with explicit data availability. */
|
|
3502
3920
|
async getDerivativeChain(query) {
|
|
3921
|
+
this.assertOpen();
|
|
3503
3922
|
const contracts = await this.getDerivativeContracts(query);
|
|
3504
3923
|
if (!contracts.length) {
|
|
3505
3924
|
throw new Error(`IBKR returned no ${query.assetClass} contracts for ${query.underlying} ${query.expiration}`);
|
|
@@ -3512,6 +3931,7 @@ export class IbkrClient {
|
|
|
3512
3931
|
}
|
|
3513
3932
|
/** Quote the broker-linked underlying (for example, the Sep NQ future behind QN3). */
|
|
3514
3933
|
async getDerivativeReferenceQuote(contract) {
|
|
3934
|
+
this.assertOpen();
|
|
3515
3935
|
const detailResponse = await this.req({
|
|
3516
3936
|
path: "trsrv/secdef",
|
|
3517
3937
|
params: { conids: String(contract.conid) },
|
|
@@ -3558,6 +3978,7 @@ export class IbkrClient {
|
|
|
3558
3978
|
}
|
|
3559
3979
|
/** Discover every listed weekly/monthly expiry in the requested calendar range. */
|
|
3560
3980
|
async getOptionExpiries(symbol, right, fromDate, toDate, options = {}) {
|
|
3981
|
+
this.assertOpen();
|
|
3561
3982
|
const normalized = symbol.trim().toUpperCase();
|
|
3562
3983
|
const months = monthCodes(fromDate, toDate);
|
|
3563
3984
|
const contracts = [];
|
|
@@ -3573,6 +3994,7 @@ export class IbkrClient {
|
|
|
3573
3994
|
}
|
|
3574
3995
|
/** Build one exact-expiry chain with canonical OSI symbols and required pricing/greeks. */
|
|
3575
3996
|
async getOptionChain(symbol, expiry, right, options = {}) {
|
|
3997
|
+
this.assertOpen();
|
|
3576
3998
|
const month = monthCode(expiry);
|
|
3577
3999
|
const normalized = symbol.trim().toUpperCase();
|
|
3578
4000
|
const discovery = await this.discoverOptions(normalized, month, right, options);
|
|
@@ -3592,6 +4014,7 @@ export class IbkrClient {
|
|
|
3592
4014
|
}
|
|
3593
4015
|
/** Return every qualified contract for one exact expiry and side without hiding sparse data. */
|
|
3594
4016
|
async getOptionChainSnapshot(symbol, expiry, right, options = {}) {
|
|
4017
|
+
this.assertOpen();
|
|
3595
4018
|
const month = monthCode(expiry);
|
|
3596
4019
|
const normalized = symbol.trim().toUpperCase();
|
|
3597
4020
|
const discovery = await this.discoverOptions(normalized, month, right, options);
|
|
@@ -3603,6 +4026,7 @@ export class IbkrClient {
|
|
|
3603
4026
|
}
|
|
3604
4027
|
/** Fetch one exact option quote; null means the contract is not listed. */
|
|
3605
4028
|
async getOptionQuote(input) {
|
|
4029
|
+
this.assertOpen();
|
|
3606
4030
|
const contract = await this.resolveOptionContract(input);
|
|
3607
4031
|
if (!contract)
|
|
3608
4032
|
return null;
|
|
@@ -3610,6 +4034,7 @@ export class IbkrClient {
|
|
|
3610
4034
|
}
|
|
3611
4035
|
/** Resolve a conid back into the canonical OSI-bearing option contract. */
|
|
3612
4036
|
async getOptionContract(conid) {
|
|
4037
|
+
this.assertOpen();
|
|
3613
4038
|
const response = await this.req({
|
|
3614
4039
|
path: "trsrv/secdef",
|
|
3615
4040
|
params: { conids: String(conid) },
|
|
@@ -4984,6 +5409,8 @@ export class IbkrClient {
|
|
|
4984
5409
|
return this.scheduledRequest(input, "SINGLE_ATTEMPT");
|
|
4985
5410
|
}
|
|
4986
5411
|
scheduledRequest(input, retryPolicy, signal, onTerminalFailure) {
|
|
5412
|
+
if (this.closed)
|
|
5413
|
+
return Promise.reject(this.closedError());
|
|
4987
5414
|
return this.requestScheduler.schedule({
|
|
4988
5415
|
endpoint: this.requestEndpoint(input.path),
|
|
4989
5416
|
priority: this.requestPriority(input.path),
|