@huskly/ibkr-client 0.12.0 → 0.14.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.
@@ -268,6 +268,170 @@ 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
+ }
339
+ async submitDerivativeOrderGraph(request) {
340
+ this.validateOrderGraph(request);
341
+ const diagnostics = await this.getTradingDiagnostics(request.accountId);
342
+ if (!diagnostics.authenticated || diagnostics.competingSession) {
343
+ throw new Error("IBKR brokerage session is not safely authenticated for submission");
344
+ }
345
+ await this.prepareBrokerageAccount(request.accountId);
346
+ const response = await this.singleAttemptRequest({
347
+ path: `iserver/account/${request.accountId}/orders`,
348
+ method: "POST",
349
+ data: { orders: request.nodes.map((node) => this.graphOrderTicket(request, node)) },
350
+ });
351
+ return this.normalizeOrderGraphSubmission(response, request, []);
352
+ }
353
+ async acknowledgeDerivativeOrderGraphWarning(input) {
354
+ if (input.confirmed !== true)
355
+ throw new Error("Order warning confirmation must be true");
356
+ this.validateOrderGraph(input.continuation.request);
357
+ if (!input.continuation.replyId.trim())
358
+ throw new Error("An exact warning reply ID is required");
359
+ const diagnostics = await this.getTradingDiagnostics(input.continuation.request.accountId);
360
+ if (!diagnostics.authenticated || diagnostics.competingSession) {
361
+ throw new Error("IBKR brokerage session is not safely authenticated for submission");
362
+ }
363
+ await this.prepareBrokerageAccount(input.continuation.request.accountId);
364
+ const response = await this.singleAttemptRequest({
365
+ path: `iserver/reply/${encodeURIComponent(input.continuation.replyId)}`,
366
+ method: "POST",
367
+ data: { confirmed: true },
368
+ });
369
+ return this.normalizeOrderGraphSubmission(response, input.continuation.request, input.continuation.members);
370
+ }
371
+ async recoverDerivativeOrderGraph(input, request) {
372
+ this.validateOrderGraph(request);
373
+ if (input.accountId !== request.accountId)
374
+ throw new Error("Graph recovery account does not match request");
375
+ await this.prepareBrokerageAccount(input.accountId);
376
+ const response = await this.req({
377
+ path: "iserver/account/orders",
378
+ params: { force: true, accountId: input.accountId },
379
+ });
380
+ const accountOrders = (response.orders ?? []).filter((order) => this.orderBelongsToAccount(order, input.accountId));
381
+ const byMemberId = new Map();
382
+ for (const node of request.nodes) {
383
+ const matches = accountOrders.filter((order) => this.liveOrderMatchesGraphNode(request, node, order));
384
+ const [match] = matches;
385
+ if (matches.length === 1 && match !== undefined)
386
+ byMemberId.set(node.memberId, match);
387
+ }
388
+ const candidates = [...byMemberId.values()];
389
+ const linkedOrders = accountOrders.filter((order) => (order.cOID ?? order.order_ref) === request.rootClientOrderId ||
390
+ String(order.parentId ?? "") === request.rootClientOrderId);
391
+ const candidateSet = new Set(candidates);
392
+ if (input.orderId !== undefined &&
393
+ !candidates.some((order) => String(order.order_id ?? order.orderId) === input.orderId)) {
394
+ throw new Error(`Broker order ${input.orderId} is not a member of the requested graph`);
395
+ }
396
+ if (input.rootClientOrderId !== undefined &&
397
+ input.rootClientOrderId !== request.rootClientOrderId) {
398
+ throw new Error("Root client order ID does not match the requested graph");
399
+ }
400
+ const members = request.nodes.map((node) => {
401
+ const order = byMemberId.get(node.memberId);
402
+ return this.graphMemberEvidence(request, node, order);
403
+ });
404
+ const ids = members.flatMap(({ orderId }) => (orderId === null ? [] : [orderId]));
405
+ const brokerParentsMatch = request.nodes.every((node) => {
406
+ if (node.parentMemberId === undefined)
407
+ return true;
408
+ return (byMemberId.get(node.memberId) !== undefined &&
409
+ String(byMemberId.get(node.memberId)?.parentId ?? "") === request.rootClientOrderId);
410
+ });
411
+ if (candidates.length !== request.nodes.length ||
412
+ linkedOrders.length !== request.nodes.length ||
413
+ linkedOrders.some((order) => !candidateSet.has(order)) ||
414
+ new Set(ids).size !== request.nodes.length ||
415
+ !brokerParentsMatch) {
416
+ return {
417
+ state: "recovery_required",
418
+ rootClientOrderId: request.rootClientOrderId,
419
+ members,
420
+ reasons: [
421
+ "Exact graph recovery found incomplete, duplicated, or ambiguous member evidence",
422
+ ],
423
+ warnings: [],
424
+ errors: [],
425
+ unrecognizedResponses: [],
426
+ };
427
+ }
428
+ return {
429
+ state: "accepted",
430
+ rootClientOrderId: request.rootClientOrderId,
431
+ members: this.attachGraphParentOrderIds(members),
432
+ warnings: [],
433
+ };
434
+ }
271
435
  async acknowledgeOrderWarning(input) {
272
436
  if (!input.replyId.trim())
273
437
  throw new Error("An exact warning reply ID is required");
@@ -278,6 +442,24 @@ export class IbkrClient {
278
442
  });
279
443
  return this.normalizeOrderSubmission(response, null);
280
444
  }
