@huskly/ibkr-client 2.4.3 → 2.7.1

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.
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { ASSET_CLASS_LABELS, toNullableNumber, toNumber } from "../helpers.js";
3
3
  import { normalizeOptionContract, parseOsiOptionSymbol } from "./optionContract.js";
4
+ import { normalizeEquityContract } from "./equityContract.js";
4
5
  import { normalizeDerivativeContract, normalizeDerivativeDataAvailability, } from "./derivativeContract.js";
5
6
  import { IbkrRequestScheduler, } from "./requestScheduler.js";
6
7
  // `ibkr-client`'s published ESM build is broken: its `import` condition points
@@ -572,6 +573,39 @@ export class IbkrClient {
572
573
  : [],
573
574
  };
574
575
  }
576
+ async previewEquityOrder(request) {
577
+ this.assertOpen();
578
+ this.validateEquityOrderFields(request);
579
+ return this.withTradingMutation(request.accountId, "IBKR brokerage session is not safely authenticated for What-If", async (diagnostics) => {
580
+ await this.req({
581
+ path: "iserver/marketdata/snapshot",
582
+ params: { conids: String(request.contract.conid), fields: "6509" },
583
+ });
584
+ const response = await this.singleAttemptRequest({
585
+ path: `iserver/account/${request.accountId}/orders/whatif`,
586
+ method: "POST",
587
+ data: { orders: [this.equityOrderTicket(request)] },
588
+ });
589
+ return this.normalizeComboPreview(request.accountId, diagnostics, response);
590
+ });
591
+ }
592
+ async submitEquityOrder(request) {
593
+ this.assertOpen();
594
+ this.validateEquityOrderFields(request);
595
+ if (!request.clientOrderId.trim() || request.clientOrderId.length > 64) {
596
+ throw new Error("Client order ID must contain 1 to 64 characters");
597
+ }
598
+ return this.withTradingMutation(request.accountId, "IBKR brokerage session is not safely authenticated for submission", async () => {
599
+ const response = await this.singleAttemptRequest({
600
+ path: `iserver/account/${request.accountId}/orders`,
601
+ method: "POST",
602
+ data: {
603
+ orders: [{ ...this.equityOrderTicket(request), cOID: request.clientOrderId }],
604
+ },
605
+ });
606
+ return this.normalizeOrderSubmission(response, request.clientOrderId);
607
+ });
608
+ }
575
609
  async previewDerivativeCombo(request) {
576
610
  this.assertOpen();
577
611
  this.validateComboPreview(request);
@@ -1578,6 +1612,15 @@ export class IbkrClient {
1578
1612
  await this.wait(Math.min(pollMs, Math.max(0, deadline - this.now())));
1579
1613
  }
1580
1614
  }
