diff --git a/src/api-next/api-next.ts b/src/api-next/api-next.ts index 38c1731f..0bede87d 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], ], ) @@ -2409,8 +2410,6 @@ export class IBApiNext { legStr, }; - // console.log("onScannerData", item); - const lastAllValue = subscription.lastAllValue ?? new Map(); @@ -2429,7 +2428,6 @@ export class IBApiNext { added: existing ? undefined : updated, }); } else { - // console.log("saving for future use", lastValue); subscription.lastAllValue = lastAllValue; } }; 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 "; const OPTION_ARGUMENTS: [string, string][] = [ ...IBApiNextApp.DEFAULT_CONTRACT_OPTIONS, - ["ticks=", "Comma separated list of generic ticks to fetch."], + // Snapshot market data subscription is not applicable to generic ticks (Error #321) + // ["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(); 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(); },