445
+ async acknowledgeContingentOrderWarning(input) {
446
+ const confirmed = input.confirmed;
447
+ if (confirmed !== true) {
448
+ throw new Error("Order warning confirmation must be true");
449
+ }
450
+ if (!input.continuation.replyId.trim()) {
451
+ throw new Error("An exact warning reply ID is required");
452
+ }
453
+ if (!input.continuation.parentClientOrderId.trim()) {
454
+ throw new Error("An exact parent client order ID is required");
455
+ }
456
+ const response = await this.singleAttemptRequest({
457
+ path: `iserver/reply/${encodeURIComponent(input.continuation.replyId)}`,
458
+ method: "POST",
459
+ data: { confirmed: true },
460
+ });
461
+ return this.normalizeMultiOrderSubmission(response, input.continuation.parentClientOrderId);
462
+ }
281
463
  async getDerivativeOrderStatus(accountId, orderId) {
282
464
  if (!accountId.trim() || !orderId.trim()) {
283
465
  throw new Error("Exact account and order IDs are required");
@@ -325,6 +507,59 @@ export class IbkrClient {
325
507
  }
326
508
  return this.getDerivativeOrderStatus(input.accountId, String(orderId));
327
509
  }
510
+ async listActiveDerivativeOrders(accountId) {
511
+ if (!accountId.trim())
512
+ throw new Error("An exact account ID is required");
513
+ await this.prepareBrokerageAccount(accountId);
514
+ const response = await this.req({
515
+ path: "iserver/account/orders",
516
+ params: { force: true, accountId },
517
+ });
518
+ if (response.snapshot !== true || !Array.isArray(response.orders)) {
519
+ throw new Error("IBKR active-order snapshot is incomplete");
520
+ }
521
+ const flattened = this.flattenActiveOrders(response.orders);
522
+ const invalidAccountEvidence = flattened.find(({ order }) => {
523
+ const returnedAccounts = [order.account, order.acct];
524
+ const providedAccounts = returnedAccounts.filter((value) => value !== undefined);
525
+ return (providedAccounts.length === 0 ||
526
+ providedAccounts.some((returnedAccount) => typeof returnedAccount !== "string" || returnedAccount !== accountId));
527
+ });
528
+ if (invalidAccountEvidence !== undefined) {
529
+ throw new Error("IBKR active-order response did not provide unambiguous account identity");
530
+ }
531
+ const normalized = flattened.map(({ order, nestedParent }) => this.normalizeActiveDerivativeOrder(accountId, order, nestedParent));
532
+ const byOrderId = new Map();
533
+ const byClientId = new Map();
534
+ for (const order of normalized) {
535
+ if (order.orderId !== null) {
536
+ const members = byOrderId.get(order.orderId) ?? [];
537
+ members.push(order);
538
+ byOrderId.set(order.orderId, members);
539
+ }
540
+ if (order.clientOrderId !== null) {
541
+ const members = byClientId.get(order.clientOrderId) ?? [];
542
+ members.push(order);
543
+ byClientId.set(order.clientOrderId, members);
544
+ }
545
+ }
546
+ for (const order of normalized) {
547
+ if (order.orderId !== null && (byOrderId.get(order.orderId)?.length ?? 0) > 1) {
548
+ this.addOrderUncertainty(order, "DUPLICATE_MEMBER");
549
+ }
550
+ const parentIdentity = order.parentOrderId ?? order.parentClientOrderId;
551
+ if (parentIdentity === null)
552
+ continue;
553
+ const brokerMatches = byOrderId.get(parentIdentity) ?? [];
554
+ const clientMatches = byClientId.get(parentIdentity) ?? [];
555
+ const matches = new Set([...brokerMatches, ...clientMatches]);
556
+ if (matches.size === 0)
557
+ this.addOrderUncertainty(order, "MISSING_PARENT");
558
+ if (matches.size > 1)
559
+ this.addOrderUncertainty(order, "AMBIGUOUS_PARENT");
560
+ }
561
+ return normalized;
562
+ }
328
563
  async getDerivativeExecutions(input) {
329
564
  if (!input.accountId.trim())
330
565
  throw new Error("An exact account ID is required");
@@ -616,6 +851,193 @@ export class IbkrClient {
616
851
  }
617
852
  }
618
853
  }
