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();