1615
+ getEquityOrderStatus(accountId, orderId) {
1616
+ return this.getDerivativeOrderStatus(accountId, orderId);
1617
+ }
1618
+ findEquityOrder(input) {
1619
+ return this.findDerivativeOrder(input);
1620
+ }
1621
+ cancelEquityOrder(input) {
1622
+ return this.cancelDerivativeOrder({ ...input, assetClass: "STK" });
1623
+ }
1581
1624
  async cancelDerivativeOrder(input) {
1582
1625
  this.assertOpen();
1583
1626
  if (!input.accountId.trim() || !input.orderId.trim()) {
@@ -1979,6 +2022,72 @@ export class IbkrClient {
1979
2022
  return quotes;
1980
2023
  }
1981
2024
  /** Resolve equity/ETF symbols to IBKR contracts via `trsrv/stocks`. */
2025
+ /** Resolve one exact SMART-routed US stock or ETF contract. */
2026
+ async resolveEquityContract(symbol) {
2027
+ this.assertOpen();
2028
+ const requestedSymbol = symbol.trim().toUpperCase();
2029
+ if (!requestedSymbol || /[\r\n\t]/.test(symbol)) {
2030
+ throw new Error("Equity contract resolution requires a usable symbol");
2031
+ }
2032
+ const response = await this.req({
2033
+ path: "trsrv/stocks",
2034
+ params: { symbols: requestedSymbol },
2035
+ });
2036
+ if (!isUnknownRecord(response)) {
2037
+ throw new Error("IBKR returned incomplete exact US equity search evidence");
2038
+ }
2039
+ const rawListings = response[requestedSymbol];
2040
+ if (!Array.isArray(rawListings)) {
2041
+ throw new Error("IBKR returned no exact US equity listing evidence");
2042
+ }
2043
+ const byConid = new Map();
2044
+ let conflicting = false;
2045
+ for (const rawListing of rawListings) {
2046
+ if (!isUnknownRecord(rawListing) || rawListing["assetClass"] !== "STK")
2047
+ continue;
2048
+ const contracts = rawListing["contracts"];
2049
+ if (!Array.isArray(contracts))
2050
+ continue;
2051
+ for (const rawContract of contracts) {
2052
+ if (!isUnknownRecord(rawContract) || rawContract["isUS"] !== true)
2053
+ continue;
2054
+ const conid = rawContract["conid"];
2055
+ const primaryExchange = this.trimmedString(rawContract["exchange"])?.toUpperCase();
2056
+ if (!Number.isSafeInteger(conid) || conid <= 0 || !primaryExchange)
2057
+ continue;
2058
+ const previous = byConid.get(conid);
2059
+ if (previous !== undefined && previous !== primaryExchange)
2060
+ conflicting = true;
2061
+ byConid.set(conid, primaryExchange);
2062
+ }
2063
+ }
2064
+ if (conflicting || byConid.size !== 1) {
2065
+ throw new Error("IBKR did not return one exact US equity listing");
2066
+ }
2067
+ const [candidate] = byConid;
2068
+ if (candidate === undefined)
2069
+ throw new Error("IBKR returned no exact US equity listing");
2070
+ const [conid, primaryExchange] = candidate;
2071
+ const evidence = this.contractReferenceEvidence(conid, await this.readContractReference(conid));
2072
+ const validExchanges = evidence.validExchanges
2073
+ ?.split(",")
2074
+ .map((value) => value.trim().toUpperCase())
2075
+ .filter(Boolean) ?? [];
2076
+ const contract = normalizeEquityContract({
2077
+ conid: evidence.conid,
2078
+ assetClass: evidence.instrumentType?.toUpperCase(),
2079
+ symbol: evidence.symbol,
2080
+ exchange: evidence.exchange,
2081
+ primaryExchange,
2082
+ currency: evidence.currency,
2083
+ });
2084
+ if (contract?.conid !== conid ||
2085
+ contract.symbol !== requestedSymbol ||
2086
+ !validExchanges.includes("SMART")) {
2087
+ throw new Error("IBKR equity contract details are incomplete or conflicting");
2088
+ }
2089
+ return contract;
2090
+ }
1982
2091
  async searchInstruments(symbol, projection = "symbol-search") {
1983
2092
  this.assertOpen();
1984
2093
  if (projection !== "symbol-search" && projection !== "search") {
@@ -2439,6 +2548,44 @@ export class IbkrClient {
2439
2548
  return false;
2440
2549
  }
2441
2550
  }
2551
+ validateEquityOrderFields(request) {
2552
+ if (!request.accountId.trim())
2553
+ throw new Error("An explicit IBKR account ID is required");
2554
+ if (normalizeEquityContract(request.contract) === null) {
2555
+ throw new Error("Equity order requires one exact US equity contract");
2556
+ }
2557
+ const fields = request;
2558
+ if (fields["side"] !== "BUY" && fields["side"] !== "SELL") {
2559
+ throw new Error("Equity order side must be BUY or SELL");
2560
+ }
2561
+ if (!Number.isSafeInteger(fields["quantity"]) || fields["quantity"] <= 0) {
2562
+ throw new Error("Equity order quantity must be a positive integer");
2563
+ }
2564
+ if (fields["orderType"] !== "LMT" ||
2565
+ typeof fields["limit"] !== "number" ||
2566
+ !Number.isFinite(fields["limit"]) ||
2567
+ fields["limit"] <= 0) {
2568
+ throw new Error("Equity LIMIT order requires a positive limit price");
2569
+ }
2570
+ if (fields["tif"] !== "DAY" && fields["tif"] !== "GTC") {
2571
+ throw new Error("Equity order TIF must be DAY or GTC");
2572
+ }
2573
+ if (fields["session"] !== "REGULAR" && fields["session"] !== "OVERNIGHT") {
2574
+ throw new Error("Equity order session must be REGULAR or OVERNIGHT");
2575
+ }
2576
+ }
2577
+ equityOrderTicket(request) {
2578
+ return {
2579
+ acctId: request.accountId,
2580
+ conid: request.contract.conid,
2581
+ orderType: "LMT",
2582
+ side: request.side,
2583
+ price: request.limit,
2584
+ tif: request.tif,
2585
+ quantity: request.quantity,
2586
+ outsideRTH: request.session === "OVERNIGHT",
2587
+ };
2588
+ }
2442
2589
  validateSingleOrder(request) {
2443
2590
  this.validateSingleOrderFields(request);
2444
2591
  const identityFields = request;
@@ -2489,7 +2636,7 @@ export class IbkrClient {
2489
2636
  }
2490
2637
  }
2491
2638
  cmeOperatorMetadata(assetClass, input) {
2492
- if (assetClass === "OPT")
2639
+ if (assetClass !== "FOP")
2493
2640
  return {};
2494
2641
  if (!input.extOperator?.trim() || input.manualIndicator === undefined) {
2495
2642
  throw new Error(`${assetClass} orders require exact CME operator metadata`);