@huskly/ibkr-client 0.12.0 → 0.13.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 CHANGED
@@ -61,7 +61,9 @@ Optional environment variables:
61
61
 
62
62
  `IbkrClient` owns IBKR authentication, requests, raw response types, and
63
63
  normalization. Consumers such as huskly-cli provide presentation, command
64
- routing, and caching. Its broker-neutral account API includes:
64
+ routing, and caching. Public request types are the caller contract: this package assumes consumers
65
+ use TypeScript and pass values accepted by those types. Provider responses remain untrusted and are
66
+ validated at runtime. Its broker-neutral account API includes:
65
67
 
66
68
  - `getAccountBalances()` and `getPositions()` for account state.
67
69
  - `getQuotes()` and `searchInstruments()` for equity/ETF discovery and quotes.
@@ -166,13 +168,39 @@ Permission metadata is diagnostic only; the What-If response remains authoritati
166
168
  already enforced their own reviewed-preview workflow. It does not persist previews or decide
167
169
  whether live execution is allowed.
168
170
 
171
+ Submission rejection is authoritative only when IBKR returns its documented top-level error object
172
+ with a meaningful message, code, or failure status. Array-contained or non-diagnostic error fields
173
+ return `recovery_required`, because they do not prove that every submitted ticket failed.
174
+
175
+ - `submitDerivativeSingleOrder(...)` places one single-leg LIMIT or STOP option order with exact
176
+ contract, side, quantity, TIF, and session. Standalone orders require a unique client order ID;
177
+ sequentially attached child orders instead require the parent's exact ID and omit their own
178
+ `cOID`, as required by IBKR. LIMIT and STOP requests are discriminated: LIMIT orders require only
179
+ a positive `limit`, while STOP orders require only a positive `stopPrice`. Equity-option (`OPT`)
180
+ orders omit CME-only fields; futures-option (`FOP`) orders require the caller's exact `extOperator`
181
+ and `manualIndicator`. Mixed, multiple, unknown, or malformed response evidence returns
182
+ `recovery_required`; pending-cancel acknowledgements also require recovery. Callers must reconcile
183
+ the retained broker order IDs before another write.
169
184
  - `submitDerivativeCombo(...)` places one atomic combo with the exact legs, signed ratios,
170
185
  quantity, price effect, limit, TIF, and session supplied by the caller. The request requires a
171
186
  unique client order ID. Futures-option (`FOP`) writes also require the caller's exact CME
172
187
  `extOperator` and manual/automated-origin `manualIndicator`; equity-option (`OPT`) writes omit
173
- both CME-only fields.
188
+ both CME-only fields. As with single orders, ambiguous response evidence returns
189
+ `recovery_required` and must block blind resubmission.
190
+ - `submitDerivativeContingentOrders(...)` places a general parent/child LIMIT-or-STOP pair in one
191
+ bracket request. The parent carries the caller's unique client order ID; the client derives the
192
+ child's `parentId` from it and deliberately omits a child `cOID`. Parent and child must target the
193
+ same account, but may reference the same or different contracts. IBKR does not promise an
194
+ all-or-none response: `accepted` therefore requires exactly two non-failure acknowledgements,
195
+ while mixed, incomplete, pending-cancel, canceled, rejected, unknown, or malformed evidence is
196
+ returned as `recovery_required` with every observed broker order ID retained.
174
197
  - `acknowledgeOrderWarning(...)` replies once to an exact broker warning ID. Only documented
175
198
  warning message IDs are marked `known`; consumers must stop on unknown warnings.
199
+ - A warning from `submitDerivativeContingentOrders(...)` includes a typed `continuation` containing
200
+ the exact reply ID and parent client ID. Pass that object unchanged to
201
+ `acknowledgeContingentOrderWarning(...)`; its result retains both broker acknowledgements or
202
+ returns all partial evidence as `recovery_required`. Do not route contingent warnings through the
203
+ single-order acknowledgement method.
176
204
  - `getDerivativeOrderStatus(...)` uses IBKR's exact order-ID status endpoint so fast terminal
177
205
  orders remain visible after live-list eviction. It normalizes pending, working, partial-fill,
178
206
  fill, canceled, and rejected lifecycle states with leg ratios and order economics, and fails
@@ -1,5 +1,5 @@
1
1
  import type { IbkrOauth1Config } from "./oauthConfig.js";
