From 1c33af72ea30652df77ba176d0577ebce42e7eb5 Mon Sep 17 00:00:00 2001
From: Ronan-Yann Lorin <ryl@free.fr>
Date: Wed, 29 Nov 2023 15:22:53 +0100
Subject: [PATCH 1/2] getContractDetails implemented for bonds

---
 src/api-next/api-next.ts                      |   6 +
 src/api/contract/bond.ts                      |  24 ++++
 src/api/contract/future.ts                    |   2 +-
 src/index.ts                                  |   1 +
 .../api-next/get-contract-details.test.ts     | 131 +++++++++++++++++-
 src/tests/unit/api-next/place-order.test.ts   |   1 -
 src/tests/unit/contracts.ts                   |  21 +++
 src/tools/market-data-snapshot.ts             |  12 +-
 src/tools/market-scanner.ts                   |   3 +-
 9 files changed, 189 insertions(+), 12 deletions(-)
 create mode 100644 src/api/contract/bond.ts
 create mode 100644 src/tests/unit/contracts.ts

diff --git a/src/api-next/api-next.ts b/src/api-next/api-next.ts
index 38c1731f..5372129b 100644
--- a/src/api-next/api-next.ts
+++ b/src/api-next/api-next.ts
@@ -921,6 +921,7 @@ export class IBApiNext {
           undefined,
           [
             [EventName.contractDetails, this.onContractDetails],
+            [EventName.bondContractDetails, this.onContractDetails],
             [EventName.contractDetailsEnd, this.onContractDetailsEnd],
           ],
         )
