@huskly/ibkr-client 1.5.1 → 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.
@@ -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
- /** Obtain the live session token (idempotent — safe to await repeatedly). */
445
- init() {
446
- this.initPromise ??= (async () => {
447
- try {
448
- await this.raw.init();
449
- }
450
- catch (error) {
451
- throw this.normalizeHttpError(error);
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
- const status = await this.req({
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.authenticated ?? false,
465
- competing: status.competing ?? false,
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, accounts] = await Promise.all([
547
+ const [status, rawAccounts] = await Promise.all([
472
548
  this.getAuthStatus(),
473
549
  this.req({ path: "iserver/accounts" }),
474
550
  ]);
475
- if (!accounts.accounts?.includes(accountId)) {
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.selectedAccount ?? null,
481
- environment: accounts.isPaper === true ? "paper" : "live",
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: accounts.allowFeatures?.showGFIS ?? null,
485
- advisoryAssetPermissions: accounts.allowFeatures?.allowedAssetTypes
486
- ?.split(",")
487
- .map((value) => value.trim())
488
- .filter(Boolean) ?? [],
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
- const diagnostics = await this.getTradingDiagnostics(request.accountId);
494
- if (!diagnostics.authenticated || diagnostics.competingSession) {
495
- throw new Error("IBKR brokerage session is not safely authenticated for What-If");
496
- }
497
- await this.prepareBrokerageAccount(request.accountId);
498
- const conids = request.legs.map(({ contract }) => contract.conid).join(",");
499
- await this.req({
500
- path: "iserver/marketdata/snapshot",
501
- params: { conids, fields: "6509" },
502
- });
503
- const response = await this.singleAttemptRequest({
504
- path: `iserver/account/${request.accountId}/orders/whatif`,
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
- const diagnostics = await this.getTradingDiagnostics(request.accountId);
519
- if (!diagnostics.authenticated || diagnostics.competingSession) {
520
- throw new Error("IBKR brokerage session is not safely authenticated for submission");
521
- }
522
- await this.prepareBrokerageAccount(request.accountId);
523
- const response = await this.singleAttemptRequest({
524
- path: `iserver/account/${request.accountId}/orders`,
525
- method: "POST",
526
- data: {
527
- orders: [
528
- {
529
- ...this.comboOrderTicket(request),
530
- cOID: request.clientOrderId,
531
- ...cmeOperatorMetadata,
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
- const diagnostics = await this.getTradingDiagnostics(request.accountId);
542
- if (!diagnostics.authenticated || diagnostics.competingSession) {
543
- throw new Error("IBKR brokerage session is not safely authenticated for submission");
544
- }
545
- await this.prepareBrokerageAccount(request.accountId);
546
- const response = await this.singleAttemptRequest({
547
- path: `iserver/account/${request.accountId}/orders`,
548
- method: "POST",
549
- data: {
550
- orders: [
551
- {
552
- ...this.singleOrderTicket(request),
553
- ...(request.clientOrderId !== undefined ? { cOID: request.clientOrderId } : {}),
554
- ...(request.parentId !== undefined ? { parentId: request.parentId } : {}),
555
- ...cmeOperatorMetadata,
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
- const diagnostics = await this.getTradingDiagnostics(accountId);
582
- if (!diagnostics.authenticated || diagnostics.competingSession) {
583
- throw new Error("IBKR brokerage session is not safely authenticated for submission");
584
- }
585
- await this.prepareBrokerageAccount(accountId);
586
- const response = await this.singleAttemptRequest({
587
- path: `iserver/account/${accountId}/orders`,
588
- method: "POST",
589
- data: {
590
- orders: [
591
- {
592
- ...this.singleOrderTicket(parent),
593
- cOID: parent.clientOrderId,
594
- ...parentMetadata,
595
- },
596
- {
597
- ...this.singleOrderTicket(child),
598
- parentId: parent.clientOrderId,
599
- ...childMetadata,
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
- const diagnostics = await this.getTradingDiagnostics(request.accountId);
609
- if (!diagnostics.authenticated || diagnostics.competingSession) {
610
- throw new Error("IBKR brokerage session is not safely authenticated for submission");
611
- }
612
- await this.prepareBrokerageAccount(request.accountId);
613
- const response = await this.singleAttemptRequest({
614
- path: `iserver/account/${request.accountId}/orders`,
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
- const diagnostics = await this.getTradingDiagnostics(input.continuation.request.accountId);
627
- if (!diagnostics.authenticated || diagnostics.competingSession) {
628
- throw new Error("IBKR brokerage session is not safely authenticated for submission");
629
- }
630
- await this.prepareBrokerageAccount(input.continuation.request.accountId);
631
- const response = await this.singleAttemptRequest({
632
- path: `iserver/reply/${encodeURIComponent(input.continuation.replyId)}`,
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
- await this.prepareBrokerageAccount(input.accountId);
650
- const response = await this.req({
651
- path: "iserver/account/orders",
652
- params: { accountId: input.accountId },
653
- });
654
- const flattenedActiveSnapshot = this.resolveFlattenedGraphParentAliases(request, this.flattenCompleteOrderSnapshot(response));
655
- const activeSnapshotIncomplete = flattenedActiveSnapshot === null;
656
- const invalidActiveAccountEvidence = flattenedActiveSnapshot?.some(({ order }) => !this.orderHasExactAccount(order, input.accountId)) ?? false;
657
- const invalidNestedActiveEvidence = flattenedActiveSnapshot?.some(({ order, nestedParent }) => nestedParent !== null &&
658
- !this.recoveryGraphOrderMayBeAttached(request, order) &&
659
- this.recoveryGraphOrderIsAttached(request, nestedParent)) ?? false;
660
- const accountOrders = (flattenedActiveSnapshot ?? [])
661
- .map(({ order }) => order)
662
- .filter((order) => this.orderHasExactAccount(order, input.accountId));
663
- const activeMatchesByMember = new Map();
664
- const knownActiveOrders = accountOrders.filter((order) => input.orderId !== undefined && this.recoveryOrderId(order) === input.orderId);
665
- const conflictingKnownActiveOrders = knownActiveOrders.filter((order) => !this.recoveryGraphOrderMayBeAttached(request, order));
666
- const observedCandidates = activeSnapshotIncomplete || invalidActiveAccountEvidence || invalidNestedActiveEvidence
667
- ? [response]
668
- : [...conflictingKnownActiveOrders];
669
- observedCandidates.push(...accountOrders.filter((order) => this.recoveryGraphOrderMayBeAttached(request, order)));
670
- for (const node of request.nodes) {
671
- const matches = accountOrders.filter((order) => this.terminalOrderTicketIsValid(order) &&
672
- this.orderHasValidRecoveryStatus(order) &&
673
- this.recoveryOrderMatchesGraphNode(request, node, order));
674
- activeMatchesByMember.set(node.memberId, matches);
675
- }
676
- const selected = new Map();
677
- const usedOrderIds = new Set();
678
- for (const node of request.nodes) {
679
- const matches = activeMatchesByMember.get(node.memberId) ?? [];
680
- const [match] = matches;
681
- if (matches.length !== 1 || match === undefined)
682
- continue;
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
- selected.set(node.memberId, order);
703
- const orderId = this.recoveryOrderId(order);
704
- if (orderId !== undefined)
705
- usedOrderIds.add(orderId);
706
- observedCandidates.push(order);
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
- observedCandidates.push(...terminalEvidence.observedResponses);
710
- if (terminalEvidence.invalidAttachedEvidence) {
711
- observedCandidates.push({ reason: "Invalid or conflicting terminal broker evidence" });
712
- }
713
- const selectedCandidates = [...selected.values()];
714
- const requestedOrderIdMissing = input.orderId !== undefined &&
715
- !selectedCandidates.some((order) => this.recoveryOrderId(order) === input.orderId);
716
- const members = request.nodes.map((node) => {
717
- const order = selected.get(node.memberId);
718
- return this.graphMemberEvidence(request, node, order);
719
- });
720
- const ids = members.flatMap(({ orderId }) => (orderId === null ? [] : [orderId]));
721
- const hasDistinctOrderIds = new Set(ids).size === request.nodes.length;
722
- const linkedOrderIds = new Set();
723
- let linkedOrderMissingBrokerId = false;
724
- for (const order of accountOrders) {
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
- const orderId = this.recoveryOrderId(order);
732
- if (orderId === undefined) {
733
- linkedOrderMissingBrokerId = true;
734
- continue;
781
+ observedCandidates.push(...terminalEvidence.observedResponses);
782
+ if (terminalEvidence.invalidAttachedEvidence) {
783
+ observedCandidates.push({ reason: "Invalid or conflicting terminal broker evidence" });
735
784
  }
736
- linkedOrderIds.add(orderId);
737
- }
738
- for (const order of terminalEvidence.linkedOrders) {
739
- const orderId = this.recoveryOrderId(order);
740
- if (orderId === undefined) {
741
- linkedOrderMissingBrokerId = true;
742
- continue;
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: "recovery_required",
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
- const response = await this.singleAttemptRequest({
1299
- path: `iserver/reply/${encodeURIComponent(input.replyId)}`,
1300
- method: "POST",
1301
- data: { confirmed: true },
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
- const response = await this.singleAttemptRequest({
1317
- path: `iserver/reply/${encodeURIComponent(input.continuation.replyId)}`,
1318
- method: "POST",
1319
- data: { confirmed: true },
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
- await this.prepareBrokerageAccount(accountId);
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
- await this.prepareBrokerageAccount(input.accountId);
1353
- const response = await this.req({
1354
- path: "iserver/account/orders",
1355
- params: { accountId: input.accountId },
1356
- });
1357
- const order = response.orders?.find((candidate) => {
1358
- if (!this.orderBelongsToAccount(candidate, input.accountId))
1359
- return false;
1360
- return (candidate.cOID ?? candidate.order_ref) === input.clientOrderId;
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
- await this.prepareBrokerageAccount(accountId);
1374
- const response = await this.req({
1375
- path: "iserver/account/orders",
1376
- params: { accountId },
1377
- });
1378
- const flattened = this.flattenCompleteOrderSnapshot(response);
1379
- if (flattened === null) {
1380
- throw new Error("IBKR active-order snapshot is incomplete");
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
- if (order.clientOrderId !== null) {
1401
- const members = byClientId.get(order.clientOrderId) ?? [];
1402
- members.push(order);
1403
- byClientId.set(order.clientOrderId, members);
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
- for (const order of normalized) {
1407
- if (order.orderId !== null && (byOrderId.get(order.orderId)?.length ?? 0) > 1) {
1408
- this.addOrderUncertainty(order, "DUPLICATE_MEMBER");
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 parentIdentity = order.parentOrderId ?? order.parentClientOrderId;
1411
- if (parentIdentity === null)
1412
- continue;
1413
- const brokerMatches = byOrderId.get(parentIdentity) ?? [];
1414
- const clientMatches = byClientId.get(parentIdentity) ?? [];
1415
- const matches = new Set([...brokerMatches, ...clientMatches]);
1416
- if (matches.size === 0)
1417
- this.addOrderUncertainty(order, "MISSING_PARENT");
1418
- if (matches.size > 1)
1419
- this.addOrderUncertainty(order, "AMBIGUOUS_PARENT");
1420
- }
1421
- return normalized;
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
- await this.prepareBrokerageAccount(input.accountId);
1437
- const response = await this.req({
1438
- path: "iserver/account/trades",
1439
- ...(input.days === undefined ? {} : { params: { days: input.days } }),
1440
- });
1441
- return response
1442
- .filter((trade) => (trade.account ?? trade.accountCode) === input.accountId)
1443
- .filter((trade) => input.orderId === undefined || String(trade.order_id) === input.orderId)
1444
- .filter((trade) => input.clientOrderId === undefined || trade.order_ref === input.clientOrderId)
1445
- .flatMap((trade) => {
1446
- const execution = this.normalizeDerivativeExecution(input.accountId, trade);
1447
- return execution === undefined ? [] : [execution];
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
- await this.prepareBrokerageAccount(input.accountId);
1482
- const response = await this.singleAttemptRequest({
1483
- path: `iserver/account/${input.accountId}/order/${encodeURIComponent(input.orderId)}`,
1484
- method: "DELETE",
1485
- ...(Object.keys(cmeOperatorMetadata).length > 0 ? { params: cmeOperatorMetadata } : {}),
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
- await this.prepareBrokerageAccount(accountId);
1727
- const params = {};
1728
- if (options.status && options.status.toUpperCase() !== "WORKING") {
1729
- params["filters"] = this.ibkrStatusFilter(options.status);
1730
- }
1731
- const response = await this.req({
1732
- path: "iserver/account/orders",
1733
- params,
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
- normalizeMultiOrderSubmission(response, parentClientOrderId) {
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 &&
@@ -2636,7 +2910,12 @@ export class IbkrClient {
2636
2910
  filledQuantity,
2637
2911
  remainingQuantity,
2638
2912
  averagePrice: this.firstNumber(order.avgPrice, order.avg_price, order.average_price, order.averagePrice) ?? null,
2913
+ // The exact read and the active snapshot must describe one order the same way, so both
2914
+ // carry the order type and the stop trigger from the same broker fields. Nothing is
2915
+ // inferred: each stays `null` when IBKR sends no value.
2916
+ orderType: this.normalizeOrderType(order.order_type ?? order.orderType) ?? null,
2639
2917
  limitPrice: this.firstNumber(order.limitPrice, order.limit_price, order.price) ?? null,
2918
+ stopPrice: this.firstNumber(order.stopPrice, order.stop_price) ?? null,
2640
2919
  commissionAndFees: typeof order.commissionAndFees === "number"
2641
2920
  ? order.commissionAndFees
2642
2921
  : this.whatIfNumber(order.commissionAndFees),
@@ -3142,15 +3421,57 @@ export class IbkrClient {
3142
3421
  const parsed = Number(match[0].replace(/,/g, ""));
3143
3422
  return Number.isFinite(parsed) ? parsed : null;
3144
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
+ }
3145
3464
  async prepareBrokerageAccount(accountId) {
3146
- const brokerageAccounts = await this.req({
3465
+ const rawBrokerageAccounts = await this.req({
3147
3466
  path: "iserver/accounts",
3148
3467
  });
3149
- if (brokerageAccounts.selectedAccount === accountId)
3150
- return;
3151
- if (brokerageAccounts.accounts && !brokerageAccounts.accounts.includes(accountId)) {
3468
+ const brokerageAccounts = isUnknownRecord(rawBrokerageAccounts) ? rawBrokerageAccounts : {};
3469
+ const accountIds = this.accountIdsOrNull(brokerageAccounts["accounts"]);
3470
+ if (!accountIds?.includes(accountId)) {
3152
3471
  throw new Error(`IBKR account ${accountId} is not available for trading/order queries.`);
3153
3472
  }
3473
+ if (brokerageAccounts["selectedAccount"] === accountId)
3474
+ return;
3154
3475
  const switchedAccount = await this.singleAttemptRequest({
3155
3476
  path: "iserver/account",
3156
3477
  method: "POST",
@@ -3289,6 +3610,7 @@ export class IbkrClient {
3289
3610
  }
3290
3611
  /** Return complete daily history with the exact validated IBKR request context. */
3291
3612
  async getPriceHistory(input) {
3613
+ this.assertOpen();
3292
3614
  const requestedSymbol = input.symbol.trim().toUpperCase();
3293
3615
  if (!requestedSymbol) {
3294
3616
  throw new IbkrPriceHistoryContractError("Price history requires a symbol", "CONTRACT_INVALID");
@@ -3439,6 +3761,7 @@ export class IbkrClient {
3439
3761
  }
3440
3762
  /** Discover listed derivative series over an inclusive calendar range. */
3441
3763
  async getDerivativeExpiries(query) {
3764
+ this.assertOpen();
3442
3765
  const contracts = [];
3443
3766
  for (const month of monthCodes(query.from, query.to)) {
3444
3767
  contracts.push(...(await this.discoverDerivativeMonth(query.underlying, query.assetClass, month, query.exchange, query.right)));
@@ -3472,6 +3795,7 @@ export class IbkrClient {
3472
3795
  }
3473
3796
  /** Discover contracts for one exact expiration, preserving class and venue identity. */
3474
3797
  async getDerivativeContracts(query) {
3798
+ this.assertOpen();
3475
3799
  const tradingClass = query.tradingClass?.trim().toUpperCase();
3476
3800
  return (await this.discoverDerivativeMonth(query.underlying, query.assetClass, monthCode(query.expiration), query.exchange, query.right, query.strike)).filter((contract) => contract.expiration === query.expiration &&
3477
3801
  (query.right === undefined || contract.right === query.right) &&
@@ -3480,6 +3804,7 @@ export class IbkrClient {
3480
3804
  }
3481
3805
  /** Resolve exactly one contract and reject missing or ambiguous semantic identity. */
3482
3806
  async resolveDerivativeContract(query) {
3807
+ this.assertOpen();
3483
3808
  const contracts = await this.getDerivativeContracts(query);
3484
3809
  if (!contracts.length) {
3485
3810
  throw new Error(`IBKR returned no exact ${query.assetClass} contract for ${query.underlying} ${query.expiration} ${query.right}${String(query.strike)}`);
@@ -3495,6 +3820,7 @@ export class IbkrClient {
3495
3820
  }
3496
3821
  /** Return an exact-expiration derivative chain with explicit data availability. */
3497
3822
  async getDerivativeChain(query) {
3823
+ this.assertOpen();
3498
3824
  const contracts = await this.getDerivativeContracts(query);
3499
3825
  if (!contracts.length) {
3500
3826
  throw new Error(`IBKR returned no ${query.assetClass} contracts for ${query.underlying} ${query.expiration}`);
@@ -3507,6 +3833,7 @@ export class IbkrClient {
3507
3833
  }
3508
3834
  /** Quote the broker-linked underlying (for example, the Sep NQ future behind QN3). */
3509
3835
  async getDerivativeReferenceQuote(contract) {
3836
+ this.assertOpen();
3510
3837
  const detailResponse = await this.req({
3511
3838
  path: "trsrv/secdef",
3512
3839
  params: { conids: String(contract.conid) },
@@ -3536,6 +3863,9 @@ export class IbkrClient {
3536
3863
  const bid = snapshot ? (this.snapshotNumber(snapshot, "84") ?? null) : null;
3537
3864
  const ask = snapshot ? (this.snapshotNumber(snapshot, "86") ?? null) : null;
3538
3865
  const suppliedMark = snapshot ? (this.snapshotNumber(snapshot, "7635") ?? null) : null;
3866
+ const trade = snapshot
3867
+ ? this.snapshotTradePrice(snapshot)
3868
+ : { last: undefined, close: undefined };
3539
3869
  return {
3540
3870
  conid: referenceConid,
3541
3871
  symbol: String(snapshot?.["55"] ?? detail?.undSym ?? contract.underlying),
@@ -3543,12 +3873,14 @@ export class IbkrClient {
3543
3873
  timestamp: snapshot ? this.snapshotTimestamp(snapshot) : null,
3544
3874
  bid,
3545
3875
  ask,
3546
- last: snapshot ? (this.snapshotNumber(snapshot, "31") ?? null) : null,
3876
+ last: trade.last ?? null,
3877
+ close: trade.close ?? null,
3547
3878
  mark: suppliedMark ?? (bid !== null && ask !== null ? (bid + ask) / 2 : null),
3548
3879
  };
3549
3880
  }
3550
3881
  /** Discover every listed weekly/monthly expiry in the requested calendar range. */
3551
3882
  async getOptionExpiries(symbol, right, fromDate, toDate, options = {}) {
3883
+ this.assertOpen();
3552
3884
  const normalized = symbol.trim().toUpperCase();
3553
3885
  const months = monthCodes(fromDate, toDate);
3554
3886
  const contracts = [];
@@ -3564,6 +3896,7 @@ export class IbkrClient {
3564
3896
  }
3565
3897
  /** Build one exact-expiry chain with canonical OSI symbols and required pricing/greeks. */
3566
3898
  async getOptionChain(symbol, expiry, right, options = {}) {
3899
+ this.assertOpen();
3567
3900
  const month = monthCode(expiry);
3568
3901
  const normalized = symbol.trim().toUpperCase();
3569
3902
  const discovery = await this.discoverOptions(normalized, month, right, options);
@@ -3583,6 +3916,7 @@ export class IbkrClient {
3583
3916
  }
3584
3917
  /** Return every qualified contract for one exact expiry and side without hiding sparse data. */
3585
3918
  async getOptionChainSnapshot(symbol, expiry, right, options = {}) {
3919
+ this.assertOpen();
3586
3920
  const month = monthCode(expiry);
3587
3921
  const normalized = symbol.trim().toUpperCase();
3588
3922
  const discovery = await this.discoverOptions(normalized, month, right, options);
@@ -3594,6 +3928,7 @@ export class IbkrClient {
3594
3928
  }
3595
3929
  /** Fetch one exact option quote; null means the contract is not listed. */
3596
3930
  async getOptionQuote(input) {
3931
+ this.assertOpen();
3597
3932
  const contract = await this.resolveOptionContract(input);
3598
3933
  if (!contract)
3599
3934
  return null;
@@ -3601,6 +3936,7 @@ export class IbkrClient {
3601
3936
  }
3602
3937
  /** Resolve a conid back into the canonical OSI-bearing option contract. */
3603
3938
  async getOptionContract(conid) {
3939
+ this.assertOpen();
3604
3940
  const response = await this.req({
3605
3941
  path: "trsrv/secdef",
3606
3942
  params: { conids: String(conid) },
@@ -3853,13 +4189,17 @@ export class IbkrClient {
3853
4189
  .map((snapshot) => [snapshot.conid, snapshot]));
3854
4190
  for (const contract of batch) {
3855
4191
  const snapshot = byConid.get(contract.conid);
4192
+ const trade = snapshot
4193
+ ? this.snapshotTradePrice(snapshot)
4194
+ : { last: undefined, close: undefined };
3856
4195
  result.push({
3857
4196
  contract,
3858
4197
  availability: normalizeDerivativeDataAvailability(snapshot?.["6509"]),
3859
4198
  timestamp: snapshot ? this.snapshotTimestamp(snapshot) : null,
3860
4199
  bid: snapshot ? (this.snapshotNumber(snapshot, "84") ?? null) : null,
3861
4200
  ask: snapshot ? (this.snapshotNumber(snapshot, "86") ?? null) : null,
3862
- last: snapshot ? (this.snapshotNumber(snapshot, "31") ?? null) : null,
4201
+ last: trade.last ?? null,
4202
+ close: trade.close ?? null,
3863
4203
  mark: snapshot ? (this.snapshotNumber(snapshot, "7635") ?? null) : null,
3864
4204
  delta: snapshot ? (this.snapshotNumber(snapshot, "7308") ?? null) : null,
3865
4205
  impliedVolatility: snapshot ? (this.snapshotNumber(snapshot, "7633") ?? null) : null,
@@ -4817,13 +5157,11 @@ export class IbkrClient {
4817
5157
  const exchange = this.snapshotString(snapshot, "6004") ?? contract.exchange;
4818
5158
  const latestBar = this.latestHistoryBar(history);
4819
5159
  const previousBar = this.previousHistoryBar(history);
4820
- const snapshotLastPrice = this.snapshotNumber(snapshot, "31");
4821
- const lastPrice = this.snapshotHasPrefix(snapshot, "31", "C")
4822
- ? (latestBar?.c ?? snapshotLastPrice)
4823
- : (snapshotLastPrice ?? latestBar?.c);
5160
+ const snapshotTrade = this.snapshotTradePrice(snapshot);
5161
+ const lastPrice = snapshotTrade.last ?? latestBar?.c;
4824
5162
  const bidPrice = this.snapshotNumber(snapshot, "84");
4825
5163
  const askPrice = this.snapshotNumber(snapshot, "86");
4826
- const closePrice = previousBar?.c;
5164
+ const closePrice = previousBar?.c ?? snapshotTrade.close;
4827
5165
  const highPrice = this.snapshotNumber(snapshot, "70") ?? latestBar?.h;
4828
5166
  const lowPrice = this.snapshotNumber(snapshot, "71") ?? latestBar?.l;
4829
5167
  const openPrice = latestBar?.o;
@@ -4904,6 +5242,22 @@ export class IbkrClient {
4904
5242
  const date = new Date(updatedMs);
4905
5243
  return Number.isNaN(date.getTime()) ? null : date.toISOString();
4906
5244
  }
5245
+ /**
5246
+ * Split IBKR snapshot field `31` into a traded last price and a previous close.
5247
+ *
5248
+ * IBKR marks the value with a `C` prefix when the contract has not traded in the current
5249
+ * session and the number is the previous close. The prefix is the only signal that separates a
5250
+ * close from a trade, so the close is reported as `close` and `last` stays undefined. A value
5251
+ * with no `C` prefix is a real last trade.
5252
+ */
5253
+ snapshotTradePrice(snapshot) {
5254
+ const value = this.snapshotNumber(snapshot, "31");
5255
+ if (value === undefined)
5256
+ return { last: undefined, close: undefined };
5257
+ return this.snapshotHasPrefix(snapshot, "31", "C")
5258
+ ? { last: undefined, close: value }
5259
+ : { last: value, close: undefined };
5260
+ }
4907
5261
  snapshotHasPrefix(snapshot, field, prefix) {
4908
5262
  return this.snapshotString(snapshot, field)?.toUpperCase().startsWith(prefix) ?? false;
4909
5263
  }
@@ -4957,6 +5311,8 @@ export class IbkrClient {
4957
5311
  return this.scheduledRequest(input, "SINGLE_ATTEMPT");
4958
5312
  }
4959
5313
  scheduledRequest(input, retryPolicy, signal, onTerminalFailure) {
5314
+ if (this.closed)
5315
+ return Promise.reject(this.closedError());
4960
5316
  return this.requestScheduler.schedule({
4961
5317
  endpoint: this.requestEndpoint(input.path),
4962
5318
  priority: this.requestPriority(input.path),