854
+ validateOrderGraph(request) {
855
+ if (!request.accountId.trim())
856
+ throw new Error("An explicit IBKR account ID is required");
857
+ if (!request.rootClientOrderId.trim() || request.rootClientOrderId.length > 48) {
858
+ throw new Error("Root client order ID must contain 1 to 48 characters");
859
+ }
860
+ if (request.nodes.length < 1 || request.nodes.length > 8) {
861
+ throw new Error("Derivative order graphs require 1 to 8 members");
862
+ }
863
+ const seen = new Set();
864
+ let roots = 0;
865
+ for (const node of request.nodes) {
866
+ if (!node.memberId.trim() ||
867
+ node.memberId.length > 15 ||
868
+ !/^[A-Za-z0-9_-]+$/.test(node.memberId)) {
869
+ throw new Error("Graph member IDs must contain 1 to 15 safe characters");
870
+ }
871
+ if (seen.has(node.memberId))
872
+ throw new Error("Graph member IDs must be unique");
873
+ if (node.accountId !== request.accountId)
874
+ throw new Error("Every graph member must target the graph account");
875
+ if (node.parentMemberId === undefined)
876
+ roots += 1;
877
+ else if (!seen.has(node.parentMemberId))
878
+ throw new Error("Graph parents must precede their children");
879
+ else if (node.parentMemberId !== request.nodes[0]?.memberId)
880
+ throw new Error("Derivative order graphs support only root-to-child attachments");
881
+ if ("legs" in node) {
882
+ this.validateComboPreview(node);
883
+ }
884
+ else {
885
+ this.validateGraphSingleNode(node);
886
+ }
887
+ seen.add(node.memberId);
888
+ }
889
+ if (roots !== 1 || request.nodes[0]?.parentMemberId !== undefined) {
890
+ throw new Error("Derivative order graphs require exactly one root as the first member");
891
+ }
892
+ }
893
+ validateGraphSingleNode(node) {
894
+ const orderType = node.orderType;
895
+ if (orderType !== "LMT" && orderType !== "STP" && orderType !== "MKT") {
896
+ throw new Error("Graph single order type must be LMT, STP, or MKT");
897
+ }
898
+ if (!Number.isSafeInteger(node.quantity) || node.quantity <= 0)
899
+ throw new Error("Order quantity must be a positive integer");
900
+ if (!Number.isSafeInteger(node.contract.conid) || node.contract.conid <= 0)
901
+ throw new Error("Order contract has an invalid IBKR conid");
902
+ if (node.orderType === "LMT" && (!Number.isFinite(node.limit) || node.limit <= 0))
903
+ throw new Error("LIMIT order requires a positive limit price");
904
+ if (node.orderType === "STP" && (!Number.isFinite(node.stopPrice) || node.stopPrice <= 0))
905
+ throw new Error("STOP order requires a positive stop price");
906
+ this.cmeOperatorMetadata(node.contract.assetClass, node);
907
+ }
908
+ graphClientOrderId(request, node) {
909
+ return node.parentMemberId === undefined
910
+ ? request.rootClientOrderId
911
+ : `${request.rootClientOrderId}:${node.memberId}`;
912
+ }
913
+ graphOrderTicket(request, node) {
914
+ const parent = request.nodes.find(({ memberId }) => memberId === node.parentMemberId);
915
+ if (node.parentMemberId !== undefined && parent === undefined) {
916
+ throw new Error("Graph parent evidence was lost after validation");
917
+ }
918
+ const identity = parent === undefined
919
+ ? { cOID: this.graphClientOrderId(request, node) }
920
+ : { parentId: this.graphClientOrderId(request, parent) };
921
+ if ("legs" in node)
922
+ return {
923
+ ...this.comboOrderTicket(node),
924
+ ...identity,
925
+ ...this.cmeOperatorMetadata(node.legs[0].contract.assetClass, node),
926
+ };
927
+ return {
928
+ acctId: node.accountId,
929
+ conid: node.contract.conid,
930
+ orderType: node.orderType,
931
+ side: node.side,
932
+ ...(node.orderType === "LMT"
933
+ ? { price: node.limit }
934
+ : node.orderType === "STP"
935
+ ? { price: node.stopPrice }
936
+ : {}),
937
+ tif: node.tif,
938
+ quantity: node.quantity,
939
+ outsideRTH: node.session === "OVERNIGHT",
940
+ ...identity,
941
+ ...this.cmeOperatorMetadata(node.contract.assetClass, node),
942
+ };
943
+ }
944
+ liveOrderMatchesGraphNode(request, node, order) {
945
+ if (node.parentMemberId === undefined) {
946
+ if ((order.cOID ?? order.order_ref) !== request.rootClientOrderId)
947
+ return false;
948
+ if (String(order.parentId ?? "").trim() !== "")
949
+ return false;
950
+ }
951
+ else if (String(order.parentId ?? "") !== request.rootClientOrderId)
952
+ return false;
953
+ if ("legs" in node) {
954
+ const liveLegs = this.parseComboLegs(order.conidex);
955
+ const orderType = this.normalizeOrderType(order.order_type ?? order.orderType);
956
+ const side = this.normalizeOrderSide(order.side);
957
+ const quantity = this.firstPositiveNumber(order.total_size, order.totalSize, order.size);
958
+ const price = this.firstNumber(order.limitPrice, order.limit_price, order.price);
959
+ const expectedPrice = node.priceEffect === "CREDIT" ? -node.limit : node.limit;
960
+ const outsideRth = order.outsideRTH ?? order.outside_rth;
961
+ return (liveLegs.length === node.legs.length &&
962
+ node.legs.every((leg, index) => {
963
+ const liveLeg = liveLegs[index];
964
+ return liveLeg?.conid === leg.contract.conid && liveLeg.ratio === leg.ratio;
965
+ }) &&
966
+ orderType === this.normalizeOrderType("LMT") &&
967
+ side === "BUY" &&
968
+ quantity === node.quantity &&
969
+ price === expectedPrice &&
970
+ (order.tif === undefined || order.tif.toUpperCase() === node.tif) &&
971
+ (outsideRth === undefined || outsideRth === (node.session === "OVERNIGHT")));
972
+ }
973
+ const orderType = this.normalizeOrderType(order.order_type ?? order.orderType);
974
+ const expectedOrderType = this.normalizeOrderType(node.orderType);
975
+ const side = this.normalizeOrderSide(order.side);
976
+ const quantity = this.firstPositiveNumber(order.total_size, order.totalSize, order.size);
977
+ const price = node.orderType === "LMT"
978
+ ? this.firstNumber(order.limitPrice, order.limit_price, order.price)
979
+ : node.orderType === "STP"
980
+ ? this.firstNumber(order.stopPrice, order.price)
981
+ : undefined;
982
+ const expectedPrice = node.orderType === "LMT" ? node.limit : node.orderType === "STP" ? node.stopPrice : undefined;
983
+ const outsideRth = order.outsideRTH ?? order.outside_rth;
984
+ return (order.conid === node.contract.conid &&
985
+ orderType === expectedOrderType &&
986
+ side === node.side &&
987
+ quantity === node.quantity &&
988
+ price === expectedPrice &&
989
+ (order.tif === undefined || order.tif.toUpperCase() === node.tif) &&
990
+ (outsideRth === undefined || outsideRth === (node.session === "OVERNIGHT")));
991
+ }
992
+ validateSingleOrder(request) {
993
+ this.validateSingleOrderFields(request);
994
+ const identityFields = request;
995
+ const hasClientOrderId = "clientOrderId" in identityFields;
996
+ const hasParentId = "parentId" in identityFields;
997
+ if (hasClientOrderId && typeof identityFields.clientOrderId !== "string") {
998
+ throw new Error("Client order ID must be a string");
999
+ }
1000
+ if (hasParentId && typeof identityFields.parentId !== "string") {
1001
+ throw new Error("Parent order ID must be a string");
1002
+ }
1003
+ const clientOrderId = hasClientOrderId ? identityFields.clientOrderId : undefined;
1004
+ const parentId = hasParentId ? identityFields.parentId : undefined;
1005
+ if (clientOrderId !== undefined && parentId !== undefined) {
1006
+ throw new Error("Attached child orders must not include a client order ID");
1007
+ }
1008
+ const identity = clientOrderId ?? parentId;
1009
+ if (!identity?.trim() || identity.length > 64) {
1010
+ throw new Error(parentId === undefined
1011
+ ? "Client order ID must contain 1 to 64 characters"
1012
+ : "Parent order ID must contain 1 to 64 characters");
1013
+ }
1014
+ }
1015
+ validateSingleOrderFields(request) {
1016
+ if (!request.accountId.trim())
1017
+ throw new Error("An explicit IBKR account ID is required");
1018
+ const orderType = request.orderType;
1019
+ if (orderType !== "LMT" && orderType !== "STP") {
1020
+ throw new Error("Order type must be LMT or STP");
1021
+ }
1022
+ if (!Number.isSafeInteger(request.quantity) || request.quantity <= 0) {
1023
+ throw new Error("Order quantity must be a positive integer");
1024
+ }
1025
+ if (!Number.isSafeInteger(request.contract.conid) || request.contract.conid <= 0) {
1026
+ throw new Error("Order contract has an invalid IBKR conid");
1027
+ }
1028
+ if (request.orderType === "LMT") {
1029
+ const limit = request.limit;
1030
+ if (typeof limit !== "number" || !Number.isFinite(limit) || limit <= 0) {
1031
+ throw new Error("LIMIT order requires a positive limit price");
1032
+ }
1033
+ }
1034
+ if (request.orderType === "STP") {
1035
+ const stopPrice = request.stopPrice;
1036
+ if (typeof stopPrice !== "number" || !Number.isFinite(stopPrice) || stopPrice <= 0) {
1037
+ throw new Error("STOP order requires a positive stop price");
1038
+ }
1039
+ }
1040
+ }
619
1041
  cmeOperatorMetadata(assetClass, input) {
620
1042
  if (assetClass === "OPT")
621
1043
  return {};
@@ -643,58 +1065,366 @@ export class IbkrClient {
643
1065
  outsideRTH: request.session === "OVERNIGHT",
644
1066
  };
645
1067
  }
