@huskly/ibkr-client 0.13.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.
@@ -336,6 +336,102 @@ export class IbkrClient {
336
336
  });
337
337
  return this.normalizeMultiOrderSubmission(response, parent.clientOrderId);
338
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
+ }
339
435
  async acknowledgeOrderWarning(input) {
340
436
  if (!input.replyId.trim())
341
437
  throw new Error("An exact warning reply ID is required");
@@ -411,6 +507,59 @@ export class IbkrClient {
411
507
  }
412
508
  return this.getDerivativeOrderStatus(input.accountId, String(orderId));
413
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
+ }
414
563
  async getDerivativeExecutions(input) {
415
564
  if (!input.accountId.trim())
416
565
  throw new Error("An exact account ID is required");
@@ -702,6 +851,144 @@ export class IbkrClient {
702
851
  }
703
852
  }
704
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
+ }
705
992
  validateSingleOrder(request) {
706
993
  this.validateSingleOrderFields(request);
707
994
  const identityFields = request;
@@ -833,6 +1120,122 @@ export class IbkrClient {
833
1120
  }
834
1121
  return this.singleOrderRecoveryResult(decoded, orders);
835
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
+ }
836
1239
  normalizeMultiOrderSubmission(response, parentClientOrderId) {
837
1240
  const decoded = this.decodeOrderSubmission(response);
838
1241
  const hasDistinctBrokerOrderIds = decoded.orders.length === 2 &&
@@ -1083,6 +1486,167 @@ export class IbkrClient {
1083
1486
  updatedAt: this.parseOrderTime(order)?.toISOString() ?? null,
1084
1487
  };
1085
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
+ }
1086
1650
  normalizeDerivativeExecution(accountId, trade) {
1087
1651
  if (!trade.execution_id || !Number.isSafeInteger(trade.conid) || Number(trade.conid) <= 0) {
1088
1652
  return undefined;
@@ -1296,14 +1860,14 @@ export class IbkrClient {
1296
1860
  }
1297
1861
  normalizeDerivativeOrderStatus(value, filledQuantity, remainingQuantity) {
1298
1862
  const status = this.canonicalIbkrOrderStatus(value);
1299
- if (filledQuantity > 0 && remainingQuantity > 0)
1300
- return "PARTIALLY_FILLED";
1301
1863
  if (status === "FILLED")
1302
1864
  return "FILLED";
1303
1865
  if (status === "CANCELLED" || status === "CANCELED")
1304
1866
  return "CANCELED";
1305
1867
  if (status === "INACTIVE" || status === "REJECTED")
1306
1868
  return "REJECTED";
1869
+ if (filledQuantity > 0 && remainingQuantity > 0)
1870
+ return "PARTIALLY_FILLED";
1307
1871
  if (status === "API_PENDING" || status === "PENDING_SUBMIT")
1308
1872
  return "PENDING";
1309
1873
  if (status !== undefined && IBKR_WORKING_STATUSES.has(status))
@@ -1365,11 +1929,14 @@ export class IbkrClient {
1365
1929
  if (brokerageAccounts.accounts && !brokerageAccounts.accounts.includes(accountId)) {
1366
1930
  throw new Error(`IBKR account ${accountId} is not available for trading/order queries.`);
1367
1931
  }
1368
- await this.req({
1932
+ const switchedAccount = await this.req({
1369
1933
  path: "iserver/account",
1370
1934
  method: "POST",
1371
1935
  data: { acctId: accountId },
1372
1936
  });
1937
+ if (switchedAccount.set !== true || switchedAccount.acctId !== accountId) {
1938
+ throw new Error(`IBKR account switch was not confirmed for ${accountId}.`);
1939
+ }
1373
1940
  }
1374
1941
  normalizeStockListing(symbol, listing) {
1375
1942
  const assetType = listing.assetClass === "STK" ? "EQUITY" : listing.assetClass;