2
- import type { AccountBalances, AuthStatus, BrokerAccountOrders, BrokerClient, BrokerInstrument, BrokerInstrumentSearchProjection, BrokerOrdersOptions, BrokerPosition, BrokerQuote, BrokerTransactionHistory, DerivativeContract, DerivativeContractQuery, DerivativeComboExecutionRequest, DerivativeComboReconciliation, DerivativeComboReconciliationRequest, DerivativeComboPreviewRequest, DerivativeComboPreviewResult, DerivativeDiscoveryClient, DerivativeExecutionClient, DerivativeExecution, DerivativeExecutionQuery, DerivativeOrderCancellationResult, DerivativeOrderCancelRequest, DerivativeOrderLifecycle, DerivativeOrderLookup, DerivativeOrderSubmissionResult, DerivativePreviewClient, DerivativeExpiry, DerivativeExpiryQuery, DerivativeQuote, DerivativeReferenceQuote, OptionContract, OptionMarketQuote, OptionQuoteRequest, OptionRight, PriceHistoryBar, PriceHistoryRequest, TradingDiagnostics } from "../types.js";
2
+ import type { AccountBalances, AuthStatus, BrokerAccountOrders, BrokerClient, BrokerInstrument, BrokerInstrumentSearchProjection, BrokerOrdersOptions, BrokerPosition, BrokerQuote, BrokerTransactionHistory, DerivativeContract, DerivativeContractQuery, DerivativeComboExecutionRequest, DerivativeComboReconciliation, DerivativeComboReconciliationRequest, DerivativeComboPreviewRequest, DerivativeComboPreviewResult, DerivativeContingentChildOrderRequest, DerivativeContingentParentOrderRequest, DerivativeDiscoveryClient, DerivativeExecutionClient, DerivativeExecution, DerivativeExecutionQuery, DerivativeMultiOrderResult, DerivativeOrderCancellationResult, DerivativeOrderCancelRequest, DerivativeOrderLifecycle, DerivativeOrderLookup, DerivativeOrderSubmissionResult, DerivativeSingleOrderRequest, DerivativePreviewClient, DerivativeExpiry, DerivativeExpiryQuery, DerivativeQuote, DerivativeReferenceQuote, OptionContract, OptionMarketQuote, OptionQuoteRequest, OptionRight, PriceHistoryBar, PriceHistoryRequest, TradingDiagnostics } from "../types.js";
3
3
  import { type IbkrRequestSchedulerOptions, type IbkrRequestTelemetry } from "./requestScheduler.js";