1068
+ singleOrderTicket(request) {
1069
+ return {
1070
+ acctId: request.accountId,
1071
+ conid: request.contract.conid,
1072
+ orderType: request.orderType,
1073
+ side: request.side,
1074
+ price: request.orderType === "LMT" ? request.limit : request.stopPrice,
1075
+ tif: request.tif,
1076
+ quantity: request.quantity,
1077
+ outsideRTH: request.session === "OVERNIGHT",
1078
+ };
1079
+ }
646
1080
  normalizeOrderSubmission(response, clientOrderId) {
1081
+ const decoded = this.decodeOrderSubmission(response);
1082
+ const correlatedClientOrderId = decoded.orders.length === 1 ? clientOrderId : null;
1083
+ const orders = decoded.orders.map((order) => ({
1084
+ ...order,
1085
+ clientOrderId: correlatedClientOrderId,
1086
+ }));
1087
+ const hasWarnings = decoded.warnings.length > 0;
1088
+ const hasErrors = decoded.errors.length > 0;
1089
+ const hasUnknown = decoded.unrecognizedResponses.length > 0;
1090
+ const hasPendingCancel = decoded.pendingCancelOrderIds.length > 0;
1091
+ if (decoded.warnings.length === 1 && !hasErrors && orders.length === 0 && !hasUnknown) {
1092
+ return { state: "warning", warnings: decoded.warnings };
1093
+ }
1094
+ if (hasErrors &&
1095
+ !decoded.responseIsArray &&
1096
+ !hasWarnings &&
1097
+ orders.length === 0 &&
1098
+ !hasUnknown) {
1099
+ return {
1100
+ state: "rejected",
1101
+ reasons: decoded.errors.map(({ message }) => message),
1102
+ errors: decoded.errors,
1103
+ };
1104
+ }
1105
+ if (!hasWarnings && !hasErrors && !hasUnknown && !hasPendingCancel && orders.length === 1) {
1106
+ const order = orders[0];
1107
+ if (order === undefined)
1108
+ throw new Error("Single-order normalization lost order evidence");
1109
+ if (order.status === "REJECTED" || order.status === "CANCELED") {
1110
+ return {
1111
+ state: "rejected",
1112
+ reasons: [`Order ${order.orderId} returned terminal status ${order.status}`],
1113
+ errors: [],
1114
+ orders,
1115
+ };
1116
+ }
1117
+ if (order.status !== "UNKNOWN") {
1118
+ return { state: "accepted", ...order, warnings: [] };
1119
+ }
1120
+ }
1121
+ return this.singleOrderRecoveryResult(decoded, orders);
1122
+ }
1123
+ graphMemberEvidence(request, node, order) {
1124
+ const index = request.nodes.findIndex(({ memberId }) => memberId === node.memberId);
1125
+ let depth = 0;
1126
+ let parentId = node.parentMemberId;
1127
+ while (parentId !== undefined) {
1128
+ depth += 1;
1129
+ parentId = request.nodes.find(({ memberId }) => memberId === parentId)?.parentMemberId;
1130
+ }
1131
+ const rawId = order?.order_id ?? order?.orderId;
1132
+ const orderId = typeof rawId === "string" || typeof rawId === "number" ? String(rawId).trim() || null : null;
1133
+ const filledQuantity = this.firstNumber(order?.cum_fill, order?.cumFill, order?.filledQuantity, order?.filled);
1134
+ const quantity = this.firstPositiveNumber(order?.total_size, order?.totalSize, order?.size);
1135
+ const remainingQuantity = this.firstNumber(order?.remainingQuantity, order?.remaining_size, order?.remaining) ??
1136
+ (quantity !== undefined && filledQuantity !== undefined
1137
+ ? Math.max(0, quantity - filledQuantity)
1138
+ : undefined);
1139
+ return {
1140
+ memberId: node.memberId,
1141
+ role: index < 0
1142
+ ? "unknown"
1143
+ : depth === 0
1144
+ ? "root"
1145
+ : depth === 1
1146
+ ? "child"
1147
+ : depth === 2
1148
+ ? "grandchild"
1149
+ : "descendant",
1150
+ parentMemberId: node.parentMemberId ?? null,
1151
+ parentOrderId: null,
1152
+ orderId,
1153
+ status: order === undefined
1154
+ ? "WARNING_PENDING"
1155
+ : this.normalizeDerivativeOrderStatus(order.order_status ?? order.orderStatus ?? order.status, filledQuantity ?? 0, remainingQuantity ?? 0),
1156
+ clientOrderId: this.graphClientOrderId(request, node),
1157
+ request: node,
1158
+ };
1159
+ }
1160
+ attachGraphParentOrderIds(members) {
1161
+ const ids = new Map(members.map(({ memberId, orderId }) => [memberId, orderId]));
1162
+ return members.map((member) => ({
1163
+ ...member,
1164
+ parentOrderId: member.parentMemberId === null ? null : (ids.get(member.parentMemberId) ?? null),
1165
+ }));
1166
+ }
1167
+ normalizeOrderGraphSubmission(response, request, previousMembers) {
1168
+ const decoded = this.decodeOrderSubmission(response);
1169
+ const cleanOrders = decoded.warnings.length === 0 &&
1170
+ decoded.errors.length === 0 &&
1171
+ decoded.unrecognizedResponses.length === 0;
1172
+ const distinct = new Set(decoded.orders.map(({ orderId }) => orderId)).size === decoded.orders.length;
1173
+ const canCorrelatePositionally = cleanOrders && distinct && decoded.orders.length === request.nodes.length;
1174
+ const members = this.attachGraphParentOrderIds(request.nodes.map((node, index) => {
1175
+ const order = canCorrelatePositionally ? decoded.orders[index] : undefined;
1176
+ if (order === undefined)
1177
+ return (previousMembers.find(({ memberId }) => memberId === node.memberId) ??
1178
+ this.graphMemberEvidence(request, node));
1179
+ const evidence = this.graphMemberEvidence(request, node, {
1180
+ order_id: order.orderId,
1181
+ order_status: order.status,
1182
+ });
1183
+ return { ...evidence, status: order.status };
1184
+ }));
1185
+ if (decoded.warnings.length === 1 &&
1186
+ decoded.orders.length === 0 &&
1187
+ decoded.errors.length === 0 &&
1188
+ decoded.unrecognizedResponses.length === 0) {
1189
+ const warning = decoded.warnings[0];
1190
+ if (warning === undefined)
1191
+ throw new Error("Graph warning evidence was lost");
1192
+ return {
1193
+ state: "warning",
1194
+ rootClientOrderId: request.rootClientOrderId,
1195
+ members,
1196
+ warnings: decoded.warnings,
1197
+ continuation: { replyId: warning.replyId, request, members },
1198
+ };
1199
+ }
1200
+ if (decoded.errors.length > 0 &&
1201
+ !decoded.responseIsArray &&
1202
+ decoded.orders.length === 0 &&
1203
+ decoded.warnings.length === 0 &&
1204
+ decoded.unrecognizedResponses.length === 0) {
1205
+ return {
1206
+ state: "rejected",
1207
+ rootClientOrderId: request.rootClientOrderId,
1208
+ members,
1209
+ reasons: decoded.errors.map(({ message }) => message),
1210
+ errors: decoded.errors,
1211
+ };
1212
+ }
1213
+ if (cleanOrders &&
1214
+ distinct &&
1215
+ decoded.orders.length === request.nodes.length &&
1216
+ decoded.pendingCancelOrderIds.length === 0 &&
1217
+ decoded.orders.every(({ status }) => status !== "UNKNOWN" && status !== "REJECTED" && status !== "CANCELED")) {
1218
+ return {
1219
+ state: "accepted",
1220
+ rootClientOrderId: request.rootClientOrderId,
1221
+ members,
1222
+ warnings: [],
1223
+ };
1224
+ }
1225
+ return {
1226
+ state: "recovery_required",
1227
+ rootClientOrderId: request.rootClientOrderId,
1228
+ members,
1229
+ reasons: [
1230
+ this.submissionRecoveryReason(decoded, decoded.orders.length, request.nodes.length),
1231
+ ],
1232
+ warnings: decoded.warnings,
1233
+ errors: decoded.errors,
1234
+ unrecognizedResponses: canCorrelatePositionally
1235
+ ? decoded.unrecognizedResponses
1236
+ : [...decoded.unrecognizedResponses, ...decoded.orders],
1237
+ };
1238
+ }
1239
+ normalizeMultiOrderSubmission(response, parentClientOrderId) {
1240
+ const decoded = this.decodeOrderSubmission(response);
1241
+ const hasDistinctBrokerOrderIds = decoded.orders.length === 2 &&
1242
+ new Set(decoded.orders.map(({ orderId }) => orderId)).size === 2;
1243
+ const rolesArePositionallyComplete = decoded.orders.length === 2 &&
1244
+ hasDistinctBrokerOrderIds &&
1245
+ decoded.warnings.length === 0 &&
1246
+ decoded.errors.length === 0 &&
1247
+ decoded.unrecognizedResponses.length === 0;
1248
+ const orders = decoded.orders.map((order, index) => ({
1249
+ ...order,
1250
+ clientOrderId: rolesArePositionallyComplete && index === 0 ? parentClientOrderId : null,
1251
+ role: rolesArePositionallyComplete && index === 0
1252
+ ? "parent"
1253
+ : rolesArePositionallyComplete && index === 1
1254
+ ? "child"
1255
+ : "unknown",
1256
+ }));
1257
+ const hasWarnings = decoded.warnings.length > 0;
1258
+ const hasErrors = decoded.errors.length > 0;
1259
+ const hasUnknown = decoded.unrecognizedResponses.length > 0;
1260
+ if (decoded.warnings.length === 1 && !hasErrors && orders.length === 0 && !hasUnknown) {
1261
+ const warning = decoded.warnings[0];
1262
+ if (warning === undefined)
1263
+ throw new Error("Contingent warning evidence was lost");
1264
+ return {
1265
+ state: "warning",
1266
+ warnings: decoded.warnings,
1267
+ continuation: { replyId: warning.replyId, parentClientOrderId },
1268
+ };
1269
+ }
1270
+ if (hasErrors &&
1271
+ !decoded.responseIsArray &&
1272
+ !hasWarnings &&
1273
+ orders.length === 0 &&
1274
+ !hasUnknown) {
1275
+ return {
1276
+ state: "rejected",
1277
+ parentClientOrderId,
1278
+ reasons: decoded.errors.map(({ message }) => message),
1279
+ errors: decoded.errors,
1280
+ };
1281
+ }
1282
+ if (!hasWarnings &&
1283
+ !hasErrors &&
1284
+ !hasUnknown &&
1285
+ decoded.pendingCancelOrderIds.length === 0 &&
1286
+ orders.length === 2 &&
1287
+ hasDistinctBrokerOrderIds) {
1288
+ const [parent, child] = orders;
1289
+ if (parent !== undefined && child !== undefined) {
1290
+ const terminalFailure = orders.find(({ status }) => status === "REJECTED" || status === "CANCELED");
1291
+ const unknownStatus = orders.find(({ status }) => status === "UNKNOWN");
1292
+ if (terminalFailure === undefined && unknownStatus === undefined) {
1293
+ return { state: "accepted", orders: [parent, child], warnings: [] };
1294
+ }
1295
+ }
1296
+ }
1297
+ return this.contingentRecoveryResult(decoded, orders, parentClientOrderId);
1298
+ }
1299
+ decodeOrderSubmission(response) {
647
1300
  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")
1301
+ const decoded = {
1302
+ responseIsArray: Array.isArray(response),
1303
+ orders: [],
1304
+ pendingCancelOrderIds: [],
1305
+ warnings: [],
1306
+ errors: [],
1307
+ unrecognizedResponses: [],
1308
+ };
1309
+ for (const item of items) {
1310
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
1311
+ decoded.unrecognizedResponses.push(item);
1312
+ continue;
1313
+ }
1314
+ const record = item;
1315
+ let recognized = false;
1316
+ const rawWarningId = record["id"];
1317
+ if (typeof rawWarningId === "string" && rawWarningId.trim()) {
1318
+ const rawMessageIds = record["messageIds"];
1319
+ const messageIds = Array.isArray(rawMessageIds)
1320
+ ? rawMessageIds.filter((value) => typeof value === "string")
1321
+ : [];
1322
+ decoded.warnings.push({
1323
+ replyId: rawWarningId.trim(),
1324
+ messages: Array.isArray(record["message"])
1325
+ ? record["message"].filter((value) => typeof value === "string")
659
1326
  : [],
660
1327
  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),
1328
+ known: Array.isArray(rawMessageIds) &&
1329
+ rawMessageIds.length > 0 &&
1330
+ rawMessageIds.every((id) => typeof id === "string" && KNOWN_ORDER_WARNING_IDS.has(id)),
1331
+ });
1332
+ recognized = true;
1333
+ }
1334
+ else if ("id" in record) {
1335
+ decoded.unrecognizedResponses.push({ ...record });
1336
+ recognized = true;
1337
+ }
1338
+ if (record["error"] !== undefined && record["error"] !== null) {
1339
+ if (this.isMeaningfulBrokerError(record["error"], record)) {
1340
+ decoded.errors.push(this.normalizeBrokerError(record["error"], record));
1341
+ }
1342
+ else {
1343
+ decoded.unrecognizedResponses.push({ ...record });
1344
+ }
1345
+ recognized = true;
1346
+ }
1347
+ const hasOrderId = "order_id" in record || "orderId" in record;
1348
+ const rawOrderId = record["order_id"] ?? record["orderId"];
1349
+ const orderId = typeof rawOrderId === "string" && rawOrderId.trim()
1350
+ ? rawOrderId.trim()
1351
+ : typeof rawOrderId === "number" && Number.isSafeInteger(rawOrderId) && rawOrderId > 0
1352
+ ? String(rawOrderId)
1353
+ : null;
1354
+ if (orderId !== null) {
1355
+ const orderStatus = record["order_status"] ?? record["orderStatus"];
1356
+ if (typeof orderStatus === "string" &&
1357
+ this.canonicalIbkrOrderStatus(orderStatus) === "PENDING_CANCEL") {
1358
+ decoded.pendingCancelOrderIds.push(orderId);
1359
+ }
1360
+ decoded.orders.push({
1361
+ orderId,
685
1362
  status: this.normalizeDerivativeOrderStatus(typeof orderStatus === "string" ? orderStatus : undefined, 0, 0),
686
- clientOrderId,
687
- warnings: [],
688
- };
1363
+ clientOrderId: null,
1364
+ });
1365
+ recognized = true;
689
1366
  }