@@ -2565,6 +2566,7 @@ export class IBApiNext {
     order: Order,
     orderState: OrderState,
   ): void => {
+    console.log("onOpenOrder");
     subscriptions.forEach((sub) => {
       const allOrders = sub.lastAllValue ?? [];
       const changeOrderIndex = allOrders.findIndex(
@@ -2615,6 +2617,7 @@ export class IBApiNext {
   private readonly onOpenOrderComplete = (
     subscriptions: Map<number, IBApiNextSubscription<OpenOrder[]>>,
   ): void => {
+    console.log("onOpenOrderComplete");
     subscriptions.forEach((sub) => {
       const allOrders = sub.lastAllValue ?? [];
       sub.endEventReceived = true;
@@ -2730,6 +2733,7 @@ export class IBApiNext {
   private readonly onOpenOrderEnd = (
     subscriptions: Map<number, IBApiNextSubscription<OpenOrder[]>>,
   ): void => {
+    console.log("onOpenOrderEnd");
     // notify all subscribers
     subscriptions.forEach((subscription) => {
       const lastAllValue = subscription.lastAllValue ?? [];
@@ -2742,6 +2746,7 @@ export class IBApiNext {
    * Requests all current open orders in associated accounts at the current moment.
    */
   getAllOpenOrders(): Promise<OpenOrder[]> {
+    console.log("getAllOpenOrders");
     return lastValueFrom(
       this.subscriptions
         .register<OpenOrder[]>(
@@ -2769,6 +2774,7 @@ export class IBApiNext {
    * For client ID 0, this will bind previous manual TWS orders.
    */
   getOpenOrders(): Observable<OpenOrdersUpdate> {
+    console.log("getOpenOrders");
     return this.subscriptions.register<OpenOrder[]>(
       () => {
         this.api.reqOpenOrders();
diff --git a/src/api/contract/bond.ts b/src/api/contract/bond.ts
new file mode 100644
index 00000000..b750228c
--- /dev/null
+++ b/src/api/contract/bond.ts
@@ -0,0 +1,24 @@
+import SecType from "../data/enum/sec-type";
+import { Contract } from "./contract";
+
+/**
+ * A Bond Contract
+ */
+export class Bond implements Contract {
+  constructor(
+    public symbol: string,
+    public maturity?: string,
+    public exchange?: string,
+    public currency?: string,
+  ) {
+    this.currency = this.currency ?? "USD";
+  }
+
+  public secType = SecType.BOND;
+
+  public get lastTradeDateOrContractMonth(): string {
+    return this.maturity;
+  }
+}
+
+export default Bond;
diff --git a/src/api/contract/future.ts b/src/api/contract/future.ts
index 42d50a92..07e2eb42 100644
--- a/src/api/contract/future.ts
+++ b/src/api/contract/future.ts
@@ -2,7 +2,7 @@ import SecType from "../data/enum/sec-type";
 import { Contract } from "./contract";
 
 /**
- * A Future Option Contract
+ * A Future Contract
  */
 export class Future implements Contract {
   constructor(
diff --git a/src/index.ts b/src/index.ts
index 0b7c7c1c..b7c6a7bc 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -36,6 +36,7 @@ export { ErrorCode } from "./common/errorCode";
 
 // export contract types
 
+export { Bond } from "./api/contract/bond";
 export { CFD } from "./api/contract/cfd";
 export { Combo } from "./api/contract/combo";
 export { ComboLeg } from "./api/contract/comboLeg";
diff --git a/src/tests/unit/api-next/get-contract-details.test.ts b/src/tests/unit/api-next/get-contract-details.test.ts
index 1b955c17..c3666c98 100644
--- a/src/tests/unit/api-next/get-contract-details.test.ts
+++ b/src/tests/unit/api-next/get-contract-details.test.ts
@@ -2,7 +2,20 @@
  * This file implements tests for the [[IBApiNext.getContractDetails]] function.
  */
 
-import { ContractDetails, EventName, IBApi, IBApiNext, IBApiNextError } from "../../..";
+import { Subscription } from "rxjs";
+import {
+  ContractDetails,
+  EventName,
+  IBApi,
+  IBApiNext,
+  IBApiNextError,
+} from "../../..";
+import {
+  sample_bond,
+  sample_future,
+  sample_option,
+  sample_stock,
+} from "../contracts";
 
 describe("RxJS Wrapper: getContractDetails()", () => {
   test("Error Event", (done) => {
@@ -62,3 +75,119 @@ describe("RxJS Wrapper: getContractDetails()", () => {
     api.emit(EventName.contractDetailsEnd, 1);
   });
 });
+
+describe("ApiNext: getContractDetails()", () => {
+  jest.setTimeout(10 * 1000);
+
+  const clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client
+
+  let subscription$: Subscription;
+  let api: IBApiNext;
+  let error$: Subscription;
+
+  beforeEach(() => {
+    api = new IBApiNext();
+
+    if (!error$) {
+      error$ = api.errorSubject.subscribe((error) => {
+        if (error.reqId === -1) {
+          console.warn(`${error.error.message} (Error #${error.code})`);
+        } else {
+          console.error(
+            `${error.error.message} (Error #${error.code}) ${
+              error.advancedOrderReject ? error.advancedOrderReject : ""
+            }`,
+          );
+        }
+      });
+    }
+
+    try {
+      api.connect(clientId);
+    } catch (error) {
+      console.error(error.message);
+    }
+  });
+
+  afterEach(() => {
+    if (api) {
+      api.disconnect();
+      api = undefined;
+    }
+  });
+
+  test("Stock contract details", (done) => {
+    api
+      .getContractDetails(sample_stock)
+      .then((result) => {
+        // console.log(result);
+        expect(result.length).toBeGreaterThan(0);
+        if (result.length) {
+          expect(result[0].contract.symbol).toEqual(sample_stock.symbol);
+          expect(result[0].contract.secType).toEqual(sample_stock.secType);
+        }
+        done();
+      })
+      .catch((err: IBApiNextError) => {
+        done(
+          `getContractDetails failed with '${err.error.message}' (Error #${err.code})`,
+        );
+      });
+  });
+
+  test("Future contract details", (done) => {
+    api
+      .getContractDetails(sample_future)
+      .then((result) => {
+        // console.log(result);
+        expect(result.length).toBeGreaterThan(0);
+        if (result.length) {
+          expect(result[0].contract.symbol).toEqual(sample_future.symbol);
+          expect(result[0].contract.secType).toEqual(sample_future.secType);
+        }
+        done();
+      })
+      .catch((err: IBApiNextError) => {
+        done(
+          `getContractDetails failed with '${err.error.message}' (Error #${err.code})`,
+        );
+      });
+  });
+
+  test("Option contract details", (done) => {
+    api
+      .getContractDetails(sample_option)
+      .then((result) => {
+        // console.log(result);
+        expect(result.length).toBeGreaterThan(0);
+        if (result.length) {
+          expect(result[0].contract.symbol).toEqual(sample_option.symbol);
+          expect(result[0].contract.secType).toEqual(sample_option.secType);
+        }
+        done();
+      })
+      .catch((err: IBApiNextError) => {
+        done(
+          `getContractDetails failed with '${err.error.message}' (Error #${err.code})`,
+        );
+      });
+  });
+
+  test("Bond contract details", (done) => {
+    api
+      .getContractDetails(sample_bond)
+      .then((result) => {
+        // console.log(result);
+        expect(result.length).toBeGreaterThan(0);
+        if (result.length) {
+          expect(result[0].contract.secType).toEqual(sample_bond.secType);
+        }
+        done();
+      })
+      .catch((err: IBApiNextError) => {
+        done(
+          `getContractDetails failed with '${err.error.message}' (Error #${err.code})`,
+        );
+      });
+  });
+});
diff --git a/src/tests/unit/api-next/place-order.test.ts b/src/tests/unit/api-next/place-order.test.ts
index 1634509a..a22df96a 100644
--- a/src/tests/unit/api-next/place-order.test.ts
+++ b/src/tests/unit/api-next/place-order.test.ts
@@ -11,7 +11,6 @@ import IBApi, {
   Stock,
 } from "../../..";
 import configuration from "../../../common/configuration";
-// import configuration from "../../../common/configuration";
 
 describe("Place orders to IB", () => {
   test("Error Event", (done) => {
diff --git a/src/tests/unit/contracts.ts b/src/tests/unit/contracts.ts
new file mode 100644
index 00000000..02e10959
--- /dev/null
+++ b/src/tests/unit/contracts.ts
@@ -0,0 +1,21 @@
+/**
+ * This file describe sample contracts to be used in various tests code.
+ */
+import { Bond, Contract, Future, Option, OptionType, Stock } from "../..";
+
+export const sample_stock: Contract = new Stock("AAPL");
+export const sample_etf: Contract = new Stock("SPY");
+export const sample_future: Contract = new Future(
+  "ES",
+  "ESZ3",
+  "202312",
+  "CME",
+  50,
+);
+export const sample_option: Contract = new Option(
+  "AAPL",
+  "20251219",
+  200,
+  OptionType.Put,
+);
+export const sample_bond: Contract = new Bond("912828C57");
diff --git a/src/tools/market-data-snapshot.ts b/src/tools/market-data-snapshot.ts
index 2556468a..65878121 100644
--- a/src/tools/market-data-snapshot.ts
+++ b/src/tools/market-data-snapshot.ts
@@ -16,7 +16,8 @@ const DESCRIPTION_TEXT =
 const USAGE_TEXT = "Usage: market-data-snapshot.js <options>";
 const OPTION_ARGUMENTS: [string, string][] = [
   ...IBApiNextApp.DEFAULT_CONTRACT_OPTIONS,
-  ["ticks=<ticks>", "Comma separated list of generic ticks to fetch."],
+  // Snapshot market data subscription is not applicable to generic ticks (Error #321)
+  // ["ticks=<ticks>", "Comma separated list of generic ticks to fetch."],
 ];
 const EXAMPLE_TEXT =
   "market-data-snapshot.js -symbol=AAPL -conid=265598 -sectype=STK -exchange=SMART";
@@ -30,7 +31,7 @@ class PrintMarketDataSingleApp extends IBApiNextApp {
     super(DESCRIPTION_TEXT, USAGE_TEXT, OPTION_ARGUMENTS, EXAMPLE_TEXT);
   }
 
-  /** The [[Subscription]] on the PnLSingle. */
+  /** The [[Subscription]] */
   private subscription$: Subscription;
 
   /**
@@ -40,13 +41,8 @@ class PrintMarketDataSingleApp extends IBApiNextApp {
     super.start();
 
     this.api
-      .getMarketDataSnapshot(
-        this.getContractArg(),
-        this.cmdLineArgs.ticks as string,
-        false,
-      )
+      .getMarketDataSnapshot(this.getContractArg(), "", false)
       .then((marketData) => {
-        // this.printObject(marketData);
         const dataWithTickNames = new Map<string, number>();
         marketData.forEach((tick, type) => {
           if (type > IBApiNextTickType.API_NEXT_FIRST_TICK_ID) {
diff --git a/src/tools/market-scanner.ts b/src/tools/market-scanner.ts
index 4a695c20..612ad146 100644
--- a/src/tools/market-scanner.ts
+++ b/src/tools/market-scanner.ts
@@ -7,6 +7,7 @@ import { IBApiNextError } from "../api-next";
 import {
   Instrument,
   LocationCode,
+  MarketScannerUpdate,
   ScanCode,
 } from "../api-next/market-scanner/market-scanner";
 import logger from "../common/logger";
@@ -48,7 +49,7 @@ class PrintMarketScreenerApp extends IBApiNextApp {
         numberOfRows: 20,
       })
       .subscribe({
-        next: (data) => {
+        next: (data: MarketScannerUpdate) => {
           this.printObject(data.all);
           if (!this.cmdLineArgs.watch) this.stop();
         },

From 8f67cb5a4c45a7db352e9026fe184c59897a5d5f Mon Sep 17 00:00:00 2001
From: Ronan-Yann Lorin <ryl@free.fr>
Date: Wed, 29 Nov 2023 18:13:56 +0100
Subject: [PATCH 2/2] console.log removed

---
 src/api-next/api-next.ts | 8 --------
 1 file changed, 8 deletions(-)

diff --git a/src/api-next/api-next.ts b/src/api-next/api-next.ts
index 5372129b..0bede87d 100644
--- a/src/api-next/api-next.ts
+++ b/src/api-next/api-next.ts
@@ -2410,8 +2410,6 @@ export class IBApiNext {
       legStr,
     };
 
-    // console.log("onScannerData", item);
-
     const lastAllValue =
       subscription.lastAllValue ??
       new Map<MarketScannerItemRank, MarketScannerItem>();
@@ -2430,7 +2428,6 @@ export class IBApiNext {
         added: existing ? undefined : updated,
       });
     } else {
-      // console.log("saving for future use", lastValue);
       subscription.lastAllValue = lastAllValue;
     }
   };
@@ -2566,7 +2563,6 @@ export class IBApiNext {
     order: Order,
     orderState: OrderState,
   ): void => {
-    console.log("onOpenOrder");
     subscriptions.forEach((sub) => {
       const allOrders = sub.lastAllValue ?? [];
       const changeOrderIndex = allOrders.findIndex(
@@ -2617,7 +2613,6 @@ export class IBApiNext {
   private readonly onOpenOrderComplete = (
     subscriptions: Map<number, IBApiNextSubscription<OpenOrder[]>>,
   ): void => {
-    console.log("onOpenOrderComplete");
     subscriptions.forEach((sub) => {
       const allOrders = sub.lastAllValue ?? [];
       sub.endEventReceived = true;
@@ -2733,7 +2728,6 @@ export class IBApiNext {
   private readonly onOpenOrderEnd = (
     subscriptions: Map<number, IBApiNextSubscription<OpenOrder[]>>,
   ): void => {
-    console.log("onOpenOrderEnd");
     // notify all subscribers
     subscriptions.forEach((subscription) => {
       const lastAllValue = subscription.lastAllValue ?? [];
@@ -2746,7 +2740,6 @@ export class IBApiNext {
    * Requests all current open orders in associated accounts at the current moment.
    */
   getAllOpenOrders(): Promise<OpenOrder[]> {
-    console.log("getAllOpenOrders");
     return lastValueFrom(
       this.subscriptions
         .register<OpenOrder[]>(
@@ -2774,7 +2767,6 @@ export class IBApiNext {
    * For client ID 0, this will bind previous manual TWS orders.
    */
   getOpenOrders(): Observable<OpenOrdersUpdate> {
-    console.log("getOpenOrders");
     return this.subscriptions.register<OpenOrder[]>(
       () => {
         this.api.reqOpenOrders();