4
4
  interface IbkrRequestInput {
5
5
  path: string;
@@ -31,10 +31,23 @@ export declare class IbkrClient implements BrokerClient, DerivativeDiscoveryClie
31
31
  getTradingDiagnostics(accountId: string): Promise<TradingDiagnostics>;
32
32
  previewDerivativeCombo(request: DerivativeComboPreviewRequest): Promise<DerivativeComboPreviewResult>;
33
33
  submitDerivativeCombo(request: DerivativeComboExecutionRequest): Promise<DerivativeOrderSubmissionResult>;
34
+ submitDerivativeSingleOrder(request: DerivativeSingleOrderRequest): Promise<DerivativeOrderSubmissionResult>;
35
+ submitDerivativeContingentOrders(request: {
36
+ accountId: string;
37
+ parent: DerivativeContingentParentOrderRequest;
38
+ child: DerivativeContingentChildOrderRequest;
39
+ }): Promise<DerivativeMultiOrderResult>;
34
40
  acknowledgeOrderWarning(input: {
35
41
  replyId: string;
36
42
  confirmed: true;
37
43
  }): Promise<DerivativeOrderSubmissionResult>;
44
+ acknowledgeContingentOrderWarning(input: {
45
+ continuation: {
46
+ replyId: string;
47
+ parentClientOrderId: string;
48
+ };
49
+ confirmed: true;
50
+ }): Promise<DerivativeMultiOrderResult>;
38
51
  getDerivativeOrderStatus(accountId: string, orderId: string): Promise<DerivativeOrderLifecycle>;
39
52
  findDerivativeOrder(input: DerivativeOrderLookup): Promise<DerivativeOrderLifecycle>;
40
53
  getDerivativeExecutions(input: DerivativeExecutionQuery): Promise<DerivativeExecution[]>;
@@ -54,10 +67,19 @@ export declare class IbkrClient implements BrokerClient, DerivativeDiscoveryClie
54
67
  fetchTransactionHistory(startDate: Date, endDate: Date): Promise<BrokerTransactionHistory[]>;
55
68
  fetchOrders(options: BrokerOrdersOptions): Promise<BrokerAccountOrders[]>;
56
69
  private validateComboPreview;
70
+ private validateSingleOrder;
71
+ private validateSingleOrderFields;
57
72
  private cmeOperatorMetadata;
58
73
  private comboOrderTicket;
74
+ private singleOrderTicket;
59
75
  private normalizeOrderSubmission;
76
+ private normalizeMultiOrderSubmission;
77
+ private decodeOrderSubmission;
78
+ private singleOrderRecoveryResult;
79
+ private contingentRecoveryResult;
80
+ private submissionRecoveryReason;
60
81
  private normalizeBrokerError;
82
+ private isMeaningfulBrokerError;
61
83
  private normalizeDerivativeOrderLifecycle;
62
84
  private normalizeDerivativeExecution;
63
85
  private parseTradeTime;
@@ -69,6 +91,7 @@ export declare class IbkrClient implements BrokerClient, DerivativeDiscoveryClie
69
91
  private round;
70
92
  private parseComboLegs;
71
93
  private normalizeDerivativeOrderStatus;
94
+ private canonicalIbkrOrderStatus;
72
95
  private normalizeComboPreview;
73
96
  private whatIfMargin;
74
97
  private whatIfNumber;
@@ -1 +1 @@
1
- {"version":3,"file":"ibkrClient.d.ts","sourceRoot":"","sources":["../../src/ibkr/ibkrClient.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ibkrClient.d.ts","sourceRoot":"","sources":["../../src/ibkr/ibkrClient.ts"],"names":[],"mappings":"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"}
@@ -268,6 +268,74 @@ export class IbkrClient {
268
268
  });
269
269
  return this.normalizeOrderSubmission(response, request.clientOrderId);
270
270
  }
271
+ async submitDerivativeSingleOrder(request) {
272
+ this.validateSingleOrder(request);
273
+ const cmeOperatorMetadata = this.cmeOperatorMetadata(request.contract.assetClass, request);
274
+ const diagnostics = await this.getTradingDiagnostics(request.accountId);
275
+ if (!diagnostics.authenticated || diagnostics.competingSession) {
276
+ throw new Error("IBKR brokerage session is not safely authenticated for submission");
277
+ }
278
+ await this.prepareBrokerageAccount(request.accountId);
279
+ const response = await this.singleAttemptRequest({
280
+ path: `iserver/account/${request.accountId}/orders`,
281
+ method: "POST",
282
+ data: {
283
+ orders: [
284
+ {
285
+ ...this.singleOrderTicket(request),
286
+ ...(request.clientOrderId !== undefined ? { cOID: request.clientOrderId } : {}),
287
+ ...(request.parentId !== undefined ? { parentId: request.parentId } : {}),
288
+ ...cmeOperatorMetadata,
289
+ },
290
+ ],
291
+ },
292
+ });
293
+ return this.normalizeOrderSubmission(response, request.clientOrderId ?? null);
294
+ }
295
+ async submitDerivativeContingentOrders(request) {
296
+ const { accountId, parent, child } = request;
297
+ if (!accountId.trim())
298
+ throw new Error("An explicit IBKR account ID is required");
299
+ if (parent.accountId !== accountId || child.accountId !== accountId) {
300
+ throw new Error("Contingent parent and child orders must target the exact same account");
301
+ }
302
+ this.validateSingleOrderFields(parent);
303
+ this.validateSingleOrderFields(child);
304
+ if (typeof parent.clientOrderId !== "string" ||
305
+ !parent.clientOrderId.trim() ||
306
+ parent.clientOrderId.length > 64) {
307
+ throw new Error("Parent client order ID must contain 1 to 64 characters");
308
+ }
309
+ if ("clientOrderId" in child || "parentId" in child) {
310
+ throw new Error("Contingent child identity is derived from the parent order");
311
+ }
312
+ const parentMetadata = this.cmeOperatorMetadata(parent.contract.assetClass, parent);
313
+ const childMetadata = this.cmeOperatorMetadata(child.contract.assetClass, child);
314
+ const diagnostics = await this.getTradingDiagnostics(accountId);
315
+ if (!diagnostics.authenticated || diagnostics.competingSession) {
316
+ throw new Error("IBKR brokerage session is not safely authenticated for submission");
317
+ }
318
+ await this.prepareBrokerageAccount(accountId);
319
+ const response = await this.singleAttemptRequest({
320
+ path: `iserver/account/${accountId}/orders`,
321
+ method: "POST",
322
+ data: {
323
+ orders: [
324
+ {
325
+ ...this.singleOrderTicket(parent),
326
+ cOID: parent.clientOrderId,
327
+ ...parentMetadata,
328
+ },
329
+ {
330
+ ...this.singleOrderTicket(child),
331
+ parentId: parent.clientOrderId,
332
+ ...childMetadata,
333
+ },
334
+ ],
335
+ },
336
+ });
337
+ return this.normalizeMultiOrderSubmission(response, parent.clientOrderId);
338
+ }
271
339
  async acknowledgeOrderWarning(input) {
272
340
  if (!input.replyId.trim())
273
341
  throw new Error("An exact warning reply ID is required");
@@ -278,6 +346,24 @@ export class IbkrClient {
278
346
  });