1367
+ else if (hasOrderId) {
1368
+ decoded.unrecognizedResponses.push({ ...record });
1369
+ recognized = true;
1370
+ }
1371
+ if (!recognized)
1372
+ decoded.unrecognizedResponses.push({ ...record });
690
1373
  }
691
- const unknown = "IBKR returned an unknown order response";
1374
+ if (items.length === 0)
1375
+ decoded.unrecognizedResponses.push({});
1376
+ return decoded;
1377
+ }
1378
+ singleOrderRecoveryResult(decoded, orders) {
692
1379
  return {
693
- state: "rejected",
694
- reasons: [unknown],
695
- errors: [{ message: unknown, code: null, statusCode: null, details: {} }],
1380
+ state: "recovery_required",
1381
+ reasons: [this.submissionRecoveryReason(decoded, orders.length, 1)],
1382
+ orders,
1383
+ warnings: decoded.warnings,
1384
+ errors: decoded.errors,
1385
+ unrecognizedResponses: decoded.unrecognizedResponses,
696
1386
  };
697
1387
  }
1388
+ contingentRecoveryResult(decoded, orders, parentClientOrderId) {
1389
+ return {
1390
+ state: "recovery_required",
1391
+ parentClientOrderId,
1392
+ reasons: [this.submissionRecoveryReason(decoded, orders.length, 2)],
1393
+ orders,
1394
+ warnings: decoded.warnings,
1395
+ errors: decoded.errors,
1396
+ unrecognizedResponses: decoded.unrecognizedResponses,
1397
+ };
1398
+ }
1399
+ submissionRecoveryReason(decoded, orderCount, expectedOrderCount) {
1400
+ const pendingCancelOrderId = decoded.pendingCancelOrderIds[0];
1401
+ if (pendingCancelOrderId !== undefined) {
1402
+ return `Order ${pendingCancelOrderId} has a pending cancellation`;
1403
+ }
1404
+ const terminal = decoded.orders.find(({ status }) => status === "REJECTED" || status === "CANCELED");
1405
+ if (terminal !== undefined) {
1406
+ return `Order ${terminal.orderId} returned terminal status ${terminal.status}`;
1407
+ }
1408
+ if (decoded.orders.some(({ status }) => status === "UNKNOWN")) {
1409
+ return "IBKR returned an order ID with an unknown status";
1410
+ }
1411
+ if (new Set(decoded.orders.map(({ orderId }) => orderId)).size < decoded.orders.length) {
1412
+ return "IBKR returned duplicate broker order IDs";
1413
+ }
1414
+ if (decoded.errors.length > 0 && decoded.warnings.length > 0) {
1415
+ return "IBKR returned both warnings and rejections for one submission";
1416
+ }
1417
+ if (decoded.warnings.length > 1) {
1418
+ return "IBKR returned multiple warning continuations for one submission";
1419
+ }
1420
+ if (orderCount !== expectedOrderCount) {
1421
+ return `IBKR returned ${String(orderCount)} of ${String(expectedOrderCount)} expected order acknowledgements`;
1422
+ }
1423
+ if (decoded.unrecognizedResponses.length > 0) {
1424
+ return "IBKR returned one or more unrecognized order responses";
1425
+ }
1426
+ return "IBKR returned mixed or incomplete order evidence";
1427
+ }
698
1428
  normalizeBrokerError(error, response) {
699
1429
  const nested = typeof error === "object" && error !== null ? error : undefined;
700
1430
  const nestedMessage = nested ? nested.message : undefined;
@@ -714,6 +1444,20 @@ export class IbkrClient {
714
1444
  details: response,
715
1445
  };
716
1446
  }
