@huskly/ibkr-client 0.14.0 → 0.16.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 +23 -10
- package/dist/helpers.d.ts +2 -0
- package/dist/helpers.d.ts.map +1 -1
- package/dist/helpers.js +9 -0
- package/dist/helpers.js.map +1 -1
- package/dist/ibkr/ibkrClient.d.ts +19 -1
- package/dist/ibkr/ibkrClient.d.ts.map +1 -1
- package/dist/ibkr/ibkrClient.js +611 -48
- 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 +28 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/ibkr/ibkrClient.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { ASSET_CLASS_LABELS, toNumber } from "../helpers.js";
|
|
2
|
+
import { ASSET_CLASS_LABELS, toNullableNumber, toNumber } from "../helpers.js";
|
|
3
3
|
import { normalizeOptionContract, parseOsiOptionSymbol } from "./optionContract.js";
|
|
4
4
|
import { normalizeDerivativeContract, normalizeDerivativeDataAvailability, } from "./derivativeContract.js";
|
|
5
5
|
import { IbkrRequestScheduler, } from "./requestScheduler.js";
|
|
@@ -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;
|
|
@@ -372,47 +384,121 @@ export class IbkrClient {
|
|
|
372
384
|
this.validateOrderGraph(request);
|
|
373
385
|
if (input.accountId !== request.accountId)
|
|
374
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
|
+
}
|
|
375
394
|
await this.prepareBrokerageAccount(input.accountId);
|
|
376
395
|
const response = await this.req({
|
|
377
396
|
path: "iserver/account/orders",
|
|
378
397
|
params: { force: true, accountId: input.accountId },
|
|
379
398
|
});
|
|
380
|
-
const
|
|
381
|
-
const
|
|
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)));
|
|
382
413
|
for (const node of request.nodes) {
|
|
383
|
-
const matches = accountOrders.filter((order) => this.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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);
|
|
387
418
|
}
|
|
388
|
-
const
|
|
389
|
-
const
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
+
}
|
|
395
449
|
}
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
450
|
+
observedCandidates.push(...terminalEvidence.observedResponses);
|
|
451
|
+
if (terminalEvidence.invalidAttachedEvidence) {
|
|
452
|
+
observedCandidates.push({ reason: "Invalid or conflicting terminal broker evidence" });
|
|
399
453
|
}
|
|
454
|
+
const selectedCandidates = [...selected.values()];
|
|
455
|
+
const requestedOrderIdMissing = input.orderId !== undefined &&
|
|
456
|
+
!selectedCandidates.some((order) => this.recoveryOrderId(order) === input.orderId);
|
|
400
457
|
const members = request.nodes.map((node) => {
|
|
401
|
-
const order =
|
|
458
|
+
const order = selected.get(node.memberId);
|
|
402
459
|
return this.graphMemberEvidence(request, node, order);
|
|
403
460
|
});
|
|
404
461
|
const ids = members.flatMap(({ orderId }) => (orderId === null ? [] : [orderId]));
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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")) {
|
|
416
502
|
return {
|
|
417
503
|
state: "recovery_required",
|
|
418
504
|
rootClientOrderId: request.rootClientOrderId,
|
|
@@ -422,7 +508,7 @@ export class IbkrClient {
|
|
|
422
508
|
],
|
|
423
509
|
warnings: [],
|
|
424
510
|
errors: [],
|
|
425
|
-
unrecognizedResponses:
|
|
511
|
+
unrecognizedResponses: observedCandidates,
|
|
426
512
|
};
|
|
427
513
|
}
|
|
428
514
|
return {
|
|
@@ -432,6 +518,362 @@ export class IbkrClient {
|
|
|
432
518
|
warnings: [],
|
|
433
519
|
};
|
|
434
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
|
+
}
|
|
435
877
|
async acknowledgeOrderWarning(input) {
|
|
436
878
|
if (!input.replyId.trim())
|
|
437
879
|
throw new Error("An exact warning reply ID is required");
|
|
@@ -468,12 +910,12 @@ export class IbkrClient {
|
|
|
468
910
|
const order = await this.req({
|
|
469
911
|
path: `iserver/account/order/status/${encodeURIComponent(orderId)}`,
|
|
470
912
|
});
|
|
471
|
-
if (
|
|
472
|
-
throw new Error(`IBKR response does not match the requested order ${orderId}`);
|
|
473
|
-
}
|
|
474
|
-
if ((order.account ?? order.acct) !== accountId) {
|
|
913
|
+
if (!this.orderHasExactAccount(order, accountId)) {
|
|
475
914
|
throw new Error(`IBKR order ${orderId} does not belong to the requested account`);
|
|
476
915
|
}
|
|
916
|
+
if (this.recoveryOrderId(order) !== orderId) {
|
|
917
|
+
throw new Error(`IBKR response does not match the requested order ${orderId}`);
|
|
918
|
+
}
|
|
477
919
|
const lifecycle = this.normalizeDerivativeOrderLifecycle(accountId, orderId, order);
|
|
478
920
|
if (lifecycle.status === "UNKNOWN") {
|
|
479
921
|
throw new Error(`IBKR order ${orderId} returned an unrecognized status`);
|
|
@@ -515,10 +957,10 @@ export class IbkrClient {
|
|
|
515
957
|
path: "iserver/account/orders",
|
|
516
958
|
params: { force: true, accountId },
|
|
517
959
|
});
|
|
518
|
-
|
|
960
|
+
const flattened = this.flattenCompleteOrderSnapshot(response);
|
|
961
|
+
if (flattened === null) {
|
|
519
962
|
throw new Error("IBKR active-order snapshot is incomplete");
|
|
520
963
|
}
|
|
521
|
-
const flattened = this.flattenActiveOrders(response.orders);
|
|
522
964
|
const invalidAccountEvidence = flattened.find(({ order }) => {
|
|
523
965
|
const returnedAccounts = [order.account, order.acct];
|
|
524
966
|
const providedAccounts = returnedAccounts.filter((value) => value !== undefined);
|
|
@@ -650,11 +1092,38 @@ export class IbkrClient {
|
|
|
650
1092
|
path: `portfolio/${accountId}/summary`,
|
|
651
1093
|
});
|
|
652
1094
|
const amount = (key) => toNumber(summary[key]?.amount);
|
|
1095
|
+
const marginSnapshot = (suffix) => ({
|
|
1096
|
+
equityWithLoanValue: toNullableNumber(summary[`equitywithloanvalue${suffix}`]?.amount),
|
|
1097
|
+
regTEquity: toNullableNumber(summary[`regtequity${suffix}`]?.amount),
|
|
1098
|
+
regTMargin: toNullableNumber(summary[`regtmargin${suffix}`]?.amount),
|
|
1099
|
+
initialMarginRequirement: toNullableNumber(summary[`initmarginreq${suffix}`]?.amount),
|
|
1100
|
+
maintenanceMarginRequirement: toNullableNumber(summary[`maintmarginreq${suffix}`]?.amount),
|
|
1101
|
+
availableFunds: toNullableNumber(summary[`availablefunds${suffix}`]?.amount),
|
|
1102
|
+
excessLiquidity: toNullableNumber(summary[`excessliquidity${suffix}`]?.amount),
|
|
1103
|
+
cushion: toNullableNumber(summary[`cushion${suffix}`]?.amount),
|
|
1104
|
+
sma: toNullableNumber(summary[`sma${suffix}`]?.amount),
|
|
1105
|
+
buyingPower: toNullableNumber(summary[`buyingpower${suffix}`]?.amount),
|
|
1106
|
+
fullInitialMarginRequirement: toNullableNumber(summary[`fullinitmarginreq${suffix}`]?.amount),
|
|
1107
|
+
fullMaintenanceMarginRequirement: toNullableNumber(summary[`fullmaintmarginreq${suffix}`]?.amount),
|
|
1108
|
+
fullAvailableFunds: toNullableNumber(summary[`fullavailablefunds${suffix}`]?.amount),
|
|
1109
|
+
fullExcessLiquidity: toNullableNumber(summary[`fullexcessliquidity${suffix}`]?.amount),
|
|
1110
|
+
lookAheadInitialMarginRequirement: toNullableNumber(summary[`lookaheadinitmarginreq${suffix}`]?.amount),
|
|
1111
|
+
lookAheadMaintenanceMarginRequirement: toNullableNumber(summary[`lookaheadmaintmarginreq${suffix}`]?.amount),
|
|
1112
|
+
lookAheadAvailableFunds: toNullableNumber(summary[`lookaheadavailablefunds${suffix}`]?.amount),
|
|
1113
|
+
lookAheadExcessLiquidity: toNullableNumber(summary[`lookaheadexcessliquidity${suffix}`]?.amount),
|
|
1114
|
+
lookAheadNextChange: toNullableNumber(summary[`lookaheadnextchange${suffix}`]?.amount),
|
|
1115
|
+
leverage: toNullableNumber(summary[`leverage${suffix}`]?.amount),
|
|
1116
|
+
});
|
|
653
1117
|
return {
|
|
654
1118
|
netLiquidation: amount("netliquidation"),
|
|
655
1119
|
availableFunds: amount("availablefunds"),
|
|
656
1120
|
buyingPower: amount("buyingpower"),
|
|
657
1121
|
cashBalance: amount("totalcashvalue"),
|
|
1122
|
+
margin: {
|
|
1123
|
+
total: marginSnapshot(""),
|
|
1124
|
+
securities: marginSnapshot("-s"),
|
|
1125
|
+
commodities: marginSnapshot("-c"),
|
|
1126
|
+
},
|
|
658
1127
|
};
|
|
659
1128
|
}
|
|
660
1129
|
async getPositions(symbol) {
|
|
@@ -942,13 +1411,17 @@ export class IbkrClient {
|
|
|
942
1411
|
};
|
|
943
1412
|
}
|
|
944
1413
|
liveOrderMatchesGraphNode(request, node, order) {
|
|
1414
|
+
const parentIdentity = this.consistentStringAliases(order.parentId, order.parent_id, order.parentClientOrderId, order.parent_order_ref);
|
|
1415
|
+
if (!parentIdentity.valid)
|
|
1416
|
+
return false;
|
|
945
1417
|
if (node.parentMemberId === undefined) {
|
|
946
|
-
|
|
1418
|
+
const clientIdentity = this.consistentStringAliases(order.cOID, order.order_ref);
|
|
1419
|
+
if (!clientIdentity.valid || clientIdentity.value !== request.rootClientOrderId)
|
|
947
1420
|
return false;
|
|
948
|
-
if (
|
|
1421
|
+
if (parentIdentity.value !== undefined)
|
|
949
1422
|
return false;
|
|
950
1423
|
}
|
|
951
|
-
else if (
|
|
1424
|
+
else if (parentIdentity.value !== request.rootClientOrderId)
|
|
952
1425
|
return false;
|
|
953
1426
|
if ("legs" in node) {
|
|
954
1427
|
const liveLegs = this.parseComboLegs(order.conidex);
|
|
@@ -958,6 +1431,7 @@ export class IbkrClient {
|
|
|
958
1431
|
const price = this.firstNumber(order.limitPrice, order.limit_price, order.price);
|
|
959
1432
|
const expectedPrice = node.priceEffect === "CREDIT" ? -node.limit : node.limit;
|
|
960
1433
|
const outsideRth = order.outsideRTH ?? order.outside_rth;
|
|
1434
|
+
const tif = order.tif ?? order.timeInForce;
|
|
961
1435
|
return (liveLegs.length === node.legs.length &&
|
|
962
1436
|
node.legs.every((leg, index) => {
|
|
963
1437
|
const liveLeg = liveLegs[index];
|
|
@@ -967,7 +1441,7 @@ export class IbkrClient {
|
|
|
967
1441
|
side === "BUY" &&
|
|
968
1442
|
quantity === node.quantity &&
|
|
969
1443
|
price === expectedPrice &&
|
|
970
|
-
(
|
|
1444
|
+
(tif === undefined || tif.toUpperCase() === node.tif) &&
|
|
971
1445
|
(outsideRth === undefined || outsideRth === (node.session === "OVERNIGHT")));
|
|
972
1446
|
}
|
|
973
1447
|
const orderType = this.normalizeOrderType(order.order_type ?? order.orderType);
|
|
@@ -981,14 +1455,39 @@ export class IbkrClient {
|
|
|
981
1455
|
: undefined;
|
|
982
1456
|
const expectedPrice = node.orderType === "LMT" ? node.limit : node.orderType === "STP" ? node.stopPrice : undefined;
|
|
983
1457
|
const outsideRth = order.outsideRTH ?? order.outside_rth;
|
|
1458
|
+
const tif = order.tif ?? order.timeInForce;
|
|
984
1459
|
return (order.conid === node.contract.conid &&
|
|
985
1460
|
orderType === expectedOrderType &&
|
|
986
1461
|
side === node.side &&
|
|
987
1462
|
quantity === node.quantity &&
|
|
988
1463
|
price === expectedPrice &&
|
|
989
|
-
(
|
|
1464
|
+
(tif === undefined || tif.toUpperCase() === node.tif) &&
|
|
990
1465
|
(outsideRth === undefined || outsideRth === (node.session === "OVERNIGHT")));
|
|
991
1466
|
}
|
|
1467
|
+
recoveryOrderMatchesGraphNode(request, node, order) {
|
|
1468
|
+
try {
|
|
1469
|
+
return this.liveOrderMatchesGraphNode(request, node, order);
|
|
1470
|
+
}
|
|
1471
|
+
catch {
|
|
1472
|
+
return false;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
terminalOrderMatchesGraphNode(request, node, order) {
|
|
1476
|
+
try {
|
|
1477
|
+
const tif = order.tif ?? order.timeInForce;
|
|
1478
|
+
const outsideRth = order.outsideRTH ?? order.outside_rth;
|
|
1479
|
+
const status = order.order_status ?? order.orderStatus ?? order.status;
|
|
1480
|
+
if (typeof tif !== "string" ||
|
|
1481
|
+
typeof outsideRth !== "boolean" ||
|
|
1482
|
+
typeof status !== "string") {
|
|
1483
|
+
return false;
|
|
1484
|
+
}
|
|
1485
|
+
return this.recoveryOrderMatchesGraphNode(request, node, order);
|
|
1486
|
+
}
|
|
1487
|
+
catch {
|
|
1488
|
+
return false;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
992
1491
|
validateSingleOrder(request) {
|
|
993
1492
|
this.validateSingleOrderFields(request);
|
|
994
1493
|
const identityFields = request;
|
|
@@ -1486,11 +1985,42 @@ export class IbkrClient {
|
|
|
1486
1985
|
updatedAt: this.parseOrderTime(order)?.toISOString() ?? null,
|
|
1487
1986
|
};
|
|
1488
1987
|
}
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1988
|
+
flattenCompleteOrderSnapshot(response) {
|
|
1989
|
+
if (typeof response !== "object" || response === null || Array.isArray(response))
|
|
1990
|
+
return null;
|
|
1991
|
+
const record = response;
|
|
1992
|
+
if (record["snapshot"] !== true || !Array.isArray(record["orders"]))
|
|
1993
|
+
return null;
|
|
1994
|
+
const flattened = [];
|
|
1995
|
+
const visiting = new Set();
|
|
1996
|
+
const visit = (orders, nestedParent) => {
|
|
1997
|
+
for (const rawOrder of orders) {
|
|
1998
|
+
if (typeof rawOrder !== "object" || rawOrder === null || Array.isArray(rawOrder))
|
|
1999
|
+
return false;
|
|
2000
|
+
if (visiting.has(rawOrder))
|
|
2001
|
+
return false;
|
|
2002
|
+
visiting.add(rawOrder);
|
|
2003
|
+
const orderRecord = rawOrder;
|
|
2004
|
+
const childCollections = [orderRecord["childOrders"], orderRecord["children"]];
|
|
2005
|
+
if (childCollections.some((children) => children !== undefined && !Array.isArray(children))) {
|
|
2006
|
+
return false;
|
|
2007
|
+
}
|
|
2008
|
+
const order = rawOrder;
|
|
2009
|
+
flattened.push({ order, nestedParent });
|
|
2010
|
+
const children = new Set();
|
|
2011
|
+
for (const collection of childCollections) {
|
|
2012
|
+
if (Array.isArray(collection)) {
|
|
2013
|
+
for (const child of collection)
|
|
2014
|
+
children.add(child);
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
if (!visit([...children], order))
|
|
2018
|
+
return false;
|
|
2019
|
+
visiting.delete(rawOrder);
|
|
2020
|
+
}
|
|
2021
|
+
return true;
|
|
2022
|
+
};
|
|
2023
|
+
return visit(record["orders"], null) ? flattened : null;
|
|
1494
2024
|
}
|
|
1495
2025
|
normalizeActiveDerivativeOrder(accountId, order, nestedParent) {
|
|
1496
2026
|
const uncertainty = [];
|
|
@@ -1694,8 +2224,39 @@ export class IbkrClient {
|
|
|
1694
2224
|
const parsed = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}Z`);
|
|
1695
2225
|
return Number.isNaN(parsed.getTime()) ? null : parsed.toISOString();
|
|
1696
2226
|
}
|
|
2227
|
+
recoveryOrderId(order) {
|
|
2228
|
+
const identity = this.consistentScalarAliases(order.order_id, order.orderId);
|
|
2229
|
+
return identity.valid ? identity.value : undefined;
|
|
2230
|
+
}
|
|
2231
|
+
consistentScalarAliases(...aliases) {
|
|
2232
|
+
const provided = aliases.filter((alias) => alias !== undefined);
|
|
2233
|
+
if (provided.length === 0)
|
|
2234
|
+
return { valid: true, value: undefined };
|
|
2235
|
+
const normalized = provided.map((alias) => {
|
|
2236
|
+
if (typeof alias !== "string" && typeof alias !== "number")
|
|
2237
|
+
return undefined;
|
|
2238
|
+
const value = String(alias).trim();
|
|
2239
|
+
return value.length === 0 ? undefined : value;
|
|
2240
|
+
});
|
|
2241
|
+
const [first] = normalized;
|
|
2242
|
+
if (first === undefined || normalized.some((value) => value !== first)) {
|
|
2243
|
+
return { valid: false, value: undefined };
|
|
2244
|
+
}
|
|
2245
|
+
return { valid: true, value: first };
|
|
2246
|
+
}
|
|
2247
|
+
consistentStringAliases(...aliases) {
|
|
2248
|
+
const provided = aliases.filter((alias) => alias !== undefined);
|
|
2249
|
+
if (provided.length === 0)
|
|
2250
|
+
return { valid: true, value: undefined };
|
|
2251
|
+
const normalized = provided.map((alias) => typeof alias === "string" && alias.trim() !== "" ? alias.trim() : undefined);
|
|
2252
|
+
const [first] = normalized;
|
|
2253
|
+
if (first === undefined || normalized.some((value) => value !== first)) {
|
|
2254
|
+
return { valid: false, value: undefined };
|
|
2255
|
+
}
|
|
2256
|
+
return { valid: true, value: first };
|
|
2257
|
+
}
|
|
1697
2258
|
trimmedString(value) {
|
|
1698
|
-
if (value
|
|
2259
|
+
if (typeof value !== "string")
|
|
1699
2260
|
return null;
|
|
1700
2261
|
const trimmed = value.trim();
|
|
1701
2262
|
return trimmed.length === 0 ? null : trimmed;
|
|
@@ -1875,8 +2436,10 @@ export class IbkrClient {
|
|
|
1875
2436
|
return "UNKNOWN";
|
|
1876
2437
|
}
|
|
1877
2438
|
canonicalIbkrOrderStatus(value) {
|
|
2439
|
+
if (typeof value !== "string")
|
|
2440
|
+
return undefined;
|
|
1878
2441
|
return value
|
|
1879
|
-
|
|
2442
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
1880
2443
|
.replace(/\s+/g, "_")
|
|
1881
2444
|
.toUpperCase();
|
|
1882
2445
|
}
|