279
347
  return this.normalizeOrderSubmission(response, null);
280
348
  }
349
+ async acknowledgeContingentOrderWarning(input) {
350
+ const confirmed = input.confirmed;
351
+ if (confirmed !== true) {
352
+ throw new Error("Order warning confirmation must be true");
353
+ }
354
+ if (!input.continuation.replyId.trim()) {
355
+ throw new Error("An exact warning reply ID is required");
356
+ }
357
+ if (!input.continuation.parentClientOrderId.trim()) {
358
+ throw new Error("An exact parent client order ID is required");
359
+ }
360
+ const response = await this.singleAttemptRequest({
361
+ path: `iserver/reply/${encodeURIComponent(input.continuation.replyId)}`,
362
+ method: "POST",
363
+ data: { confirmed: true },
364
+ });
365
+ return this.normalizeMultiOrderSubmission(response, input.continuation.parentClientOrderId);
366
+ }
281
367
  async getDerivativeOrderStatus(accountId, orderId) {
282
368
  if (!accountId.trim() || !orderId.trim()) {
283
369
  throw new Error("Exact account and order IDs are required");
@@ -616,6 +702,55 @@ export class IbkrClient {
616
702
  }
617
703
  }
618
704
  }
705
+ validateSingleOrder(request) {
706
+ this.validateSingleOrderFields(request);
707
+ const identityFields = request;
708
+ const hasClientOrderId = "clientOrderId" in identityFields;
709
+ const hasParentId = "parentId" in identityFields;
710
+ if (hasClientOrderId && typeof identityFields.clientOrderId !== "string") {
711
+ throw new Error("Client order ID must be a string");
712
+ }
713
+ if (hasParentId && typeof identityFields.parentId !== "string") {
714
+ throw new Error("Parent order ID must be a string");
715
+ }
716
+ const clientOrderId = hasClientOrderId ? identityFields.clientOrderId : undefined;
717
+ const parentId = hasParentId ? identityFields.parentId : undefined;
718
+ if (clientOrderId !== undefined && parentId !== undefined) {
719
+ throw new Error("Attached child orders must not include a client order ID");
720
+ }
721
+ const identity = clientOrderId ?? parentId;
722
+ if (!identity?.trim() || identity.length > 64) {
723
+ throw new Error(parentId === undefined
724
+ ? "Client order ID must contain 1 to 64 characters"
725
+ : "Parent order ID must contain 1 to 64 characters");
726
+ }
727
+ }
728
+ validateSingleOrderFields(request) {
729
+ if (!request.accountId.trim())
730
+ throw new Error("An explicit IBKR account ID is required");
731
+ const orderType = request.orderType;
732
+ if (orderType !== "LMT" && orderType !== "STP") {
733
+ throw new Error("Order type must be LMT or STP");
734
+ }
735
+ if (!Number.isSafeInteger(request.quantity) || request.quantity <= 0) {
736
+ throw new Error("Order quantity must be a positive integer");
737
+ }
738
+ if (!Number.isSafeInteger(request.contract.conid) || request.contract.conid <= 0) {
739
+ throw new Error("Order contract has an invalid IBKR conid");
740
+ }
741
+ if (request.orderType === "LMT") {
742
+ const limit = request.limit;
743
+ if (typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0) {
744
+ throw new Error("LIMIT order requires a positive limit price");
745
+ }
746
+ }
747
+ if (request.orderType === "STP") {
748
+ const stopPrice = request.stopPrice;
749
+ if (typeof stopPrice !== "number" || !Number.isFinite(stopPrice) || stopPrice <= 0) {
750
+ throw new Error("STOP order requires a positive stop price");
751
+ }
752
+ }
753
+ }
619
754
  cmeOperatorMetadata(assetClass, input) {
620
755
  if (assetClass === "OPT")
621
756
  return {};
@@ -643,58 +778,250 @@ export class IbkrClient {
643
778
  outsideRTH: request.session === "OVERNIGHT",
644
779
  };
645
780
  }
