@huskly/ibkr-client 0.13.0 → 0.15.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 +60 -8
- package/dist/ibkr/ibkrApiTypes.d.ts +14 -0
- package/dist/ibkr/ibkrApiTypes.d.ts.map +1 -1
- package/dist/ibkr/ibkrClient.d.ts +38 -1
- package/dist/ibkr/ibkrClient.d.ts.map +1 -1
- package/dist/ibkr/ibkrClient.js +1112 -9
- 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
|
@@ -74,6 +74,7 @@ const IBKR_WORKING_STATUSES = new Set([
|
|
|
74
74
|
"SUBMITTED",
|
|
75
75
|
"PENDING_CANCEL",
|
|
76
76
|
]);
|
|
77
|
+
const RECOVERY_TERMINAL_ORDER_FILTERS = ["filled", "cancelled", "inactive"];
|
|
77
78
|
const KNOWN_ORDER_WARNING_IDS = new Set(["o163"]);
|
|
78
79
|
/** Extract the canonical OSI symbol embedded in an IBKR option description. */
|
|
79
80
|
function extractOsiPositionSymbol(contractDescription) {
|
|
@@ -100,6 +101,17 @@ function isHeadersLike(input) {
|
|
|
100
101
|
input !== null &&
|
|
101
102
|
typeof input.get === "function");
|
|
102
103
|
}
|
|
104
|
+
function isIbkrTrade(input) {
|
|
105
|
+
if (typeof input !== "object" || input === null || Array.isArray(input))
|
|
106
|
+
return false;
|
|
107
|
+
const record = input;
|
|
108
|
+
return ((record["account"] === undefined || typeof record["account"] === "string") &&
|
|
109
|
+
(record["accountCode"] === undefined || typeof record["accountCode"] === "string") &&
|
|
110
|
+
(record["order_ref"] === undefined || typeof record["order_ref"] === "string") &&
|
|
111
|
+
(record["order_id"] === undefined ||
|
|
112
|
+
typeof record["order_id"] === "string" ||
|
|
113
|
+
typeof record["order_id"] === "number"));
|
|
114
|
+
}
|
|
103
115
|
function headerToString(value) {
|
|
104
116
|
if (value === undefined || value === null)
|
|
105
117
|
return undefined;
|
|
@@ -336,6 +348,532 @@ export class IbkrClient {
|
|
|
336
348
|
});
|
|
337
349
|
return this.normalizeMultiOrderSubmission(response, parent.clientOrderId);
|
|
338
350
|
}
|
|
351
|
+
async submitDerivativeOrderGraph(request) {
|
|
352
|
+
this.validateOrderGraph(request);
|
|
353
|
+
const diagnostics = await this.getTradingDiagnostics(request.accountId);
|
|
354
|
+
if (!diagnostics.authenticated || diagnostics.competingSession) {
|
|
355
|
+
throw new Error("IBKR brokerage session is not safely authenticated for submission");
|
|
356
|
+
}
|
|
357
|
+
await this.prepareBrokerageAccount(request.accountId);
|
|
358
|
+
const response = await this.singleAttemptRequest({
|
|
359
|
+
path: `iserver/account/${request.accountId}/orders`,
|
|
360
|
+
method: "POST",
|
|
361
|
+
data: { orders: request.nodes.map((node) => this.graphOrderTicket(request, node)) },
|
|
362
|
+
});
|
|
363
|
+
return this.normalizeOrderGraphSubmission(response, request, []);
|
|
364
|
+
}
|
|
365
|
+
async acknowledgeDerivativeOrderGraphWarning(input) {
|
|
366
|
+
if (input.confirmed !== true)
|
|
367
|
+
throw new Error("Order warning confirmation must be true");
|
|
368
|
+
this.validateOrderGraph(input.continuation.request);
|
|
369
|
+
if (!input.continuation.replyId.trim())
|
|
370
|
+
throw new Error("An exact warning reply ID is required");
|
|
371
|
+
const diagnostics = await this.getTradingDiagnostics(input.continuation.request.accountId);
|
|
372
|
+
if (!diagnostics.authenticated || diagnostics.competingSession) {
|
|
373
|
+
throw new Error("IBKR brokerage session is not safely authenticated for submission");
|
|
374
|
+
}
|
|
375
|
+
await this.prepareBrokerageAccount(input.continuation.request.accountId);
|
|
376
|
+
const response = await this.singleAttemptRequest({
|
|
377
|
+
path: `iserver/reply/${encodeURIComponent(input.continuation.replyId)}`,
|
|
378
|
+
method: "POST",
|
|
379
|
+
data: { confirmed: true },
|
|
380
|
+
});
|
|
381
|
+
return this.normalizeOrderGraphSubmission(response, input.continuation.request, input.continuation.members);
|
|
382
|
+
}
|
|
383
|
+
async recoverDerivativeOrderGraph(input, request) {
|
|
384
|
+
this.validateOrderGraph(request);
|
|
385
|
+
if (input.accountId !== request.accountId)
|
|
386
|
+
throw new Error("Graph recovery account does not match request");
|
|
387
|
+
if (input.rootClientOrderId !== undefined &&
|
|
388
|
+
input.rootClientOrderId !== request.rootClientOrderId) {
|
|
389
|
+
throw new Error("Root client order ID does not match the requested graph");
|
|
390
|
+
}
|
|
391
|
+
if (input.orderId !== undefined && !input.orderId.trim()) {
|
|
392
|
+
throw new Error("An exact broker order ID is required for graph recovery");
|
|
393
|
+
}
|
|
394
|
+
await this.prepareBrokerageAccount(input.accountId);
|
|
395
|
+
const response = await this.req({
|
|
396
|
+
path: "iserver/account/orders",
|
|
397
|
+
params: { force: true, accountId: input.accountId },
|
|
398
|
+
});
|
|
399
|
+
const flattenedActiveSnapshot = this.flattenCompleteOrderSnapshot(response);
|
|
400
|
+
const activeSnapshotIncomplete = flattenedActiveSnapshot === null;
|
|
401
|
+
const invalidActiveAccountEvidence = flattenedActiveSnapshot?.some(({ order }) => !this.orderHasExactAccount(order, input.accountId)) ?? false;
|
|
402
|
+
const invalidNestedActiveEvidence = flattenedActiveSnapshot?.some(({ order, nestedParent }) => nestedParent !== null &&
|
|
403
|
+
!this.recoveryGraphOrderMayBeAttached(request, order) &&
|
|
404
|
+
this.recoveryGraphOrderIsAttached(request, nestedParent)) ?? false;
|
|
405
|
+
const accountOrders = (flattenedActiveSnapshot ?? [])
|
|
406
|
+
.map(({ order }) => order)
|
|
407
|
+
.filter((order) => this.orderHasExactAccount(order, input.accountId));
|
|
408
|
+
const activeMatchesByMember = new Map();
|
|
409
|
+
const observedCandidates = activeSnapshotIncomplete || invalidActiveAccountEvidence || invalidNestedActiveEvidence
|
|
410
|
+
? [response]
|
|
411
|
+
: [];
|
|
412
|
+
observedCandidates.push(...accountOrders.filter((order) => this.recoveryGraphOrderMayBeAttached(request, order)));
|
|
413
|
+
for (const node of request.nodes) {
|
|
414
|
+
const matches = accountOrders.filter((order) => this.terminalOrderTicketIsValid(order) &&
|
|
415
|
+
this.orderHasValidRecoveryStatus(order) &&
|
|
416
|
+
this.recoveryOrderMatchesGraphNode(request, node, order));
|
|
417
|
+
activeMatchesByMember.set(node.memberId, matches);
|
|
418
|
+
}
|
|
419
|
+
const selected = new Map();
|
|
420
|
+
const usedOrderIds = new Set();
|
|
421
|
+
for (const node of request.nodes) {
|
|
422
|
+
const matches = activeMatchesByMember.get(node.memberId) ?? [];
|
|
423
|
+
const [match] = matches;
|
|
424
|
+
if (matches.length !== 1 || match === undefined)
|
|
425
|
+
continue;
|
|
426
|
+
const orderId = this.recoveryOrderId(match);
|
|
427
|
+
if (orderId === undefined)
|
|
428
|
+
continue;
|
|
429
|
+
if (usedOrderIds.has(orderId))
|
|
430
|
+
continue;
|
|
431
|
+
usedOrderIds.add(orderId);
|
|
432
|
+
selected.set(node.memberId, match);
|
|
433
|
+
}
|
|
434
|
+
const unresolved = request.nodes.filter((node) => !selected.has(node.memberId));
|
|
435
|
+
const terminalEvidence = await this.findRecoveryGraphTerminalCandidates(input.accountId, request, input.orderId);
|
|
436
|
+
this.reconcileSelectedGraphMembers(request, selected, terminalEvidence);
|
|
437
|
+
if (unresolved.length > 0) {
|
|
438
|
+
const assignments = this.assignRecoveryGraphCandidates(unresolved, terminalEvidence.byNode, usedOrderIds);
|
|
439
|
+
for (const node of unresolved) {
|
|
440
|
+
const order = assignments.get(node.memberId);
|
|
441
|
+
if (order === undefined)
|
|
442
|
+
continue;
|
|
443
|
+
selected.set(node.memberId, order);
|
|
444
|
+
const orderId = this.recoveryOrderId(order);
|
|
445
|
+
if (orderId !== undefined)
|
|
446
|
+
usedOrderIds.add(orderId);
|
|
447
|
+
observedCandidates.push(order);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
observedCandidates.push(...terminalEvidence.observedResponses);
|
|
451
|
+
if (terminalEvidence.invalidAttachedEvidence) {
|
|
452
|
+
observedCandidates.push({ reason: "Invalid or conflicting terminal broker evidence" });
|
|
453
|
+
}
|
|
454
|
+
const selectedCandidates = [...selected.values()];
|
|
455
|
+
const requestedOrderIdMissing = input.orderId !== undefined &&
|
|
456
|
+
!selectedCandidates.some((order) => this.recoveryOrderId(order) === input.orderId);
|
|
457
|
+
const members = request.nodes.map((node) => {
|
|
458
|
+
const order = selected.get(node.memberId);
|
|
459
|
+
return this.graphMemberEvidence(request, node, order);
|
|
460
|
+
});
|
|
461
|
+
const ids = members.flatMap(({ orderId }) => (orderId === null ? [] : [orderId]));
|
|
462
|
+
const hasDistinctOrderIds = new Set(ids).size === request.nodes.length;
|
|
463
|
+
const linkedOrderIds = new Set();
|
|
464
|
+
let linkedOrderMissingBrokerId = false;
|
|
465
|
+
for (const order of accountOrders) {
|
|
466
|
+
if (!this.recoveryGraphOrderMayBeAttached(request, order))
|
|
467
|
+
continue;
|
|
468
|
+
if (!this.recoveryGraphOrderIsAttached(request, order)) {
|
|
469
|
+
linkedOrderMissingBrokerId = true;
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
const orderId = this.recoveryOrderId(order);
|
|
473
|
+
if (orderId === undefined) {
|
|
474
|
+
linkedOrderMissingBrokerId = true;
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
linkedOrderIds.add(orderId);
|
|
478
|
+
}
|
|
479
|
+
for (const order of terminalEvidence.linkedOrders) {
|
|
480
|
+
const orderId = this.recoveryOrderId(order);
|
|
481
|
+
if (orderId === undefined) {
|
|
482
|
+
linkedOrderMissingBrokerId = true;
|
|
483
|
+
continue;
|
|
484
|
+
}
|
|
485
|
+
linkedOrderIds.add(orderId);
|
|
486
|
+
}
|
|
487
|
+
const selectedOrderIds = new Set(selectedCandidates.flatMap((order) => {
|
|
488
|
+
const orderId = this.recoveryOrderId(order);
|
|
489
|
+
return orderId === undefined ? [] : [orderId];
|
|
490
|
+
}));
|
|
491
|
+
if (selected.size !== request.nodes.length ||
|
|
492
|
+
!hasDistinctOrderIds ||
|
|
493
|
+
linkedOrderMissingBrokerId ||
|
|
494
|
+
[...linkedOrderIds].some((orderId) => !selectedOrderIds.has(orderId)) ||
|
|
495
|
+
requestedOrderIdMissing ||
|
|
496
|
+
activeSnapshotIncomplete ||
|
|
497
|
+
invalidActiveAccountEvidence ||
|
|
498
|
+
invalidNestedActiveEvidence ||
|
|
499
|
+
terminalEvidence.invalidAttachedEvidence ||
|
|
500
|
+
terminalEvidence.terminalSnapshotLookupFailed ||
|
|
501
|
+
members.some(({ status }) => status === "UNKNOWN" || status === "WARNING_PENDING")) {
|
|
502
|
+
return {
|
|
503
|
+
state: "recovery_required",
|
|
504
|
+
rootClientOrderId: request.rootClientOrderId,
|
|
505
|
+
members,
|
|
506
|
+
reasons: [
|
|
507
|
+
"Exact graph recovery found incomplete, duplicated, or ambiguous member evidence",
|
|
508
|
+
],
|
|
509
|
+
warnings: [],
|
|
510
|
+
errors: [],
|
|
511
|
+
unrecognizedResponses: observedCandidates,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return {
|
|
515
|
+
state: "accepted",
|
|
516
|
+
rootClientOrderId: request.rootClientOrderId,
|
|
517
|
+
members: this.attachGraphParentOrderIds(members),
|
|
518
|
+
warnings: [],
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
reconcileSelectedGraphMembers(request, selected, terminalEvidence) {
|
|
522
|
+
for (const node of request.nodes) {
|
|
523
|
+
const selectedOrder = selected.get(node.memberId);
|
|
524
|
+
const selectedOrderId = selectedOrder === undefined ? undefined : this.recoveryOrderId(selectedOrder);
|
|
525
|
+
if (selectedOrderId === undefined)
|
|
526
|
+
continue;
|
|
527
|
+
const terminalMatches = new Map();
|
|
528
|
+
for (const order of terminalEvidence.byNode.get(node.memberId) ?? []) {
|
|
529
|
+
const orderId = this.recoveryOrderId(order);
|
|
530
|
+
if (orderId === selectedOrderId)
|
|
531
|
+
terminalMatches.set(orderId, order);
|
|
532
|
+
}
|
|
533
|
+
const terminalLinked = terminalMatches.size > 0 ||
|
|
534
|
+
terminalEvidence.linkedOrders.some((order) => this.recoveryOrderId(order) === selectedOrderId && this.isTerminalRecoveryOrder(order));
|
|
535
|
+
if (!terminalLinked)
|
|
536
|
+
continue;
|
|
537
|
+
if (terminalMatches.size !== 1) {
|
|
538
|
+
terminalEvidence.invalidAttachedEvidence = true;
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const [terminalOrder] = terminalMatches.values();
|
|
542
|
+
if (terminalOrder !== undefined)
|
|
543
|
+
selected.set(node.memberId, terminalOrder);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
isTerminalRecoveryOrder(order) {
|
|
547
|
+
const status = this.canonicalIbkrOrderStatus(order.order_status ?? order.orderStatus ?? order.status);
|
|
548
|
+
return (status === "FILLED" ||
|
|
549
|
+
status === "CANCELLED" ||
|
|
550
|
+
status === "INACTIVE" ||
|
|
551
|
+
status === "REJECTED");
|
|
552
|
+
}
|
|
553
|
+
async findRecoveryGraphTerminalCandidates(accountId, request, knownOrderId) {
|
|
554
|
+
const byNode = new Map();
|
|
555
|
+
for (const node of request.nodes)
|
|
556
|
+
byNode.set(node.memberId, []);
|
|
557
|
+
const linkedOrders = [];
|
|
558
|
+
const observedResponses = [];
|
|
559
|
+
let invalidAttachedEvidence = false;
|
|
560
|
+
let terminalSnapshotLookupFailed = false;
|
|
561
|
+
const candidateOrderIds = new Set();
|
|
562
|
+
if (knownOrderId !== undefined)
|
|
563
|
+
candidateOrderIds.add(knownOrderId);
|
|
564
|
+
const terminalOrdersById = new Map();
|
|
565
|
+
for (const filter of RECOVERY_TERMINAL_ORDER_FILTERS) {
|
|
566
|
+
try {
|
|
567
|
+
const response = await this.req({
|
|
568
|
+
path: "iserver/account/orders",
|
|
569
|
+
params: { force: true, accountId, filters: filter },
|
|
570
|
+
});
|
|
571
|
+
const flattenedSnapshot = this.flattenCompleteOrderSnapshot(response);
|
|
572
|
+
if (flattenedSnapshot === null) {
|
|
573
|
+
terminalSnapshotLookupFailed = true;
|
|
574
|
+
observedResponses.push({ source: "terminal_order_snapshot", filter, response });
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
for (const { order, nestedParent } of flattenedSnapshot) {
|
|
578
|
+
if (!this.recoveryGraphOrderMayBeAttached(request, order)) {
|
|
579
|
+
if (nestedParent === null ||
|
|
580
|
+
!this.recoveryGraphOrderIsAttached(request, nestedParent)) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
observedResponses.push(order);
|
|
584
|
+
linkedOrders.push(order);
|
|
585
|
+
invalidAttachedEvidence = true;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
observedResponses.push(order);
|
|
589
|
+
if (!this.recoveryGraphOrderIsAttached(request, order)) {
|
|
590
|
+
linkedOrders.push(order);
|
|
591
|
+
invalidAttachedEvidence = true;
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
if (!this.orderHasExactAccount(order, accountId)) {
|
|
595
|
+
invalidAttachedEvidence = true;
|
|
596
|
+
continue;
|
|
597
|
+
}
|
|
598
|
+
if (!this.terminalOrderTicketIsValid(order) || !this.orderHasValidRecoveryStatus(order)) {
|
|
599
|
+
linkedOrders.push(order);
|
|
600
|
+
invalidAttachedEvidence = true;
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
const orderId = this.recoveryOrderId(order);
|
|
604
|
+
if (orderId === undefined) {
|
|
605
|
+
linkedOrders.push(order);
|
|
606
|
+
invalidAttachedEvidence = true;
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
const previousOrder = terminalOrdersById.get(orderId);
|
|
610
|
+
if (previousOrder !== undefined &&
|
|
611
|
+
(this.terminalOrderTicketConflicts(previousOrder, order) ||
|
|
612
|
+
this.recoveryGraphAttachmentKey(request, previousOrder) !==
|
|
613
|
+
this.recoveryGraphAttachmentKey(request, order))) {
|
|
614
|
+
invalidAttachedEvidence = true;
|
|
615
|
+
linkedOrders.push(order);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
terminalOrdersById.set(orderId, order);
|
|
619
|
+
linkedOrders.push(order);
|
|
620
|
+
candidateOrderIds.add(orderId);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
terminalSnapshotLookupFailed = true;
|
|
625
|
+
observedResponses.push({
|
|
626
|
+
source: "terminal_order_snapshot",
|
|
627
|
+
filter,
|
|
628
|
+
error: error instanceof Error ? error.message : String(error),
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const tradeEvidenceById = new Map();
|
|
633
|
+
try {
|
|
634
|
+
const response = await this.req({
|
|
635
|
+
path: "iserver/account/trades",
|
|
636
|
+
params: { days: 7 },
|
|
637
|
+
});
|
|
638
|
+
if (Array.isArray(response)) {
|
|
639
|
+
for (const rawTrade of response) {
|
|
640
|
+
if (typeof rawTrade !== "object" || rawTrade === null || Array.isArray(rawTrade))
|
|
641
|
+
continue;
|
|
642
|
+
const tradeRecord = rawTrade;
|
|
643
|
+
const orderRef = this.trimmedString(tradeRecord["order_ref"]);
|
|
644
|
+
const parentRef = this.recoveryOrderId({
|
|
645
|
+
order_id: tradeRecord["parent_order_ref"],
|
|
646
|
+
orderId: tradeRecord["parentOrderRef"],
|
|
647
|
+
});
|
|
648
|
+
if (orderRef !== request.rootClientOrderId && parentRef !== request.rootClientOrderId) {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const accounts = [tradeRecord["account"], tradeRecord["accountCode"]].filter((value) => value !== undefined);
|
|
652
|
+
if (accounts.length > 0 &&
|
|
653
|
+
accounts.every((value) => typeof value === "string" && value !== accountId)) {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (accounts.length === 0 ||
|
|
657
|
+
accounts.some((value) => typeof value !== "string" || value !== accountId) ||
|
|
658
|
+
!isIbkrTrade(rawTrade)) {
|
|
659
|
+
observedResponses.push(rawTrade);
|
|
660
|
+
invalidAttachedEvidence = true;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const orderId = this.recoveryOrderId(rawTrade);
|
|
664
|
+
if (orderId === undefined) {
|
|
665
|
+
observedResponses.push(rawTrade);
|
|
666
|
+
invalidAttachedEvidence = true;
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
candidateOrderIds.add(orderId);
|
|
670
|
+
tradeEvidenceById.set(orderId, {
|
|
671
|
+
...rawTrade,
|
|
672
|
+
account: accountId,
|
|
673
|
+
order_id: orderId,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
// Exact broker IDs and terminal order snapshots remain usable when trade history is unavailable.
|
|
680
|
+
}
|
|
681
|
+
for (const orderId of candidateOrderIds) {
|
|
682
|
+
const terminalOrder = terminalOrdersById.get(orderId);
|
|
683
|
+
try {
|
|
684
|
+
const order = await this.req({
|
|
685
|
+
path: `iserver/account/order/status/${encodeURIComponent(orderId)}`,
|
|
686
|
+
});
|
|
687
|
+
observedResponses.push(order);
|
|
688
|
+
if (!this.orderMatchesExactRecoveryIdentity(order, accountId, orderId)) {
|
|
689
|
+
invalidAttachedEvidence = true;
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
if (!this.orderHasValidRecoveryStatus(order)) {
|
|
693
|
+
invalidAttachedEvidence = true;
|
|
694
|
+
terminalOrdersById.delete(orderId);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (!this.terminalOrderTicketIsValid(order)) {
|
|
698
|
+
invalidAttachedEvidence = true;
|
|
699
|
+
terminalOrdersById.delete(orderId);
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (terminalOrder !== undefined &&
|
|
703
|
+
this.terminalOrderTicketConflicts(terminalOrder, order)) {
|
|
704
|
+
invalidAttachedEvidence = true;
|
|
705
|
+
terminalOrdersById.delete(orderId);
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (terminalOrder !== undefined &&
|
|
709
|
+
this.recoveryGraphAttachmentKey(request, terminalOrder) !==
|
|
710
|
+
this.recoveryGraphAttachmentKey(request, order)) {
|
|
711
|
+
invalidAttachedEvidence = true;
|
|
712
|
+
terminalOrdersById.delete(orderId);
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
if (!this.recoveryGraphOrderIsAttached(request, order)) {
|
|
716
|
+
invalidAttachedEvidence = true;
|
|
717
|
+
terminalOrdersById.delete(orderId);
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const resolvedOrder = terminalOrder === undefined ? order : { ...terminalOrder, ...order };
|
|
721
|
+
terminalOrdersById.set(orderId, resolvedOrder);
|
|
722
|
+
if (terminalOrder === undefined)
|
|
723
|
+
linkedOrders.push(resolvedOrder);
|
|
724
|
+
}
|
|
725
|
+
catch (error) {
|
|
726
|
+
observedResponses.push({
|
|
727
|
+
source: "terminal_order_status",
|
|
728
|
+
orderId,
|
|
729
|
+
error: error instanceof Error ? error.message : String(error),
|
|
730
|
+
});
|
|
731
|
+
if (terminalOrder === undefined) {
|
|
732
|
+
const tradeEvidence = tradeEvidenceById.get(orderId);
|
|
733
|
+
if (tradeEvidence !== undefined) {
|
|
734
|
+
linkedOrders.push(tradeEvidence);
|
|
735
|
+
observedResponses.push(tradeEvidence);
|
|
736
|
+
}
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const resolvedOrder = terminalOrdersById.get(orderId) ?? terminalOrder;
|
|
741
|
+
if (resolvedOrder === undefined)
|
|
742
|
+
continue;
|
|
743
|
+
if (!this.orderHasExactAccount(resolvedOrder, accountId)) {
|
|
744
|
+
invalidAttachedEvidence = true;
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
for (const node of request.nodes) {
|
|
748
|
+
if (this.terminalOrderMatchesGraphNode(request, node, resolvedOrder)) {
|
|
749
|
+
const existing = byNode.get(node.memberId);
|
|
750
|
+
if (existing !== undefined)
|
|
751
|
+
existing.push(resolvedOrder);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return {
|
|
756
|
+
byNode,
|
|
757
|
+
linkedOrders,
|
|
758
|
+
observedResponses,
|
|
759
|
+
invalidAttachedEvidence,
|
|
760
|
+
terminalSnapshotLookupFailed,
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
recoveryGraphOrderIsAttached(request, order) {
|
|
764
|
+
return this.recoveryGraphAttachmentKey(request, order) !== null;
|
|
765
|
+
}
|
|
766
|
+
recoveryGraphOrderMayBeAttached(request, order) {
|
|
767
|
+
return [
|
|
768
|
+
order.cOID,
|
|
769
|
+
order.order_ref,
|
|
770
|
+
order.parentId,
|
|
771
|
+
order.parent_id,
|
|
772
|
+
order.parentClientOrderId,
|
|
773
|
+
order.parent_order_ref,
|
|
774
|
+
].some((value) => (typeof value === "string" || typeof value === "number") &&
|
|
775
|
+
String(value).trim() === request.rootClientOrderId);
|
|
776
|
+
}
|
|
777
|
+
recoveryGraphAttachmentKey(request, order) {
|
|
778
|
+
const parentIdentity = this.consistentStringAliases(order.parentId, order.parent_id, order.parentClientOrderId, order.parent_order_ref);
|
|
779
|
+
const clientIdentity = this.consistentStringAliases(order.cOID, order.order_ref);
|
|
780
|
+
if (!parentIdentity.valid || !clientIdentity.valid)
|
|
781
|
+
return null;
|
|
782
|
+
const parentId = parentIdentity.value ?? "";
|
|
783
|
+
if (parentId === request.rootClientOrderId)
|
|
784
|
+
return "child";
|
|
785
|
+
const clientOrderId = clientIdentity.value;
|
|
786
|
+
if (clientOrderId === request.rootClientOrderId && parentId === "")
|
|
787
|
+
return "root";
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
terminalOrderTicketConflicts(first, second) {
|
|
791
|
+
const firstTicket = this.terminalOrderTicketFingerprint(first);
|
|
792
|
+
const secondTicket = this.terminalOrderTicketFingerprint(second);
|
|
793
|
+
return Object.keys(firstTicket).some((field) => field in secondTicket && firstTicket[field] !== secondTicket[field]);
|
|
794
|
+
}
|
|
795
|
+
terminalOrderTicketIsValid(order) {
|
|
796
|
+
return !Object.values(this.terminalOrderTicketFingerprint(order)).includes("__MALFORMED_TERMINAL_TICKET_FIELD__");
|
|
797
|
+
}
|
|
798
|
+
terminalOrderTicketFingerprint(order) {
|
|
799
|
+
const ticket = {};
|
|
800
|
+
const malformed = "__MALFORMED_TERMINAL_TICKET_FIELD__";
|
|
801
|
+
const addAliases = (field, values, normalize) => {
|
|
802
|
+
const provided = values.filter((value) => value !== undefined);
|
|
803
|
+
if (provided.length === 0)
|
|
804
|
+
return;
|
|
805
|
+
const normalized = provided.map(normalize);
|
|
806
|
+
const [first] = normalized;
|
|
807
|
+
ticket[field] =
|
|
808
|
+
first !== undefined && normalized.every((value) => value === first) ? first : malformed;
|
|
809
|
+
};
|
|
810
|
+
const normalizeNumber = (value) => {
|
|
811
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
812
|
+
return String(value);
|
|
813
|
+
}
|
|
814
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
815
|
+
const numeric = Number(value);
|
|
816
|
+
return Number.isFinite(numeric) ? String(numeric) : undefined;
|
|
817
|
+
}
|
|
818
|
+
return undefined;
|
|
819
|
+
};
|
|
820
|
+
addAliases("conid", [order.conid], normalizeNumber);
|
|
821
|
+
if (order.conidex !== undefined) {
|
|
822
|
+
ticket["conidex"] =
|
|
823
|
+
typeof order.conidex === "string"
|
|
824
|
+
? JSON.stringify(this.parseComboLegs(order.conidex))
|
|
825
|
+
: malformed;
|
|
826
|
+
}
|
|
827
|
+
addAliases("orderType", [order.order_type, order.orderType], (value) => typeof value === "string" ? this.normalizeOrderType(value) : undefined);
|
|
828
|
+
addAliases("side", [order.side], (value) => typeof value === "string" ? this.normalizeOrderSide(value) : undefined);
|
|
829
|
+
addAliases("quantity", [order.total_size, order.totalSize, order.size], normalizeNumber);
|
|
830
|
+
addAliases("price", [order.limitPrice, order.limit_price, order.stopPrice, order.price], normalizeNumber);
|
|
831
|
+
addAliases("tif", [order.tif, order.timeInForce], (value) => typeof value === "string" ? value.toUpperCase() : undefined);
|
|
832
|
+
addAliases("outsideRTH", [order.outsideRTH, order.outside_rth], (value) => typeof value === "boolean" ? value : undefined);
|
|
833
|
+
return ticket;
|
|
834
|
+
}
|
|
835
|
+
orderHasExactAccount(order, accountId) {
|
|
836
|
+
const accounts = [order.account, order.acct];
|
|
837
|
+
const provided = accounts.filter((account) => account !== undefined);
|
|
838
|
+
return (provided.length > 0 &&
|
|
839
|
+
provided.every((account) => typeof account === "string" && account === accountId));
|
|
840
|
+
}
|
|
841
|
+
orderMatchesExactRecoveryIdentity(order, accountId, orderId) {
|
|
842
|
+
return this.orderHasExactAccount(order, accountId) && this.recoveryOrderId(order) === orderId;
|
|
843
|
+
}
|
|
844
|
+
orderHasValidRecoveryStatus(order) {
|
|
845
|
+
const provided = [order.order_status, order.orderStatus, order.status].filter((status) => status !== undefined);
|
|
846
|
+
if (provided.length === 0)
|
|
847
|
+
return true;
|
|
848
|
+
const normalized = provided.map((status) => this.canonicalIbkrOrderStatus(status));
|
|
849
|
+
const [first] = normalized;
|
|
850
|
+
return first !== undefined && normalized.every((status) => status === first);
|
|
851
|
+
}
|
|
852
|
+
assignRecoveryGraphCandidates(unresolvedNodes, terminalCandidates, usedOrderIds) {
|
|
853
|
+
const assignments = new Map();
|
|
854
|
+
const uniqueCandidates = new Map();
|
|
855
|
+
for (const node of unresolvedNodes) {
|
|
856
|
+
const candidates = terminalCandidates.get(node.memberId) ?? [];
|
|
857
|
+
if (candidates.length !== 1)
|
|
858
|
+
continue;
|
|
859
|
+
const order = candidates[0];
|
|
860
|
+
if (order === undefined)
|
|
861
|
+
continue;
|
|
862
|
+
const orderId = this.recoveryOrderId(order);
|
|
863
|
+
if (orderId === undefined || usedOrderIds.has(orderId))
|
|
864
|
+
continue;
|
|
865
|
+
const owners = uniqueCandidates.get(orderId) ?? [];
|
|
866
|
+
owners.push({ node, order });
|
|
867
|
+
uniqueCandidates.set(orderId, owners);
|
|
868
|
+
}
|
|
869
|
+
for (const owners of uniqueCandidates.values()) {
|
|
870
|
+
const owner = owners[0];
|
|
871
|
+
if (owners.length !== 1 || owner === undefined)
|
|
872
|
+
continue;
|
|
873
|
+
assignments.set(owner.node.memberId, owner.order);
|
|
874
|
+
}
|
|
875
|
+
return assignments;
|
|
876
|
+
}
|
|
339
877
|
async acknowledgeOrderWarning(input) {
|
|
340
878
|
if (!input.replyId.trim())
|
|
341
879
|
throw new Error("An exact warning reply ID is required");
|
|
@@ -372,12 +910,12 @@ export class IbkrClient {
|
|
|
372
910
|
const order = await this.req({
|
|
373
911
|
path: `iserver/account/order/status/${encodeURIComponent(orderId)}`,
|
|
374
912
|
});
|
|
375
|
-
if (
|
|
376
|
-
throw new Error(`IBKR response does not match the requested order ${orderId}`);
|
|
377
|
-
}
|
|
378
|
-
if ((order.account ?? order.acct) !== accountId) {
|
|
913
|
+
if (!this.orderHasExactAccount(order, accountId)) {
|
|
379
914
|
throw new Error(`IBKR order ${orderId} does not belong to the requested account`);
|
|
380
915
|
}
|
|
916
|
+
if (this.recoveryOrderId(order) !== orderId) {
|
|
917
|
+
throw new Error(`IBKR response does not match the requested order ${orderId}`);
|
|
918
|
+
}
|
|
381
919
|
const lifecycle = this.normalizeDerivativeOrderLifecycle(accountId, orderId, order);
|
|
382
920
|
if (lifecycle.status === "UNKNOWN") {
|
|
383
921
|
throw new Error(`IBKR order ${orderId} returned an unrecognized status`);
|
|
@@ -411,6 +949,59 @@ export class IbkrClient {
|
|
|
411
949
|
}
|
|
412
950
|
return this.getDerivativeOrderStatus(input.accountId, String(orderId));
|
|
413
951
|
}
|
|
952
|
+
async listActiveDerivativeOrders(accountId) {
|
|
953
|
+
if (!accountId.trim())
|
|
954
|
+
throw new Error("An exact account ID is required");
|
|
955
|
+
await this.prepareBrokerageAccount(accountId);
|
|
956
|
+
const response = await this.req({
|
|
957
|
+
path: "iserver/account/orders",
|
|
958
|
+
params: { force: true, accountId },
|
|
959
|
+
});
|
|
960
|
+
const flattened = this.flattenCompleteOrderSnapshot(response);
|
|
961
|
+
if (flattened === null) {
|
|
962
|
+
throw new Error("IBKR active-order snapshot is incomplete");
|
|
963
|
+
}
|
|
964
|
+
const invalidAccountEvidence = flattened.find(({ order }) => {
|
|
965
|
+
const returnedAccounts = [order.account, order.acct];
|
|
966
|
+
const providedAccounts = returnedAccounts.filter((value) => value !== undefined);
|
|
967
|
+
return (providedAccounts.length === 0 ||
|
|
968
|
+
providedAccounts.some((returnedAccount) => typeof returnedAccount !== "string" || returnedAccount !== accountId));
|
|
969
|
+
});
|
|
970
|
+
if (invalidAccountEvidence !== undefined) {
|
|
971
|
+
throw new Error("IBKR active-order response did not provide unambiguous account identity");
|
|
972
|
+
}
|
|
973
|
+
const normalized = flattened.map(({ order, nestedParent }) => this.normalizeActiveDerivativeOrder(accountId, order, nestedParent));
|
|
974
|
+
const byOrderId = new Map();
|
|
975
|
+
const byClientId = new Map();
|
|
976
|
+
for (const order of normalized) {
|
|
977
|
+
if (order.orderId !== null) {
|
|
978
|
+
const members = byOrderId.get(order.orderId) ?? [];
|
|
979
|
+
members.push(order);
|
|
980
|
+
byOrderId.set(order.orderId, members);
|
|
981
|
+
}
|
|
982
|
+
if (order.clientOrderId !== null) {
|
|
983
|
+
const members = byClientId.get(order.clientOrderId) ?? [];
|
|
984
|
+
members.push(order);
|
|
985
|
+
byClientId.set(order.clientOrderId, members);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
for (const order of normalized) {
|
|
989
|
+
if (order.orderId !== null && (byOrderId.get(order.orderId)?.length ?? 0) > 1) {
|
|
990
|
+
this.addOrderUncertainty(order, "DUPLICATE_MEMBER");
|
|
991
|
+
}
|
|
992
|
+
const parentIdentity = order.parentOrderId ?? order.parentClientOrderId;
|
|
993
|
+
if (parentIdentity === null)
|
|
994
|
+
continue;
|
|
995
|
+
const brokerMatches = byOrderId.get(parentIdentity) ?? [];
|
|
996
|
+
const clientMatches = byClientId.get(parentIdentity) ?? [];
|
|
997
|
+
const matches = new Set([...brokerMatches, ...clientMatches]);
|
|
998
|
+
if (matches.size === 0)
|
|
999
|
+
this.addOrderUncertainty(order, "MISSING_PARENT");
|
|
1000
|
+
if (matches.size > 1)
|
|
1001
|
+
this.addOrderUncertainty(order, "AMBIGUOUS_PARENT");
|
|
1002
|
+
}
|
|
1003
|
+
return normalized;
|
|
1004
|
+
}
|
|
414
1005
|
async getDerivativeExecutions(input) {
|
|
415
1006
|
if (!input.accountId.trim())
|
|
416
1007
|
throw new Error("An exact account ID is required");
|
|
@@ -702,6 +1293,174 @@ export class IbkrClient {
|
|
|
702
1293
|
}
|
|
703
1294
|
}
|
|
704
1295
|
}
|
|
1296
|
+
validateOrderGraph(request) {
|
|
1297
|
+
if (!request.accountId.trim())
|
|
1298
|
+
throw new Error("An explicit IBKR account ID is required");
|
|
1299
|
+
if (!request.rootClientOrderId.trim() || request.rootClientOrderId.length > 48) {
|
|
1300
|
+
throw new Error("Root client order ID must contain 1 to 48 characters");
|
|
1301
|
+
}
|
|
1302
|
+
if (request.nodes.length < 1 || request.nodes.length > 8) {
|
|
1303
|
+
throw new Error("Derivative order graphs require 1 to 8 members");
|
|
1304
|
+
}
|
|
1305
|
+
const seen = new Set();
|
|
1306
|
+
let roots = 0;
|
|
1307
|
+
for (const node of request.nodes) {
|
|
1308
|
+
if (!node.memberId.trim() ||
|
|
1309
|
+
node.memberId.length > 15 ||
|
|
1310
|
+
!/^[A-Za-z0-9_-]+$/.test(node.memberId)) {
|
|
1311
|
+
throw new Error("Graph member IDs must contain 1 to 15 safe characters");
|
|
1312
|
+
}
|
|
1313
|
+
if (seen.has(node.memberId))
|
|
1314
|
+
throw new Error("Graph member IDs must be unique");
|
|
1315
|
+
if (node.accountId !== request.accountId)
|
|
1316
|
+
throw new Error("Every graph member must target the graph account");
|
|
1317
|
+
if (node.parentMemberId === undefined)
|
|
1318
|
+
roots += 1;
|
|
1319
|
+
else if (!seen.has(node.parentMemberId))
|
|
1320
|
+
throw new Error("Graph parents must precede their children");
|
|
1321
|
+
else if (node.parentMemberId !== request.nodes[0]?.memberId)
|
|
1322
|
+
throw new Error("Derivative order graphs support only root-to-child attachments");
|
|
1323
|
+
if ("legs" in node) {
|
|
1324
|
+
this.validateComboPreview(node);
|
|
1325
|
+
}
|
|
1326
|
+
else {
|
|
1327
|
+
this.validateGraphSingleNode(node);
|
|
1328
|
+
}
|
|
1329
|
+
seen.add(node.memberId);
|
|
1330
|
+
}
|
|
1331
|
+
if (roots !== 1 || request.nodes[0]?.parentMemberId !== undefined) {
|
|
1332
|
+
throw new Error("Derivative order graphs require exactly one root as the first member");
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
validateGraphSingleNode(node) {
|
|
1336
|
+
const orderType = node.orderType;
|
|
1337
|
+
if (orderType !== "LMT" && orderType !== "STP" && orderType !== "MKT") {
|
|
1338
|
+
throw new Error("Graph single order type must be LMT, STP, or MKT");
|
|
1339
|
+
}
|
|
1340
|
+
if (!Number.isSafeInteger(node.quantity) || node.quantity <= 0)
|
|
1341
|
+
throw new Error("Order quantity must be a positive integer");
|
|
1342
|
+
if (!Number.isSafeInteger(node.contract.conid) || node.contract.conid <= 0)
|
|
1343
|
+
throw new Error("Order contract has an invalid IBKR conid");
|
|
1344
|
+
if (node.orderType === "LMT" && (!Number.isFinite(node.limit) || node.limit <= 0))
|
|
1345
|
+
throw new Error("LIMIT order requires a positive limit price");
|
|
1346
|
+
if (node.orderType === "STP" && (!Number.isFinite(node.stopPrice) || node.stopPrice <= 0))
|
|
1347
|
+
throw new Error("STOP order requires a positive stop price");
|
|
1348
|
+
this.cmeOperatorMetadata(node.contract.assetClass, node);
|
|
1349
|
+
}
|
|
1350
|
+
graphClientOrderId(request, node) {
|
|
1351
|
+
return node.parentMemberId === undefined
|
|
1352
|
+
? request.rootClientOrderId
|
|
1353
|
+
: `${request.rootClientOrderId}:${node.memberId}`;
|
|
1354
|
+
}
|
|
1355
|
+
graphOrderTicket(request, node) {
|
|
1356
|
+
const parent = request.nodes.find(({ memberId }) => memberId === node.parentMemberId);
|
|
1357
|
+
if (node.parentMemberId !== undefined && parent === undefined) {
|
|
1358
|
+
throw new Error("Graph parent evidence was lost after validation");
|
|
1359
|
+
}
|
|
1360
|
+
const identity = parent === undefined
|
|
1361
|
+
? { cOID: this.graphClientOrderId(request, node) }
|
|
1362
|
+
: { parentId: this.graphClientOrderId(request, parent) };
|
|
1363
|
+
if ("legs" in node)
|
|
1364
|
+
return {
|
|
1365
|
+
...this.comboOrderTicket(node),
|
|
1366
|
+
...identity,
|
|
1367
|
+
...this.cmeOperatorMetadata(node.legs[0].contract.assetClass, node),
|
|
1368
|
+
};
|
|
1369
|
+
return {
|
|
1370
|
+
acctId: node.accountId,
|
|
1371
|
+
conid: node.contract.conid,
|
|
1372
|
+
orderType: node.orderType,
|
|
1373
|
+
side: node.side,
|
|
1374
|
+
...(node.orderType === "LMT"
|
|
1375
|
+
? { price: node.limit }
|
|
1376
|
+
: node.orderType === "STP"
|
|
1377
|
+
? { price: node.stopPrice }
|
|
1378
|
+
: {}),
|
|
1379
|
+
tif: node.tif,
|
|
1380
|
+
quantity: node.quantity,
|
|
1381
|
+
outsideRTH: node.session === "OVERNIGHT",
|
|
1382
|
+
...identity,
|
|
1383
|
+
...this.cmeOperatorMetadata(node.contract.assetClass, node),
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
liveOrderMatchesGraphNode(request, node, order) {
|
|
1387
|
+
const parentIdentity = this.consistentStringAliases(order.parentId, order.parent_id, order.parentClientOrderId, order.parent_order_ref);
|
|
1388
|
+
if (!parentIdentity.valid)
|
|
1389
|
+
return false;
|
|
1390
|
+
if (node.parentMemberId === undefined) {
|
|
1391
|
+
const clientIdentity = this.consistentStringAliases(order.cOID, order.order_ref);
|
|
1392
|
+
if (!clientIdentity.valid || clientIdentity.value !== request.rootClientOrderId)
|
|
1393
|
+
return false;
|
|
1394
|
+
if (parentIdentity.value !== undefined)
|
|
1395
|
+
return false;
|
|
1396
|
+
}
|
|
1397
|
+
else if (parentIdentity.value !== request.rootClientOrderId)
|
|
1398
|
+
return false;
|
|
1399
|
+
if ("legs" in node) {
|
|
1400
|
+
const liveLegs = this.parseComboLegs(order.conidex);
|
|
1401
|
+
const orderType = this.normalizeOrderType(order.order_type ?? order.orderType);
|
|
1402
|
+
const side = this.normalizeOrderSide(order.side);
|
|
1403
|
+
const quantity = this.firstPositiveNumber(order.total_size, order.totalSize, order.size);
|
|
1404
|
+
const price = this.firstNumber(order.limitPrice, order.limit_price, order.price);
|
|
1405
|
+
const expectedPrice = node.priceEffect === "CREDIT" ? -node.limit : node.limit;
|
|
1406
|
+
const outsideRth = order.outsideRTH ?? order.outside_rth;
|
|
1407
|
+
const tif = order.tif ?? order.timeInForce;
|
|
1408
|
+
return (liveLegs.length === node.legs.length &&
|
|
1409
|
+
node.legs.every((leg, index) => {
|
|
1410
|
+
const liveLeg = liveLegs[index];
|
|
1411
|
+
return liveLeg?.conid === leg.contract.conid && liveLeg.ratio === leg.ratio;
|
|
1412
|
+
}) &&
|
|
1413
|
+
orderType === this.normalizeOrderType("LMT") &&
|
|
1414
|
+
side === "BUY" &&
|
|
1415
|
+
quantity === node.quantity &&
|
|
1416
|
+
price === expectedPrice &&
|
|
1417
|
+
(tif === undefined || tif.toUpperCase() === node.tif) &&
|
|
1418
|
+
(outsideRth === undefined || outsideRth === (node.session === "OVERNIGHT")));
|
|
1419
|
+
}
|
|
1420
|
+
const orderType = this.normalizeOrderType(order.order_type ?? order.orderType);
|
|
1421
|
+
const expectedOrderType = this.normalizeOrderType(node.orderType);
|
|
1422
|
+
const side = this.normalizeOrderSide(order.side);
|
|
1423
|
+
const quantity = this.firstPositiveNumber(order.total_size, order.totalSize, order.size);
|
|
1424
|
+
const price = node.orderType === "LMT"
|
|
1425
|
+
? this.firstNumber(order.limitPrice, order.limit_price, order.price)
|
|
1426
|
+
: node.orderType === "STP"
|
|
1427
|
+
? this.firstNumber(order.stopPrice, order.price)
|
|
1428
|
+
: undefined;
|
|
1429
|
+
const expectedPrice = node.orderType === "LMT" ? node.limit : node.orderType === "STP" ? node.stopPrice : undefined;
|
|
1430
|
+
const outsideRth = order.outsideRTH ?? order.outside_rth;
|
|
1431
|
+
const tif = order.tif ?? order.timeInForce;
|
|
1432
|
+
return (order.conid === node.contract.conid &&
|
|
1433
|
+
orderType === expectedOrderType &&
|
|
1434
|
+
side === node.side &&
|
|
1435
|
+
quantity === node.quantity &&
|
|
1436
|
+
price === expectedPrice &&
|
|
1437
|
+
(tif === undefined || tif.toUpperCase() === node.tif) &&
|
|
1438
|
+
(outsideRth === undefined || outsideRth === (node.session === "OVERNIGHT")));
|
|
1439
|
+
}
|
|
1440
|
+
recoveryOrderMatchesGraphNode(request, node, order) {
|
|
1441
|
+
try {
|
|
1442
|
+
return this.liveOrderMatchesGraphNode(request, node, order);
|
|
1443
|
+
}
|
|
1444
|
+
catch {
|
|
1445
|
+
return false;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
terminalOrderMatchesGraphNode(request, node, order) {
|
|
1449
|
+
try {
|
|
1450
|
+
const tif = order.tif ?? order.timeInForce;
|
|
1451
|
+
const outsideRth = order.outsideRTH ?? order.outside_rth;
|
|
1452
|
+
const status = order.order_status ?? order.orderStatus ?? order.status;
|
|
1453
|
+
if (typeof tif !== "string" ||
|
|
1454
|
+
typeof outsideRth !== "boolean" ||
|
|
1455
|
+
typeof status !== "string") {
|
|
1456
|
+
return false;
|
|
1457
|
+
}
|
|
1458
|
+
return this.recoveryOrderMatchesGraphNode(request, node, order);
|
|
1459
|
+
}
|
|
1460
|
+
catch {
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
705
1464
|
validateSingleOrder(request) {
|
|
706
1465
|
this.validateSingleOrderFields(request);
|
|
707
1466
|
const identityFields = request;
|
|
@@ -833,6 +1592,122 @@ export class IbkrClient {
|
|
|
833
1592
|
}
|
|
834
1593
|
return this.singleOrderRecoveryResult(decoded, orders);
|
|
835
1594
|
}
|
|
1595
|
+
graphMemberEvidence(request, node, order) {
|
|
1596
|
+
const index = request.nodes.findIndex(({ memberId }) => memberId === node.memberId);
|
|
1597
|
+
let depth = 0;
|
|
1598
|
+
let parentId = node.parentMemberId;
|
|
1599
|
+
while (parentId !== undefined) {
|
|
1600
|
+
depth += 1;
|
|
1601
|
+
parentId = request.nodes.find(({ memberId }) => memberId === parentId)?.parentMemberId;
|
|
1602
|
+
}
|
|
1603
|
+
const rawId = order?.order_id ?? order?.orderId;
|
|
1604
|
+
const orderId = typeof rawId === "string" || typeof rawId === "number" ? String(rawId).trim() || null : null;
|
|
1605
|
+
const filledQuantity = this.firstNumber(order?.cum_fill, order?.cumFill, order?.filledQuantity, order?.filled);
|
|
1606
|
+
const quantity = this.firstPositiveNumber(order?.total_size, order?.totalSize, order?.size);
|
|
1607
|
+
const remainingQuantity = this.firstNumber(order?.remainingQuantity, order?.remaining_size, order?.remaining) ??
|
|
1608
|
+
(quantity !== undefined && filledQuantity !== undefined
|
|
1609
|
+
? Math.max(0, quantity - filledQuantity)
|
|
1610
|
+
: undefined);
|
|
1611
|
+
return {
|
|
1612
|
+
memberId: node.memberId,
|
|
1613
|
+
role: index < 0
|
|
1614
|
+
? "unknown"
|
|
1615
|
+
: depth === 0
|
|
1616
|
+
? "root"
|
|
1617
|
+
: depth === 1
|
|
1618
|
+
? "child"
|
|
1619
|
+
: depth === 2
|
|
1620
|
+
? "grandchild"
|
|
1621
|
+
: "descendant",
|
|
1622
|
+
parentMemberId: node.parentMemberId ?? null,
|
|
1623
|
+
parentOrderId: null,
|
|
1624
|
+
orderId,
|
|
1625
|
+
status: order === undefined
|
|
1626
|
+
? "WARNING_PENDING"
|
|
1627
|
+
: this.normalizeDerivativeOrderStatus(order.order_status ?? order.orderStatus ?? order.status, filledQuantity ?? 0, remainingQuantity ?? 0),
|
|
1628
|
+
clientOrderId: this.graphClientOrderId(request, node),
|
|
1629
|
+
request: node,
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
attachGraphParentOrderIds(members) {
|
|
1633
|
+
const ids = new Map(members.map(({ memberId, orderId }) => [memberId, orderId]));
|
|
1634
|
+
return members.map((member) => ({
|
|
1635
|
+
...member,
|
|
1636
|
+
parentOrderId: member.parentMemberId === null ? null : (ids.get(member.parentMemberId) ?? null),
|
|
1637
|
+
}));
|
|
1638
|
+
}
|
|
1639
|
+
normalizeOrderGraphSubmission(response, request, previousMembers) {
|
|
1640
|
+
const decoded = this.decodeOrderSubmission(response);
|
|
1641
|
+
const cleanOrders = decoded.warnings.length === 0 &&
|
|
1642
|
+
decoded.errors.length === 0 &&
|
|
1643
|
+
decoded.unrecognizedResponses.length === 0;
|
|
1644
|
+
const distinct = new Set(decoded.orders.map(({ orderId }) => orderId)).size === decoded.orders.length;
|
|
1645
|
+
const canCorrelatePositionally = cleanOrders && distinct && decoded.orders.length === request.nodes.length;
|
|
1646
|
+
const members = this.attachGraphParentOrderIds(request.nodes.map((node, index) => {
|
|
1647
|
+
const order = canCorrelatePositionally ? decoded.orders[index] : undefined;
|
|
1648
|
+
if (order === undefined)
|
|
1649
|
+
return (previousMembers.find(({ memberId }) => memberId === node.memberId) ??
|
|
1650
|
+
this.graphMemberEvidence(request, node));
|
|
1651
|
+
const evidence = this.graphMemberEvidence(request, node, {
|
|
1652
|
+
order_id: order.orderId,
|
|
1653
|
+
order_status: order.status,
|
|
1654
|
+
});
|
|
1655
|
+
return { ...evidence, status: order.status };
|
|
1656
|
+
}));
|
|
1657
|
+
if (decoded.warnings.length === 1 &&
|
|
1658
|
+
decoded.orders.length === 0 &&
|
|
1659
|
+
decoded.errors.length === 0 &&
|
|
1660
|
+
decoded.unrecognizedResponses.length === 0) {
|
|
1661
|
+
const warning = decoded.warnings[0];
|
|
1662
|
+
if (warning === undefined)
|
|
1663
|
+
throw new Error("Graph warning evidence was lost");
|
|
1664
|
+
return {
|
|
1665
|
+
state: "warning",
|
|
1666
|
+
rootClientOrderId: request.rootClientOrderId,
|
|
1667
|
+
members,
|
|
1668
|
+
warnings: decoded.warnings,
|
|
1669
|
+
continuation: { replyId: warning.replyId, request, members },
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
if (decoded.errors.length > 0 &&
|
|
1673
|
+
!decoded.responseIsArray &&
|
|
1674
|
+
decoded.orders.length === 0 &&
|
|
1675
|
+
decoded.warnings.length === 0 &&
|
|
1676
|
+
decoded.unrecognizedResponses.length === 0) {
|
|
1677
|
+
return {
|
|
1678
|
+
state: "rejected",
|
|
1679
|
+
rootClientOrderId: request.rootClientOrderId,
|
|
1680
|
+
members,
|
|
1681
|
+
reasons: decoded.errors.map(({ message }) => message),
|
|
1682
|
+
errors: decoded.errors,
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
if (cleanOrders &&
|
|
1686
|
+
distinct &&
|
|
1687
|
+
decoded.orders.length === request.nodes.length &&
|
|
1688
|
+
decoded.pendingCancelOrderIds.length === 0 &&
|
|
1689
|
+
decoded.orders.every(({ status }) => status !== "UNKNOWN" && status !== "REJECTED" && status !== "CANCELED")) {
|
|
1690
|
+
return {
|
|
1691
|
+
state: "accepted",
|
|
1692
|
+
rootClientOrderId: request.rootClientOrderId,
|
|
1693
|
+
members,
|
|
1694
|
+
warnings: [],
|
|
1695
|
+
};
|
|
1696
|
+
}
|
|
1697
|
+
return {
|
|
1698
|
+
state: "recovery_required",
|
|
1699
|
+
rootClientOrderId: request.rootClientOrderId,
|
|
1700
|
+
members,
|
|
1701
|
+
reasons: [
|
|
1702
|
+
this.submissionRecoveryReason(decoded, decoded.orders.length, request.nodes.length),
|
|
1703
|
+
],
|
|
1704
|
+
warnings: decoded.warnings,
|
|
1705
|
+
errors: decoded.errors,
|
|
1706
|
+
unrecognizedResponses: canCorrelatePositionally
|
|
1707
|
+
? decoded.unrecognizedResponses
|
|
1708
|
+
: [...decoded.unrecognizedResponses, ...decoded.orders],
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
836
1711
|
normalizeMultiOrderSubmission(response, parentClientOrderId) {
|
|
837
1712
|
const decoded = this.decodeOrderSubmission(response);
|
|
838
1713
|
const hasDistinctBrokerOrderIds = decoded.orders.length === 2 &&
|
|
@@ -1083,6 +1958,198 @@ export class IbkrClient {
|
|
|
1083
1958
|
updatedAt: this.parseOrderTime(order)?.toISOString() ?? null,
|
|
1084
1959
|
};
|
|
1085
1960
|
}
|
|
1961
|
+
flattenCompleteOrderSnapshot(response) {
|
|
1962
|
+
if (typeof response !== "object" || response === null || Array.isArray(response))
|
|
1963
|
+
return null;
|
|
1964
|
+
const record = response;
|
|
1965
|
+
if (record["snapshot"] !== true || !Array.isArray(record["orders"]))
|
|
1966
|
+
return null;
|
|
1967
|
+
const flattened = [];
|
|
1968
|
+
const visiting = new Set();
|
|
1969
|
+
const visit = (orders, nestedParent) => {
|
|
1970
|
+
for (const rawOrder of orders) {
|
|
1971
|
+
if (typeof rawOrder !== "object" || rawOrder === null || Array.isArray(rawOrder))
|
|
1972
|
+
return false;
|
|
1973
|
+
if (visiting.has(rawOrder))
|
|
1974
|
+
return false;
|
|
1975
|
+
visiting.add(rawOrder);
|
|
1976
|
+
const orderRecord = rawOrder;
|
|
1977
|
+
const childCollections = [orderRecord["childOrders"], orderRecord["children"]];
|
|
1978
|
+
if (childCollections.some((children) => children !== undefined && !Array.isArray(children))) {
|
|
1979
|
+
return false;
|
|
1980
|
+
}
|
|
1981
|
+
const order = rawOrder;
|
|
1982
|
+
flattened.push({ order, nestedParent });
|
|
1983
|
+
const children = new Set();
|
|
1984
|
+
for (const collection of childCollections) {
|
|
1985
|
+
if (Array.isArray(collection)) {
|
|
1986
|
+
for (const child of collection)
|
|
1987
|
+
children.add(child);
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
if (!visit([...children], order))
|
|
1991
|
+
return false;
|
|
1992
|
+
visiting.delete(rawOrder);
|
|
1993
|
+
}
|
|
1994
|
+
return true;
|
|
1995
|
+
};
|
|
1996
|
+
return visit(record["orders"], null) ? flattened : null;
|
|
1997
|
+
}
|
|
1998
|
+
normalizeActiveDerivativeOrder(accountId, order, nestedParent) {
|
|
1999
|
+
const uncertainty = [];
|
|
2000
|
+
const total = this.firstNumber(order.total_size, order.totalSize, order.size) ?? null;
|
|
2001
|
+
const filled = this.firstNumber(order.cum_fill, order.cumFill, order.filledQuantity, order.filled) ?? null;
|
|
2002
|
+
const remaining = this.firstNumber(order.remainingQuantity, order.remaining_size, order.remaining) ??
|
|
2003
|
+
(total !== null && filled !== null ? Math.max(0, total - filled) : null);
|
|
2004
|
+
if (total === null || filled === null || remaining === null)
|
|
2005
|
+
uncertainty.push("INCOMPLETE_QUANTITIES");
|
|
2006
|
+
const rawStatus = order.order_status ?? order.orderStatus ?? order.status;
|
|
2007
|
+
const status = this.normalizeDerivativeOrderStatus(rawStatus, filled ?? 0, remaining ?? 0);
|
|
2008
|
+
if (status === "UNKNOWN")
|
|
2009
|
+
uncertainty.push("UNKNOWN_STATUS");
|
|
2010
|
+
const rawOrderId = order.order_id ?? order.orderId;
|
|
2011
|
+
if (rawOrderId === undefined)
|
|
2012
|
+
uncertainty.push("MISSING_BROKER_ORDER_ID");
|
|
2013
|
+
const explicitParentOrderId = order.parent_order_id ?? order.parentOrderId ?? order.parent_id;
|
|
2014
|
+
const explicitParentClientId = order.parentClientOrderId ?? order.parent_order_ref ?? order.parentId;
|
|
2015
|
+
const nestedBrokerId = nestedParent?.order_id ?? nestedParent?.orderId;
|
|
2016
|
+
const nestedClientId = nestedParent?.cOID ?? nestedParent?.order_ref;
|
|
2017
|
+
if (nestedParent !== null &&
|
|
2018
|
+
explicitParentOrderId === undefined &&
|
|
2019
|
+
explicitParentClientId === undefined) {
|
|
2020
|
+
uncertainty.push("PARTIAL_GRAPH");
|
|
2021
|
+
}
|
|
2022
|
+
const legs = this.normalizeActiveDerivativeLegs(order, total, uncertainty);
|
|
2023
|
+
const orderTime = this.parseOrderTime(order)?.toISOString() ?? null;
|
|
2024
|
+
return {
|
|
2025
|
+
accountId,
|
|
2026
|
+
orderId: rawOrderId === undefined ? null : String(rawOrderId),
|
|
2027
|
+
clientOrderId: order.cOID ?? order.order_ref ?? null,
|
|
2028
|
+
parentOrderId: explicitParentOrderId === undefined
|
|
2029
|
+
? nestedBrokerId === undefined
|
|
2030
|
+
? null
|
|
2031
|
+
: String(nestedBrokerId)
|
|
2032
|
+
: String(explicitParentOrderId),
|
|
2033
|
+
parentClientOrderId: explicitParentClientId === undefined
|
|
2034
|
+
? (nestedClientId ?? null)
|
|
2035
|
+
: String(explicitParentClientId),
|
|
2036
|
+
graphRole: nestedParent !== null ||
|
|
2037
|
+
explicitParentOrderId !== undefined ||
|
|
2038
|
+
explicitParentClientId !== undefined
|
|
2039
|
+
? "CHILD"
|
|
2040
|
+
: rawOrderId === undefined
|
|
2041
|
+
? "UNKNOWN"
|
|
2042
|
+
: "ROOT",
|
|
2043
|
+
status,
|
|
2044
|
+
totalQuantity: total,
|
|
2045
|
+
filledQuantity: filled,
|
|
2046
|
+
remainingQuantity: remaining,
|
|
2047
|
+
tif: order.tif ?? order.timeInForce ?? null,
|
|
2048
|
+
session: order.outsideRTH === true || order.outside_rth === true
|
|
2049
|
+
? "OVERNIGHT"
|
|
2050
|
+
: order.outsideRTH === false || order.outside_rth === false
|
|
2051
|
+
? "REGULAR"
|
|
2052
|
+
: "UNKNOWN",
|
|
2053
|
+
orderType: this.normalizeOrderType(order.order_type ?? order.orderType) ?? null,
|
|
2054
|
+
limitPrice: this.firstNumber(order.limitPrice, order.limit_price, order.price) ?? null,
|
|
2055
|
+
stopPrice: this.firstNumber(order.stopPrice) ?? null,
|
|
2056
|
+
enteredAt: orderTime,
|
|
2057
|
+
updatedAt: order.lastExecutionTime_r !== undefined || order.lastExecutionTime !== undefined
|
|
2058
|
+
? orderTime
|
|
2059
|
+
: null,
|
|
2060
|
+
legs,
|
|
2061
|
+
uncertainty,
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
normalizeActiveDerivativeLegs(order, total, orderUncertainty) {
|
|
2065
|
+
const description = [
|
|
2066
|
+
order.orderDescriptionWithContract,
|
|
2067
|
+
order.order_description_with_contract,
|
|
2068
|
+
order.contractDescription1,
|
|
2069
|
+
order.contract_description_1,
|
|
2070
|
+
order.description1,
|
|
2071
|
+
order.symbol,
|
|
2072
|
+
]
|
|
2073
|
+
.filter((value) => typeof value === "string")
|
|
2074
|
+
.join(" ");
|
|
2075
|
+
const describedOptions = [...description.matchAll(/([A-Z ]{1,6}\d{6}[CP]\d{8})/gi)].flatMap((match) => {
|
|
2076
|
+
const symbol = match[1]?.toUpperCase() ?? "";
|
|
2077
|
+
const parsed = parseOsiOptionSymbol(symbol);
|
|
2078
|
+
return parsed === null ? [] : [{ symbol, ...parsed }];
|
|
2079
|
+
});
|
|
2080
|
+
const uniqueDescribedOptions = describedOptions.filter((option, index) => describedOptions.findIndex((candidate) => candidate.symbol === option.symbol) === index);
|
|
2081
|
+
const side = this.normalizeOrderSide(order.side);
|
|
2082
|
+
const signedSide = side === "BUY" ? 1 : side === "SELL" ? -1 : null;
|
|
2083
|
+
let rawLegs = [];
|
|
2084
|
+
const conidex = typeof order.conidex === "string" ? order.conidex.trim() : null;
|
|
2085
|
+
if (conidex?.includes(";;;")) {
|
|
2086
|
+
const match = /^(\d+)(?:@[A-Za-z0-9._-]+)?;;;(.+)$/.exec(conidex);
|
|
2087
|
+
if (match?.[1] !== "28812380") {
|
|
2088
|
+
orderUncertainty.push("MALFORMED_CONIDEX");
|
|
2089
|
+
}
|
|
2090
|
+
else {
|
|
2091
|
+
rawLegs = (match[2] ?? "").split(",").map((member) => {
|
|
2092
|
+
const legMatch = /^(\d+)\/([+-]?\d+)$/.exec(member.trim());
|
|
2093
|
+
const conid = Number(legMatch?.[1]);
|
|
2094
|
+
const ratio = Number(legMatch?.[2]);
|
|
2095
|
+
if (!legMatch ||
|
|
2096
|
+
!Number.isSafeInteger(conid) ||
|
|
2097
|
+
conid <= 0 ||
|
|
2098
|
+
!Number.isSafeInteger(ratio) ||
|
|
2099
|
+
ratio === 0) {
|
|
2100
|
+
return { conid: null, ratio: null, quantityRatio: null };
|
|
2101
|
+
}
|
|
2102
|
+
return {
|
|
2103
|
+
conid,
|
|
2104
|
+
ratio: signedSide === null ? null : signedSide * ratio,
|
|
2105
|
+
quantityRatio: Math.abs(ratio),
|
|
2106
|
+
};
|
|
2107
|
+
});
|
|
2108
|
+
if (rawLegs.length === 0 || rawLegs.some((leg) => leg.conid === null)) {
|
|
2109
|
+
orderUncertainty.push("MALFORMED_CONIDEX");
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
if (rawLegs.length === 0)
|
|
2113
|
+
orderUncertainty.push("AGGREGATE_ONLY");
|
|
2114
|
+
}
|
|
2115
|
+
else if (Number.isSafeInteger(order.conid) && Number(order.conid) > 0) {
|
|
2116
|
+
rawLegs = [{ conid: Number(order.conid), ratio: signedSide, quantityRatio: 1 }];
|
|
2117
|
+
}
|
|
2118
|
+
else {
|
|
2119
|
+
if (conidex)
|
|
2120
|
+
orderUncertainty.push("MALFORMED_CONIDEX");
|
|
2121
|
+
orderUncertainty.push("MISSING_LEG_IDENTITY");
|
|
2122
|
+
}
|
|
2123
|
+
if (rawLegs.length === 0) {
|
|
2124
|
+
rawLegs = [{ conid: null, ratio: null, quantityRatio: null }];
|
|
2125
|
+
}
|
|
2126
|
+
return rawLegs.map((leg) => {
|
|
2127
|
+
const legUncertainty = [];
|
|
2128
|
+
if (leg.conid === null)
|
|
2129
|
+
legUncertainty.push("MISSING_LEG_IDENTITY");
|
|
2130
|
+
if (leg.ratio === null) {
|
|
2131
|
+
const directionUncertainty = leg.conid !== null && signedSide === null ? "UNKNOWN_SIDE" : "MALFORMED_CONIDEX";
|
|
2132
|
+
legUncertainty.push(directionUncertainty);
|
|
2133
|
+
if (!orderUncertainty.includes(directionUncertainty)) {
|
|
2134
|
+
orderUncertainty.push(directionUncertainty);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
return {
|
|
2138
|
+
conid: leg.conid,
|
|
2139
|
+
ratio: leg.ratio,
|
|
2140
|
+
side: leg.ratio === null ? "UNKNOWN" : leg.ratio > 0 ? "BUY" : "SELL",
|
|
2141
|
+
quantity: total === null || leg.quantityRatio === null ? null : total * leg.quantityRatio,
|
|
2142
|
+
option: rawLegs.length === 1 && uniqueDescribedOptions.length === 1
|
|
2143
|
+
? (uniqueDescribedOptions[0] ?? null)
|
|
2144
|
+
: null,
|
|
2145
|
+
uncertainty: legUncertainty,
|
|
2146
|
+
};
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
addOrderUncertainty(order, uncertainty) {
|
|
2150
|
+
if (!order.uncertainty.includes(uncertainty))
|
|
2151
|
+
order.uncertainty.push(uncertainty);
|
|
2152
|
+
}
|
|
1086
2153
|
normalizeDerivativeExecution(accountId, trade) {
|
|
1087
2154
|
if (!trade.execution_id || !Number.isSafeInteger(trade.conid) || Number(trade.conid) <= 0) {
|
|
1088
2155
|
return undefined;
|
|
@@ -1130,8 +2197,39 @@ export class IbkrClient {
|
|
|
1130
2197
|
const parsed = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`);
|
|
1131
2198
|
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
1132
2199
|
}
|
|
2200
|
+
recoveryOrderId(order) {
|
|
2201
|
+
const identity = this.consistentScalarAliases(order.order_id, order.orderId);
|
|
2202
|
+
return identity.valid ? identity.value : undefined;
|
|
2203
|
+
}
|
|
2204
|
+
consistentScalarAliases(...aliases) {
|
|
2205
|
+
const provided = aliases.filter((alias) => alias !== undefined);
|
|
2206
|
+
if (provided.length === 0)
|
|
2207
|
+
return { valid: true, value: undefined };
|
|
2208
|
+
const normalized = provided.map((alias) => {
|
|
2209
|
+
if (typeof alias !== "string" && typeof alias !== "number")
|
|
2210
|
+
return undefined;
|
|
2211
|
+
const value = String(alias).trim();
|
|
2212
|
+
return value.length === 0 ? undefined : value;
|
|
2213
|
+
});
|
|
2214
|
+
const [first] = normalized;
|
|
2215
|
+
if (first === undefined || normalized.some((value) => value !== first)) {
|
|
2216
|
+
return { valid: false, value: undefined };
|
|
2217
|
+
}
|
|
2218
|
+
return { valid: true, value: first };
|
|
2219
|
+
}
|
|
2220
|
+
consistentStringAliases(...aliases) {
|
|
2221
|
+
const provided = aliases.filter((alias) => alias !== undefined);
|
|
2222
|
+
if (provided.length === 0)
|
|
2223
|
+
return { valid: true, value: undefined };
|
|
2224
|
+
const normalized = provided.map((alias) => typeof alias === "string" && alias.trim() !== "" ? alias.trim() : undefined);
|
|
2225
|
+
const [first] = normalized;
|
|
2226
|
+
if (first === undefined || normalized.some((value) => value !== first)) {
|
|
2227
|
+
return { valid: false, value: undefined };
|
|
2228
|
+
}
|
|
2229
|
+
return { valid: true, value: first };
|
|
2230
|
+
}
|
|
1133
2231
|
trimmedString(value) {
|
|
1134
|
-
if (value
|
|
2232
|
+
if (typeof value !== "string")
|
|
1135
2233
|
return null;
|
|
1136
2234
|
const trimmed = value.trim();
|
|
1137
2235
|
return trimmed.length === 0 ? null : trimmed;
|
|
@@ -1296,14 +2394,14 @@ export class IbkrClient {
|
|
|
1296
2394
|
}
|
|
1297
2395
|
normalizeDerivativeOrderStatus(value, filledQuantity, remainingQuantity) {
|
|
1298
2396
|
const status = this.canonicalIbkrOrderStatus(value);
|
|
1299
|
-
if (filledQuantity > 0 && remainingQuantity > 0)
|
|
1300
|
-
return "PARTIALLY_FILLED";
|
|
1301
2397
|
if (status === "FILLED")
|
|
1302
2398
|
return "FILLED";
|
|
1303
2399
|
if (status === "CANCELLED" || status === "CANCELED")
|
|
1304
2400
|
return "CANCELED";
|
|
1305
2401
|
if (status === "INACTIVE" || status === "REJECTED")
|
|
1306
2402
|
return "REJECTED";
|
|
2403
|
+
if (filledQuantity > 0 && remainingQuantity > 0)
|
|
2404
|
+
return "PARTIALLY_FILLED";
|
|
1307
2405
|
if (status === "API_PENDING" || status === "PENDING_SUBMIT")
|
|
1308
2406
|
return "PENDING";
|
|
1309
2407
|
if (status !== undefined && IBKR_WORKING_STATUSES.has(status))
|
|
@@ -1311,8 +2409,10 @@ export class IbkrClient {
|
|
|
1311
2409
|
return "UNKNOWN";
|
|
1312
2410
|
}
|
|
1313
2411
|
canonicalIbkrOrderStatus(value) {
|
|
2412
|
+
if (typeof value !== "string")
|
|
2413
|
+
return undefined;
|
|
1314
2414
|
return value
|
|
1315
|
-
|
|
2415
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
1316
2416
|
.replace(/\s+/g, "_")
|
|
1317
2417
|
.toUpperCase();
|
|
1318
2418
|
}
|
|
@@ -1365,11 +2465,14 @@ export class IbkrClient {
|
|
|
1365
2465
|
if (brokerageAccounts.accounts && !brokerageAccounts.accounts.includes(accountId)) {
|
|
1366
2466
|
throw new Error(`IBKR account ${accountId} is not available for trading/order queries.`);
|
|
1367
2467
|
}
|
|
1368
|
-
await this.req({
|
|
2468
|
+
const switchedAccount = await this.req({
|
|
1369
2469
|
path: "iserver/account",
|
|
1370
2470
|
method: "POST",
|
|
1371
2471
|
data: { acctId: accountId },
|
|
1372
2472
|
});
|
|
2473
|
+
if (switchedAccount.set !== true || switchedAccount.acctId !== accountId) {
|
|
2474
|
+
throw new Error(`IBKR account switch was not confirmed for ${accountId}.`);
|
|
2475
|
+
}
|
|
1373
2476
|
}
|
|
1374
2477
|
normalizeStockListing(symbol, listing) {
|
|
1375
2478
|
const assetType = listing.assetClass === "STK" ? "EQUITY" : listing.assetClass;
|