1447
+ isMeaningfulBrokerError(error, response) {
1448
+ const nested = typeof error === "object" && error !== null ? error : undefined;
1449
+ const nestedRecord = nested;
1450
+ const messages = [error, nestedRecord?.["message"], response["message"]];
1451
+ if (messages.some((value) => typeof value === "string" && value.trim()))
1452
+ return true;
1453
+ const code = nestedRecord?.["code"] ?? response["code"];
1454
+ if ((typeof code === "string" && code.trim()) ||
1455
+ (typeof code === "number" && Number.isFinite(code))) {
1456
+ return true;
1457
+ }
1458
+ const status = nestedRecord?.["statusCode"] ?? nestedRecord?.["status"] ?? response["statusCode"];
1459
+ return typeof status === "number" && Number.isFinite(status) && status >= 400;
1460
+ }
717
1461
  normalizeDerivativeOrderLifecycle(accountId, orderId, order) {
718
1462
  const quantity = this.firstPositiveNumber(order.total_size, order.totalSize, order.size);
719
1463
  const filledQuantity = this.firstNumber(order.cum_fill, order.cumFill, order.filledQuantity, order.filled);
@@ -742,6 +1486,167 @@ export class IbkrClient {
742
1486
  updatedAt: this.parseOrderTime(order)?.toISOString() ?? null,
743
1487
  };
744
1488
  }