781
+ singleOrderTicket(request) {
782
+ return {
783
+ acctId: request.accountId,
784
+ conid: request.contract.conid,
785
+ orderType: request.orderType,
786
+ side: request.side,
787
+ price: request.orderType === "LMT" ? request.limit : request.stopPrice,
788
+ tif: request.tif,
789
+ quantity: request.quantity,
790
+ outsideRTH: request.session === "OVERNIGHT",
791
+ };
792
+ }
646
793
  normalizeOrderSubmission(response, clientOrderId) {
794
+ const decoded = this.decodeOrderSubmission(response);
795
+ const correlatedClientOrderId = decoded.orders.length === 1 ? clientOrderId : null;
796
+ const orders = decoded.orders.map((order) => ({
797
+ ...order,
798
+ clientOrderId: correlatedClientOrderId,
799
+ }));
800
+ const hasWarnings = decoded.warnings.length > 0;
801
+ const hasErrors = decoded.errors.length > 0;
802
+ const hasUnknown = decoded.unrecognizedResponses.length > 0;
803
+ const hasPendingCancel = decoded.pendingCancelOrderIds.length > 0;
804
+ if (decoded.warnings.length === 1 && !hasErrors && orders.length === 0 && !hasUnknown) {
805
+ return { state: "warning", warnings: decoded.warnings };
806
+ }
807
+ if (hasErrors &&
808
+ !decoded.responseIsArray &&
809
+ !hasWarnings &&
810
+ orders.length === 0 &&
811
+ !hasUnknown) {
812
+ return {
813
+ state: "rejected",
814
+ reasons: decoded.errors.map(({ message }) => message),
815
+ errors: decoded.errors,
816
+ };
817
+ }
818
+ if (!hasWarnings && !hasErrors && !hasUnknown && !hasPendingCancel && orders.length === 1) {
819
+ const order = orders[0];
820
+ if (order === undefined)
821
+ throw new Error("Single-order normalization lost order evidence");
822
+ if (order.status === "REJECTED" || order.status === "CANCELED") {
823
+ return {
824
+ state: "rejected",
825
+ reasons: [`Order ${order.orderId} returned terminal status ${order.status}`],
826
+ errors: [],
827
+ orders,
828
+ };
829
+ }
830
+ if (order.status !== "UNKNOWN") {
831
+ return { state: "accepted", ...order, warnings: [] };
832
+ }
833
+ }
834
+ return this.singleOrderRecoveryResult(decoded, orders);
835
+ }
836
+ normalizeMultiOrderSubmission(response, parentClientOrderId) {
837
+ const decoded = this.decodeOrderSubmission(response);
838
+ const hasDistinctBrokerOrderIds = decoded.orders.length === 2 &&
839
+ new Set(decoded.orders.map(({ orderId }) => orderId)).size === 2;
840
+ const rolesArePositionallyComplete = decoded.orders.length === 2 &&
841
+ hasDistinctBrokerOrderIds &&
842
+ decoded.warnings.length === 0 &&
843
+ decoded.errors.length === 0 &&
844
+ decoded.unrecognizedResponses.length === 0;
845
+ const orders = decoded.orders.map((order, index) => ({
846
+ ...order,
847
+ clientOrderId: rolesArePositionallyComplete && index === 0 ? parentClientOrderId : null,
848
+ role: rolesArePositionallyComplete && index === 0
849
+ ? "parent"
850
+ : rolesArePositionallyComplete && index === 1
851
+ ? "child"
852
+ : "unknown",
853
+ }));
854
+ const hasWarnings = decoded.warnings.length > 0;
855
+ const hasErrors = decoded.errors.length > 0;
856
+ const hasUnknown = decoded.unrecognizedResponses.length > 0;
857
+ if (decoded.warnings.length === 1 && !hasErrors && orders.length === 0 && !hasUnknown) {
858
+ const warning = decoded.warnings[0];
859
+ if (warning === undefined)
860
+ throw new Error("Contingent warning evidence was lost");
861
+ return {
862
+ state: "warning",
863
+ warnings: decoded.warnings,
864
+ continuation: { replyId: warning.replyId, parentClientOrderId },
865
+ };
866
+ }
867
+ if (hasErrors &&
868
+ !decoded.responseIsArray &&
869
+ !hasWarnings &&
870
+ orders.length === 0 &&
871
+ !hasUnknown) {
872
+ return {
873
+ state: "rejected",
874
+ parentClientOrderId,
875
+ reasons: decoded.errors.map(({ message }) => message),
876
+ errors: decoded.errors,
877
+ };
878
+ }
879
+ if (!hasWarnings &&
880
+ !hasErrors &&
881
+ !hasUnknown &&
882
+ decoded.pendingCancelOrderIds.length === 0 &&
883
+ orders.length === 2 &&
884
+ hasDistinctBrokerOrderIds) {
885
+ const [parent, child] = orders;
886
+ if (parent !== undefined && child !== undefined) {
887
+ const terminalFailure = orders.find(({ status }) => status === "REJECTED" || status === "CANCELED");
888
+ const unknownStatus = orders.find(({ status }) => status === "UNKNOWN");
889
+ if (terminalFailure === undefined && unknownStatus === undefined) {
890
+ return { state: "accepted", orders: [parent, child], warnings: [] };
891
+ }
892
+ }
893
+ }
894
+ return this.contingentRecoveryResult(decoded, orders, parentClientOrderId);
895
+ }
896
+ decodeOrderSubmission(response) {
647
897
  const items = Array.isArray(response) ? response : [response];
648
- const warnings = items.flatMap((item) => {
649
- if (!("id" in item) || typeof item.id !== "string")
650
- return [];
651
- const messageIds = Array.isArray(item.messageIds)
652
- ? item.messageIds.filter((value) => typeof value === "string")
653
- : [];
654
- return [
655
- {
656
- replyId: item.id,
657
- messages: Array.isArray(item.message)
658
- ? item.message.filter((value) => typeof value === "string")
898
+ const decoded = {
899
+ responseIsArray: Array.isArray(response),
900
+ orders: [],
901
+ pendingCancelOrderIds: [],
902
+ warnings: [],
903
+ errors: [],
904
+ unrecognizedResponses: [],
905
+ };
906
+ for (const item of items) {
907
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
908
+ decoded.unrecognizedResponses.push(item);
909
+ continue;
910
+ }
911
+ const record = item;
912
+ let recognized = false;
913
+ const rawWarningId = record["id"];
914
+ if (typeof rawWarningId === "string" && rawWarningId.trim()) {
915
+ const rawMessageIds = record["messageIds"];
916
+ const messageIds = Array.isArray(rawMessageIds)
917
+ ? rawMessageIds.filter((value) => typeof value === "string")
918
+ : [];
919
+ decoded.warnings.push({
920
+ replyId: rawWarningId.trim(),
921
+ messages: Array.isArray(record["message"])
922
+ ? record["message"].filter((value) => typeof value === "string")
659
923
  : [],
660
924
  messageIds,
661
- known: messageIds.length > 0 && messageIds.every((id) => KNOWN_ORDER_WARNING_IDS.has(id)),
662
- },
663
- ];
664
- });
665
- if (warnings.length > 0)
666
- return { state: "warning", warnings };
667
- const errors = items.flatMap((item) => {
668
- if (!("error" in item) || item.error === undefined || item.error === null)
669
- return [];
670
- return [this.normalizeBrokerError(item.error, item)];
671
- });
672
- if (errors.length > 0) {
673
- return { state: "rejected", reasons: errors.map(({ message }) => message), errors };
674
- }
675
- const accepted = items.find((item) => "order_id" in item || "orderId" in item);
676
- if (accepted !== undefined) {
677
- const orderId = ("order_id" in accepted ? accepted.order_id : undefined) ??
678
- ("orderId" in accepted ? accepted.orderId : undefined);
679
- if (typeof orderId === "string" || typeof orderId === "number") {
680
- const orderStatus = ("order_status" in accepted ? accepted.order_status : undefined) ??
681
- ("orderStatus" in accepted ? accepted.orderStatus : undefined);
682
- return {
683
- state: "accepted",
684
- orderId: String(orderId),
925
+ known: Array.isArray(rawMessageIds) &&
926
+ rawMessageIds.length > 0 &&
927
+ rawMessageIds.every((id) => typeof id === "string" && KNOWN_ORDER_WARNING_IDS.has(id)),
928
+ });
929
+ recognized = true;
930
+ }
931
+ else if ("id" in record) {
932
+ decoded.unrecognizedResponses.push({ ...record });
933
+ recognized = true;
934
+ }
935
+ if (record["error"] !== undefined && record["error"] !== null) {
936
+ if (this.isMeaningfulBrokerError(record["error"], record)) {
937
+ decoded.errors.push(this.normalizeBrokerError(record["error"], record));
938
+ }
939
+ else {
940
+ decoded.unrecognizedResponses.push({ ...record });
941
+ }
942
+ recognized = true;
943
+ }
944
+ const hasOrderId = "order_id" in record || "orderId" in record;
945
+ const rawOrderId = record["order_id"] ?? record["orderId"];
946
+ const orderId = typeof rawOrderId === "string" && rawOrderId.trim()
947
+ ? rawOrderId.trim()
948
+ : typeof rawOrderId === "number" && Number.isSafeInteger(rawOrderId) && rawOrderId > 0
949
+ ? String(rawOrderId)
950
+ : null;
951
+ if (orderId !== null) {
952
+ const orderStatus = record["order_status"] ?? record["orderStatus"];
953
+ if (typeof orderStatus === "string" &&
954
+ this.canonicalIbkrOrderStatus(orderStatus) === "PENDING_CANCEL") {
955
+ decoded.pendingCancelOrderIds.push(orderId);
956
+ }
957
+ decoded.orders.push({
958
+ orderId,
685
959
  status: this.normalizeDerivativeOrderStatus(typeof orderStatus === "string" ? orderStatus : undefined, 0, 0),
686
- clientOrderId,
687
- warnings: [],
688
- };
960
+ clientOrderId: null,
961
+ });
962
+ recognized = true;
689
963
  }
964
+ else if (hasOrderId) {
965
+ decoded.unrecognizedResponses.push({ ...record });
966
+ recognized = true;
967
+ }
968
+ if (!recognized)
969
+ decoded.unrecognizedResponses.push({ ...record });
690
970
  }
691
- const unknown = "IBKR returned an unknown order response";
971
+ if (items.length === 0)
972
+ decoded.unrecognizedResponses.push({});
973
+ return decoded;
974
+ }
975
+ singleOrderRecoveryResult(decoded, orders) {
692
976
  return {
693
- state: "rejected",
694
- reasons: [unknown],
695
- errors: [{ message: unknown, code: null, statusCode: null, details: {} }],
977
+ state: "recovery_required",
978
+ reasons: [this.submissionRecoveryReason(decoded, orders.length, 1)],
979
+ orders,
980
+ warnings: decoded.warnings,
981
+ errors: decoded.errors,
982
+ unrecognizedResponses: decoded.unrecognizedResponses,
696
983
  };
697
984
  }
985
+ contingentRecoveryResult(decoded, orders, parentClientOrderId) {
986
+ return {
987
+ state: "recovery_required",
988
+ parentClientOrderId,
989
+ reasons: [this.submissionRecoveryReason(decoded, orders.length, 2)],
990
+ orders,
991
+ warnings: decoded.warnings,
992
+ errors: decoded.errors,
993
+ unrecognizedResponses: decoded.unrecognizedResponses,
994
+ };
995
+ }
996
+ submissionRecoveryReason(decoded, orderCount, expectedOrderCount) {
997
+ const pendingCancelOrderId = decoded.pendingCancelOrderIds[0];
998
+ if (pendingCancelOrderId !== undefined) {
999
+ return `Order ${pendingCancelOrderId} has a pending cancellation`;
1000
+ }
1001
+ const terminal = decoded.orders.find(({ status }) => status === "REJECTED" || status === "CANCELED");
1002
+ if (terminal !== undefined) {
1003
+ return `Order ${terminal.orderId} returned terminal status ${terminal.status}`;
1004
+ }
1005
+ if (decoded.orders.some(({ status }) => status === "UNKNOWN")) {
1006
+ return "IBKR returned an order ID with an unknown status";
1007
+ }
1008
+ if (new Set(decoded.orders.map(({ orderId }) => orderId)).size < decoded.orders.length) {
1009
+ return "IBKR returned duplicate broker order IDs";
1010
+ }
1011
+ if (decoded.errors.length > 0 && decoded.warnings.length > 0) {
1012
+ return "IBKR returned both warnings and rejections for one submission";
1013
+ }
1014
+ if (decoded.warnings.length > 1) {
1015
+ return "IBKR returned multiple warning continuations for one submission";
1016
+ }
1017
+ if (orderCount !== expectedOrderCount) {
1018
+ return `IBKR returned ${String(orderCount)} of ${String(expectedOrderCount)} expected order acknowledgements`;
1019
+ }
1020
+ if (decoded.unrecognizedResponses.length > 0) {
1021
+ return "IBKR returned one or more unrecognized order responses";
1022
+ }
1023
+ return "IBKR returned mixed or incomplete order evidence";
1024
+ }
698
1025
  normalizeBrokerError(error, response) {
699
1026
  const nested = typeof error === "object" && error !== null ? error : undefined;
700
1027
  const nestedMessage = nested ? nested.message : undefined;
@@ -714,6 +1041,20 @@ export class IbkrClient {
714
1041
  details: response,
715
1042
  };
716
1043
  }
1044
+ isMeaningfulBrokerError(error, response) {
1045
+ const nested = typeof error === "object" && error !== null ? error : undefined;
1046
+ const nestedRecord = nested;
1047
+ const messages = [error, nestedRecord?.["message"], response["message"]];
1048
+ if (messages.some((value) => typeof value === "string" && value.trim()))
1049
+ return true;
1050
+ const code = nestedRecord?.["code"] ?? response["code"];
1051
+ if ((typeof code === "string" && code.trim()) ||
1052
+ (typeof code === "number" && Number.isFinite(code))) {
1053
+ return true;
1054
+ }
1055
+ const status = nestedRecord?.["statusCode"] ?? nestedRecord?.["status"] ?? response["statusCode"];
1056
+ return typeof status === "number" && Number.isFinite(status) && status >= 400;
1057
+ }
717
1058
  normalizeDerivativeOrderLifecycle(accountId, orderId, order) {
718
1059
  const quantity = this.firstPositiveNumber(order.total_size, order.totalSize, order.size);
719
1060
  const filledQuantity = this.firstNumber(order.cum_fill, order.cumFill, order.filledQuantity, order.filled);
@@ -954,10 +1295,7 @@ export class IbkrClient {
954
1295
  });
955
1296
  }
956
1297
  normalizeDerivativeOrderStatus(value, filledQuantity, remainingQuantity) {
957
- const status = value
958
- ?.replace(/([a-z])([A-Z])/g, "$1_$2")
959
- .replace(/\s+/g, "_")
960
- .toUpperCase();
1298
+ const status = this.canonicalIbkrOrderStatus(value);
961
1299
  if (filledQuantity > 0 && remainingQuantity > 0)
962
1300
  return "PARTIALLY_FILLED";
963
1301
  if (status === "FILLED")
@@ -972,6 +1310,12 @@ export class IbkrClient {
972
1310
  return "WORKING";
973
1311
  return "UNKNOWN";
974
1312
  }
1313
+ canonicalIbkrOrderStatus(value) {
1314
+ return value
1315
+ ?.replace(/([a-z])([A-Z])/g, "$1_$2")
1316
+ .replace(/\s+/g, "_")
1317
+ .toUpperCase();
1318
+ }
975
1319
  normalizeComboPreview(accountId, diagnostics, response) {
976
1320
  const commission = this.whatIfNumber(response.amount?.commission);
977
1321
  const initialMargin = this.whatIfMargin(response.initial);