@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.
- package/README.md +51 -8
- package/dist/ibkr/ibkrApiTypes.d.ts +14 -0
- package/dist/ibkr/ibkrApiTypes.d.ts.map +1 -1
- package/dist/ibkr/ibkrClient.d.ts +20 -1
- package/dist/ibkr/ibkrClient.d.ts.map +1 -1
- package/dist/ibkr/ibkrClient.js +570 -3
- package/dist/ibkr/ibkrClient.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +133 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
package/dist/ibkr/ibkrClient.js
CHANGED
|
@@ -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;
|