1489
+ flattenActiveOrders(orders, nestedParent = null) {
1490
+ return orders.flatMap((order) => {
1491
+ const children = [...new Set([...(order.childOrders ?? []), ...(order.children ?? [])])];
1492
+ return [{ order, nestedParent }, ...this.flattenActiveOrders(children, order)];
1493
+ });
1494
+ }
1495
+ normalizeActiveDerivativeOrder(accountId, order, nestedParent) {
1496
+ const uncertainty = [];
1497
+ const total = this.firstNumber(order.total_size, order.totalSize, order.size) ?? null;
1498
+ const filled = this.firstNumber(order.cum_fill, order.cumFill, order.filledQuantity, order.filled) ?? null;
1499
+ const remaining = this.firstNumber(order.remainingQuantity, order.remaining_size, order.remaining) ??
1500
+ (total !== null && filled !== null ? Math.max(0, total - filled) : null);
1501
+ if (total === null || filled === null || remaining === null)
1502
+ uncertainty.push("INCOMPLETE_QUANTITIES");
1503
+ const rawStatus = order.order_status ?? order.orderStatus ?? order.status;
1504
+ const status = this.normalizeDerivativeOrderStatus(rawStatus, filled ?? 0, remaining ?? 0);
1505
+ if (status === "UNKNOWN")
1506
+ uncertainty.push("UNKNOWN_STATUS");
1507
+ const rawOrderId = order.order_id ?? order.orderId;
1508
+ if (rawOrderId === undefined)
1509
+ uncertainty.push("MISSING_BROKER_ORDER_ID");
1510
+ const explicitParentOrderId = order.parent_order_id ?? order.parentOrderId ?? order.parent_id;
1511
+ const explicitParentClientId = order.parentClientOrderId ?? order.parent_order_ref ?? order.parentId;
1512
+ const nestedBrokerId = nestedParent?.order_id ?? nestedParent?.orderId;
1513
+ const nestedClientId = nestedParent?.cOID ?? nestedParent?.order_ref;
1514
+ if (nestedParent !== null &&
1515
+ explicitParentOrderId === undefined &&
1516
+ explicitParentClientId === undefined) {
1517
+ uncertainty.push("PARTIAL_GRAPH");
1518
+ }
1519
+ const legs = this.normalizeActiveDerivativeLegs(order, total, uncertainty);
1520
+ const orderTime = this.parseOrderTime(order)?.toISOString() ?? null;
1521
+ return {
1522
+ accountId,
1523
+ orderId: rawOrderId === undefined ? null : String(rawOrderId),
1524
+ clientOrderId: order.cOID ?? order.order_ref ?? null,
1525
+ parentOrderId: explicitParentOrderId === undefined
1526
+ ? nestedBrokerId === undefined
1527
+ ? null
1528
+ : String(nestedBrokerId)
1529
+ : String(explicitParentOrderId),
1530
+ parentClientOrderId: explicitParentClientId === undefined
1531
+ ? (nestedClientId ?? null)
1532
+ : String(explicitParentClientId),
1533
+ graphRole: nestedParent !== null ||
1534
+ explicitParentOrderId !== undefined ||
1535
+ explicitParentClientId !== undefined
1536
+ ? "CHILD"
1537
+ : rawOrderId === undefined
1538
+ ? "UNKNOWN"
1539
+ : "ROOT",
1540
+ status,
1541
+ totalQuantity: total,
1542
+ filledQuantity: filled,
1543
+ remainingQuantity: remaining,
1544
+ tif: order.tif ?? order.timeInForce ?? null,
1545
+ session: order.outsideRTH === true || order.outside_rth === true
1546
+ ? "OVERNIGHT"
1547
+ : order.outsideRTH === false || order.outside_rth === false
1548
+ ? "REGULAR"
1549
+ : "UNKNOWN",
1550
+ orderType: this.normalizeOrderType(order.order_type ?? order.orderType) ?? null,
1551
+ limitPrice: this.firstNumber(order.limitPrice, order.limit_price, order.price) ?? null,
1552
+ stopPrice: this.firstNumber(order.stopPrice) ?? null,
1553
+ enteredAt: orderTime,
1554
+ updatedAt: order.lastExecutionTime_r !== undefined || order.lastExecutionTime !== undefined
1555
+ ? orderTime
1556
+ : null,
1557
+ legs,
1558
+ uncertainty,
1559
+ };
1560
+ }
1561
+ normalizeActiveDerivativeLegs(order, total, orderUncertainty) {
1562
+ const description = [
1563
+ order.orderDescriptionWithContract,
1564
+ order.order_description_with_contract,
1565
+ order.contractDescription1,
1566
+ order.contract_description_1,
1567
+ order.description1,
1568
+ order.symbol,
1569
+ ]
1570
+ .filter((value) => typeof value === "string")
1571
+ .join(" ");
1572
+ const describedOptions = [...description.matchAll(/([A-Z ]{1,6}\d{6}[CP]\d{8})/gi)].flatMap((match) => {
1573
+ const symbol = match[1]?.toUpperCase() ?? "";
1574
+ const parsed = parseOsiOptionSymbol(symbol);
1575
+ return parsed === null ? [] : [{ symbol, ...parsed }];
1576
+ });
1577
+ const uniqueDescribedOptions = describedOptions.filter((option, index) => describedOptions.findIndex((candidate) => candidate.symbol === option.symbol) === index);
1578
+ const side = this.normalizeOrderSide(order.side);
1579
+ const signedSide = side === "BUY" ? 1 : side === "SELL" ? -1 : null;
1580
+ let rawLegs = [];
1581
+ const conidex = typeof order.conidex === "string" ? order.conidex.trim() : null;
1582
+ if (conidex?.includes(";;;")) {
1583
+ const match = /^(\d+)(?:@[A-Za-z0-9._-]+)?;;;(.+)$/.exec(conidex);
1584
+ if (match?.[1] !== "28812380") {
1585
+ orderUncertainty.push("MALFORMED_CONIDEX");
1586
+ }
1587
+ else {
1588
+ rawLegs = (match[2] ?? "").split(",").map((member) => {
1589
+ const legMatch = /^(\d+)\/([+-]?\d+)$/.exec(member.trim());
1590
+ const conid = Number(legMatch?.[1]);
1591
+ const ratio = Number(legMatch?.[2]);
1592
+ if (!legMatch ||
1593
+ !Number.isSafeInteger(conid) ||
1594
+ conid <= 0 ||
1595
+ !Number.isSafeInteger(ratio) ||
1596
+ ratio === 0) {
1597
+ return { conid: null, ratio: null, quantityRatio: null };
1598
+ }
1599
+ return {
1600
+ conid,
1601
+ ratio: signedSide === null ? null : signedSide * ratio,
1602
+ quantityRatio: Math.abs(ratio),
1603
+ };
1604
+ });
1605
+ if (rawLegs.length === 0 || rawLegs.some((leg) => leg.conid === null)) {
1606
+ orderUncertainty.push("MALFORMED_CONIDEX");
1607
+ }
1608
+ }
1609
+ if (rawLegs.length === 0)
1610
+ orderUncertainty.push("AGGREGATE_ONLY");
1611
+ }
1612
+ else if (Number.isSafeInteger(order.conid) && Number(order.conid) > 0) {
1613
+ rawLegs = [{ conid: Number(order.conid), ratio: signedSide, quantityRatio: 1 }];
1614
+ }
1615
+ else {
1616
+ if (conidex)
1617
+ orderUncertainty.push("MALFORMED_CONIDEX");
1618
+ orderUncertainty.push("MISSING_LEG_IDENTITY");
1619
+ }
1620
+ if (rawLegs.length === 0) {
1621
+ rawLegs = [{ conid: null, ratio: null, quantityRatio: null }];
1622
+ }
1623
+ return rawLegs.map((leg) => {
1624
+ const legUncertainty = [];
1625
+ if (leg.conid === null)
1626
+ legUncertainty.push("MISSING_LEG_IDENTITY");
1627
+ if (leg.ratio === null) {
1628
+ const directionUncertainty = leg.conid !== null && signedSide === null ? "UNKNOWN_SIDE" : "MALFORMED_CONIDEX";
1629
+ legUncertainty.push(directionUncertainty);
1630
+ if (!orderUncertainty.includes(directionUncertainty)) {
1631
+ orderUncertainty.push(directionUncertainty);
1632
+ }
1633
+ }
1634
+ return {
1635
+ conid: leg.conid,
1636
+ ratio: leg.ratio,
1637
+ side: leg.ratio === null ? "UNKNOWN" : leg.ratio > 0 ? "BUY" : "SELL",
1638
+ quantity: total === null || leg.quantityRatio === null ? null : total * leg.quantityRatio,
1639
+ option: rawLegs.length === 1 && uniqueDescribedOptions.length === 1
1640
+ ? (uniqueDescribedOptions[0] ?? null)
1641
+ : null,
1642
+ uncertainty: legUncertainty,
1643
+ };
1644
+ });
1645
+ }
1646
+ addOrderUncertainty(order, uncertainty) {
1647
+ if (!order.uncertainty.includes(uncertainty))
1648
+ order.uncertainty.push(uncertainty);
1649
+ }
745
1650
  normalizeDerivativeExecution(accountId, trade) {
746
1651
  if (!trade.execution_id || !Number.isSafeInteger(trade.conid) || Number(trade.conid) <= 0) {
747
1652
  return undefined;
@@ -954,24 +1859,27 @@ export class IbkrClient {
954
1859
  });
955
1860
  }
956
1861
  normalizeDerivativeOrderStatus(value, filledQuantity, remainingQuantity) {
957
- const status = value
958
- ?.replace(/([a-z])([A-Z])/g, "$1_$2")
959
- .replace(/\s+/g, "_")
960
- .toUpperCase();
961
- if (filledQuantity > 0 && remainingQuantity > 0)
962
- return "PARTIALLY_FILLED";
1862
+ const status = this.canonicalIbkrOrderStatus(value);
963
1863
  if (status === "FILLED")
964
1864
  return "FILLED";
965
1865
  if (status === "CANCELLED" || status === "CANCELED")
966
1866
  return "CANCELED";
967
1867
  if (status === "INACTIVE" || status === "REJECTED")
968
1868
  return "REJECTED";
1869
+ if (filledQuantity > 0 && remainingQuantity > 0)
1870
+ return "PARTIALLY_FILLED";
969
1871
  if (status === "API_PENDING" || status === "PENDING_SUBMIT")
970
1872
  return "PENDING";
971
1873
  if (status !== undefined && IBKR_WORKING_STATUSES.has(status))
972
1874
  return "WORKING";
973
1875
  return "UNKNOWN";
974
1876
  }
1877
+ canonicalIbkrOrderStatus(value) {
1878
+ return value
1879
+ ?.replace(/([a-z])([A-Z])/g, "$1_$2")
1880
+ .replace(/\s+/g, "_")
1881
+ .toUpperCase();
1882
+ }
975
1883
  normalizeComboPreview(accountId, diagnostics, response) {
976
1884
  const commission = this.whatIfNumber(response.amount?.commission);
977
1885
  const initialMargin = this.whatIfMargin(response.initial);
@@ -1021,11 +1929,14 @@ export class IbkrClient {
1021
1929
  if (brokerageAccounts.accounts && !brokerageAccounts.accounts.includes(accountId)) {
1022
1930
  throw new Error(`IBKR account ${accountId} is not available for trading/order queries.`);
1023
1931
  }
1024
- await this.req({
1932
+ const switchedAccount = await this.req({
1025
1933
  path: "iserver/account",
1026
1934
  method: "POST",
1027
1935
  data: { acctId: accountId },
1028
1936
  });
1937
+ if (switchedAccount.set !== true || switchedAccount.acctId !== accountId) {
1938
+ throw new Error(`IBKR account switch was not confirmed for ${accountId}.`);
1939
+ }
1029
1940
  }
1030
1941
  normalizeStockListing(symbol, listing) {
1031
1942
  const assetType = listing.assetClass === "STK" ? "EQUITY" : listing.assetClass;