From 39128d32cfa7be59200407267020be16a3cd5bb0 Mon Sep 17 00:00:00 2001 From: Ronan-Yann Lorin Date: Sun, 10 Mar 2024 22:19:27 +0100 Subject: [PATCH] Multiple improvements to tests suites --- .github/workflows/check.yml | 1 + src/api/api.ts | 4 +- src/api/contract/crypto.ts | 11 ++- .../subscription-registry.test.ts | 2 +- src/tests/unit/api/contract-details.test.ts | 74 +++++++------------ src/tests/unit/api/historical-data.test.ts | 14 ++-- src/tests/unit/api/order/cancelOrder.test.ts | 52 +++++++------ .../api/order/placeConditionalOrder.test.ts | 58 ++++++++++++--- src/tests/unit/api/wsh-event-data.test.ts | 9 ++- src/tests/unit/sample-data/conditions.ts | 53 ------------- src/tests/unit/sample-data/contracts.ts | 38 ++++++---- 11 files changed, 151 insertions(+), 165 deletions(-) delete mode 100644 src/tests/unit/sample-data/conditions.ts diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index ebb3583b..fa8629a5 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -19,6 +19,7 @@ jobs: uses: actions/setup-node@v4 with: node-version: 18.19.1 + cache: "yarn" # caches the yarn cache folder not node_modules - name: Prepare run: yarn install --frozen-lockfile diff --git a/src/api/api.ts b/src/api/api.ts index 34e39ac6..6c1fc499 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -3,7 +3,7 @@ */ /* eslint @typescript-eslint/no-unsafe-declaration-merging:warn */ import { EventEmitter } from "eventemitter3"; -import { DurationUnit, MarketDataType, WhatToShow } from ".."; +import { DurationUnit, MarketDataType, OrderStatus, WhatToShow } from ".."; import { ErrorCode } from "../common/errorCode"; import { Controller } from "../core/io/controller"; @@ -2582,7 +2582,7 @@ export declare interface IBApi { event: EventName.orderStatus, listener: ( orderId: number, - status: string, + status: OrderStatus, filled: number, remaining: number, avgFillPrice: number, diff --git a/src/api/contract/crypto.ts b/src/api/contract/crypto.ts index 4b3ef9c2..1311cfb1 100644 --- a/src/api/contract/crypto.ts +++ b/src/api/contract/crypto.ts @@ -5,10 +5,15 @@ import { Contract } from "./contract"; * Crypto contract. */ export class Crypto implements Contract { - constructor(public symbol: string) {} + constructor( + public symbol: string, + public exchange?: string, + public currency?: string, + ) { + this.currency = this.currency ?? "USD"; + this.exchange = this.exchange ?? "PAXOS"; + } - public currency = "USD"; - public exchange = "PAXOS"; public secType = SecType.CRYPTO; } diff --git a/src/tests/unit/api-next-live/subscription-registry.test.ts b/src/tests/unit/api-next-live/subscription-registry.test.ts index 533a902f..aebb4fe9 100644 --- a/src/tests/unit/api-next-live/subscription-registry.test.ts +++ b/src/tests/unit/api-next-live/subscription-registry.test.ts @@ -43,7 +43,7 @@ describe("Subscription registry Tests", () => { it("Twice the same event callback bug", (done) => { // Two active subscriptions for the same Event #193 - done(); + done("Please fix issue #193"); return; subscription$ = api.getOpenOrders().subscribe({ next: (data) => { diff --git a/src/tests/unit/api/contract-details.test.ts b/src/tests/unit/api/contract-details.test.ts index 72fc232d..fa1acd66 100644 --- a/src/tests/unit/api/contract-details.test.ts +++ b/src/tests/unit/api/contract-details.test.ts @@ -1,17 +1,9 @@ /** * This file implements tests for the [[reqContractDetails]] API entry point. */ -import { - ContractDetails, - EventName, - Forex, - IBApi, - Option, - OptionType, - SecType, - Stock, -} from "../../.."; +import { ContractDetails, EventName, Forex, IBApi } from "../../.."; import configuration from "../../../common/configuration"; +import { sample_option, sample_stock } from "../sample-data/contracts"; describe("IBApi reqContractDetails Tests", () => { jest.setTimeout(5000); @@ -36,22 +28,21 @@ describe("IBApi reqContractDetails Tests", () => { test("Forex", (done) => { const refId = 1; + const refContract = new Forex("USD", "EUR"); ib.once(EventName.nextValidId, (_reqId) => { - const contract = new Forex("USD", "EUR"); - ib.reqContractDetails(refId, contract); + ib.reqContractDetails(refId, refContract); }) .on(EventName.contractDetails, (reqId, details: ContractDetails) => { expect(reqId).toEqual(refId); - expect(details.contract.secType).toEqual(SecType.CASH); - expect(details.contract.symbol).toEqual("EUR"); - expect(details.contract.currency).toEqual("USD"); - expect(details.marketName).toEqual("EUR.USD"); + expect(details.contract.secType).toEqual(refContract.secType); + expect(details.contract.symbol).toEqual(refContract.symbol); + expect(details.contract.currency).toEqual(refContract.currency); + expect(details.marketName).toEqual( + `${refContract.symbol}.${refContract.currency}`, + ); }) .on(EventName.contractDetailsEnd, (reqId) => { expect(reqId).toEqual(refId); - if (ib) ib.disconnect(); - }) - .on(EventName.disconnected, () => { done(); }) .on(EventName.error, (err, code, reqId) => { @@ -63,22 +54,18 @@ describe("IBApi reqContractDetails Tests", () => { test("Stock", (done) => { const refId = 2; + const refContract = sample_stock; ib.once(EventName.nextValidId, (_reqId) => { - const contract = new Stock("SPY", "ARCA", "USD"); - ib.reqContractDetails(refId, contract); + ib.reqContractDetails(refId, refContract); }) .on(EventName.contractDetails, (reqId, details: ContractDetails) => { expect(reqId).toEqual(refId); - expect(details.contract.secType).toEqual(SecType.STK); - expect(details.contract.symbol).toEqual("SPY"); - expect(details.contract.currency).toEqual("USD"); - expect(details.marketName).toEqual("SPY"); + expect(details.contract.secType).toEqual(refContract.secType); + expect(details.contract.symbol).toEqual(refContract.symbol); + expect(details.contract.currency).toEqual(refContract.currency); }) .on(EventName.contractDetailsEnd, (reqId) => { expect(reqId).toEqual(refId); - if (ib) ib.disconnect(); - }) - .on(EventName.disconnected, () => { done(); }) .on(EventName.error, (err, code, reqId) => { @@ -90,23 +77,19 @@ describe("IBApi reqContractDetails Tests", () => { test("Option", (done) => { const refId = 3; + const refContract = sample_option; ib.once(EventName.nextValidId, (_reqId) => { - const contract = new Option("SPY", "20260116", 440, OptionType.Call); - ib.reqContractDetails(refId, contract); + ib.reqContractDetails(refId, refContract); }) .on(EventName.contractDetails, (reqId, details: ContractDetails) => { expect(reqId).toEqual(refId); - expect(details.contract.secType).toEqual(SecType.OPT); - expect(details.contract.symbol).toEqual("SPY"); - expect(details.contract.currency).toEqual("USD"); + expect(details.contract.secType).toEqual(refContract.secType); + expect(details.contract.symbol).toEqual(refContract.symbol); + expect(details.contract.currency).toEqual(refContract.currency); expect(details.contract.conId).toEqual(653318228); - expect(details.marketName).toEqual("SPY"); }) .on(EventName.contractDetailsEnd, (reqId) => { expect(reqId).toEqual(refId); - if (ib) ib.disconnect(); - }) - .on(EventName.disconnected, () => { done(); }) .on(EventName.error, (err, code, reqId) => { @@ -120,26 +103,21 @@ describe("IBApi reqContractDetails Tests", () => { const refId = 4; let count = 0; + const refContract = sample_option; + refContract.strike = 0; ib.once(EventName.nextValidId, (_reqId) => { - const contract = new Option("SPY", "20260116", 0, OptionType.Call); - ib.reqContractDetails(refId, contract); + ib.reqContractDetails(refId, refContract); }) .on(EventName.contractDetails, (reqId, details: ContractDetails) => { expect(reqId).toEqual(refId); - expect(details.contract.secType).toEqual(SecType.OPT); - expect(details.contract.symbol).toEqual("SPY"); - expect(details.contract.currency).toEqual("USD"); - expect(details.marketName).toEqual("SPY"); - // console.log(details.contract); + expect(details.contract.secType).toEqual(refContract.secType); + expect(details.contract.symbol).toEqual(refContract.symbol); + expect(details.contract.currency).toEqual(refContract.currency); count++; }) .on(EventName.contractDetailsEnd, (reqId) => { expect(reqId).toEqual(refId); expect(count).toBeGreaterThanOrEqual(92); - // console.log(count); - if (ib) ib.disconnect(); - }) - .on(EventName.disconnected, () => { done(); }) .on(EventName.error, (err, code, reqId) => { diff --git a/src/tests/unit/api/historical-data.test.ts b/src/tests/unit/api/historical-data.test.ts index 7f26f8a8..41b1787c 100644 --- a/src/tests/unit/api/historical-data.test.ts +++ b/src/tests/unit/api/historical-data.test.ts @@ -87,9 +87,9 @@ describe("IBApi Historical data Tests", () => { expect(open).toEqual(429.5); expect(high).toEqual(429.6); expect(low).toEqual(429.47); - expect(close).toEqual(429.52); - expect(volume).toEqual(345338); - expect(count).toEqual(1076); + expect(close).toEqual(429.51); + expect(volume).toEqual(3487.38); + expect(count).toEqual(1090); expect(WAP).toEqual(429.532); } }, @@ -223,7 +223,7 @@ describe("IBApi Historical data Tests", () => { expect(high).toEqual(453.67); expect(low).toEqual(437.3); expect(close).toEqual(450.92); - expect(volume).toEqual(277178324); + expect(volume).toEqual(2771783.24); expect(count).toEqual(1393264); expect(WAP).toEqual(448.476); } @@ -288,7 +288,7 @@ describe("IBApi Historical data Tests", () => { expect(high).toEqual(453.67); expect(low).toEqual(449.68); expect(close).toEqual(450.92); - expect(volume).toEqual(47405890); + expect(volume).toEqual(474058.9); expect(count).toEqual(248346); expect(WAP).toEqual(451.3); } @@ -324,9 +324,9 @@ describe("IBApi Historical data Tests", () => { ib.connect().reqHistoricalTicks( refId, contract, - "20210101-16:00:00", + "20240102-16:00:00", null, - 1000, + 10, WhatToShow.TRADES, 0, true, diff --git a/src/tests/unit/api/order/cancelOrder.test.ts b/src/tests/unit/api/order/cancelOrder.test.ts index 88cc6b07..5e7a4353 100644 --- a/src/tests/unit/api/order/cancelOrder.test.ts +++ b/src/tests/unit/api/order/cancelOrder.test.ts @@ -8,12 +8,13 @@ import { IBApi, Order, OrderAction, + OrderStatus, OrderType, - Stock, TimeInForce, } from "../../../.."; import configuration from "../../../../common/configuration"; import logger from "../../../../common/logger"; +import { sample_etf } from "../../sample-data/contracts"; describe("CancelOrder", () => { jest.setTimeout(20 * 1000); @@ -41,48 +42,50 @@ describe("CancelOrder", () => { test("cancelOrder", (done) => { let refId: number; - const contract: Contract = new Stock("SPY"); + const contract: Contract = sample_etf; const order: Order = { orderType: OrderType.LMT, action: OrderAction.BUY, - lmtPrice: 1, - totalQuantity: 1, - // account: "DU123567", - tif: TimeInForce.GTC, + lmtPrice: 3, + totalQuantity: 3, + tif: TimeInForce.DAY, + outsideRth: false, transmit: true, }; - let order_found = false; + let cancelling = false; ib.once(EventName.nextValidId, (orderId: number) => { refId = orderId; ib.placeOrder(refId, contract, order); }) - .on(EventName.openOrder, (orderId, _contract, _order, _orderState) => { - expect(orderId).toEqual(refId); - if (!order_found && orderId === refId) { - order_found = true; - ib.cancelOrder(refId); - } - }) .on( EventName.orderStatus, ( orderId, - _status, + status, _filled, _remaining, _avgFillPrice, - _permId?, - _parentId?, - _lastFillPrice?, - _clientId?, - _whyHeld?, - _mktCapPrice?, + _permId, + _parentId, + _lastFillPrice, + _clientId, + _whyHeld, + _mktCapPrice, ) => { - expect(orderId).toEqual(refId); + if (orderId === refId) { + if ( + !cancelling && + [OrderStatus.PreSubmitted, OrderStatus.Submitted].includes( + status as OrderStatus, + ) + ) { + cancelling = true; + ib.cancelOrder(refId); + } + } }, ) - .on(EventName.openOrderEnd, () => {}) .on( EventName.error, ( @@ -100,8 +103,9 @@ describe("CancelOrder", () => { } else if ( code == ErrorCode.ORDER_CANCELLED && reqId == refId && - order_found + cancelling ) { + logger.info(msg); done(); } else { done(msg); diff --git a/src/tests/unit/api/order/placeConditionalOrder.test.ts b/src/tests/unit/api/order/placeConditionalOrder.test.ts index e5a70198..21f8b132 100644 --- a/src/tests/unit/api/order/placeConditionalOrder.test.ts +++ b/src/tests/unit/api/order/placeConditionalOrder.test.ts @@ -2,25 +2,65 @@ * This file implement test code for the placeOrder function . */ import { + ConjunctionConnection, Contract, ErrorCode, EventName, + ExecutionCondition, IBApi, + MarginCondition, Order, OrderAction, + OrderCondition, OrderType, + PercentChangeCondition, + PriceCondition, + TimeCondition, + TriggerMethod, + VolumeCondition, } from "../../../.."; import configuration from "../../../../common/configuration"; import logger from "../../../../common/logger"; -import { - sample_execution_condition, - sample_margin_condition, - sample_percent_condition, - sample_price_condition, - sample_time_condition, - sample_volume_condition, -} from "../../sample-data/conditions"; -import { sample_stock } from "../../sample-data/contracts"; +import { aapl_contract, sample_stock } from "../../sample-data/contracts"; + +const sample_price_condition: OrderCondition = new PriceCondition( + 29, + TriggerMethod.Default, + aapl_contract.conId, + aapl_contract.exchange, + true, + ConjunctionConnection.OR, +); +const sample_execution_condition: OrderCondition = new ExecutionCondition( + sample_stock.exchange, + sample_stock.secType, + sample_stock.symbol, + ConjunctionConnection.OR, +); +const sample_margin_condition: OrderCondition = new MarginCondition( + 10, + false, + ConjunctionConnection.OR, +); +const sample_percent_condition: OrderCondition = new PercentChangeCondition( + 0.1, + aapl_contract.conId, + aapl_contract.exchange, + true, + ConjunctionConnection.OR, +); +const sample_time_condition: OrderCondition = new TimeCondition( + "20250101-12:00:00", + true, + ConjunctionConnection.OR, +); +const sample_volume_condition: OrderCondition = new VolumeCondition( + 100, + aapl_contract.conId, + aapl_contract.exchange, + true, + ConjunctionConnection.OR, +); describe("Place Conditional Orders", () => { jest.setTimeout(20 * 1000); diff --git a/src/tests/unit/api/wsh-event-data.test.ts b/src/tests/unit/api/wsh-event-data.test.ts index d3d473c9..dbddda67 100644 --- a/src/tests/unit/api/wsh-event-data.test.ts +++ b/src/tests/unit/api/wsh-event-data.test.ts @@ -39,7 +39,8 @@ describe("IBApi Fundamental Data", () => { .on(EventName.error, (err, code, reqId) => { const msg = `[${reqId}] ${err.message} (#${code})`; if (code == ErrorCode.NEWS_FEED_NOT_ALLOWED) { - logger.error(msg); + // Ignore this error for tests + logger.warn(msg); done(); } else if (reqId == refId) { done(msg); @@ -68,7 +69,8 @@ describe("IBApi Fundamental Data", () => { .on(EventName.error, (err, code, reqId) => { const msg = `[${reqId}] ${err.message} (#${code})`; if (code == ErrorCode.NEWS_FEED_NOT_ALLOWED) { - logger.error(msg); + // Ignore this error for tests + logger.warn(msg); done(); } else if (reqId == refId) { done(msg); @@ -100,7 +102,8 @@ describe("IBApi Fundamental Data", () => { .on(EventName.error, (err, code, reqId) => { const msg = `[${reqId}] ${err.message} (#${code})`; if (code == ErrorCode.NEWS_FEED_NOT_ALLOWED) { - logger.error(msg); + // Ignore this error for tests + logger.warn(msg); done(); } else if (reqId == refId) { done(msg); diff --git a/src/tests/unit/sample-data/conditions.ts b/src/tests/unit/sample-data/conditions.ts deleted file mode 100644 index 504b7589..00000000 --- a/src/tests/unit/sample-data/conditions.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { - ConjunctionConnection, - ExecutionCondition, - MarginCondition, - OrderCondition, - PercentChangeCondition, - PriceCondition, - TimeCondition, - TriggerMethod, - VolumeCondition, -} from "../../.."; -import { aapl_contract, sample_stock } from "./contracts"; - -export const sample_price_condition: OrderCondition = new PriceCondition( - 29, - TriggerMethod.Default, - aapl_contract.conId, - aapl_contract.exchange, - true, - ConjunctionConnection.OR, -); -export const sample_execution_condition: OrderCondition = - new ExecutionCondition( - sample_stock.exchange, - sample_stock.secType, - sample_stock.symbol, - ConjunctionConnection.OR, - ); -export const sample_margin_condition: OrderCondition = new MarginCondition( - 10, - false, - ConjunctionConnection.OR, -); -export const sample_percent_condition: OrderCondition = - new PercentChangeCondition( - 0.1, - aapl_contract.conId, - aapl_contract.exchange, - true, - ConjunctionConnection.OR, - ); -export const sample_time_condition: OrderCondition = new TimeCondition( - "20250101-12:00:00", - true, - ConjunctionConnection.OR, -); -export const sample_volume_condition: OrderCondition = new VolumeCondition( - 100, - aapl_contract.conId, - aapl_contract.exchange, - true, - ConjunctionConnection.OR, -); diff --git a/src/tests/unit/sample-data/contracts.ts b/src/tests/unit/sample-data/contracts.ts index 72e3ccd3..5210f614 100644 --- a/src/tests/unit/sample-data/contracts.ts +++ b/src/tests/unit/sample-data/contracts.ts @@ -14,14 +14,13 @@ import { import Crypto from "../../../api/contract/crypto"; export const sample_stock: Contract = new Stock("AAPL"); -export const aapl_contract: Contract = { - conId: 265598, - secType: SecType.STK, - symbol: "AAPL", - exchange: "SMART", - currency: "USD", -}; export const sample_etf: Contract = new Stock("SPY"); +export const sample_bond: Contract = new Bond("US279158AE95"); +export const sample_index: Contract = new Index("ES", "USD"); +export const sample_dax_index: Contract = new Index("DAX", "EUR", "EUREX"); +export const sample_crypto: Contract = new Crypto("ETH"); + +// This one will need to be updated sometimes export const sample_future: Contract = new Future( "ES", "ESH4", @@ -29,13 +28,22 @@ export const sample_future: Contract = new Future( "CME", 50, ); + +// This one may need to be updated from times to times export const sample_option: Contract = new Option( - "AAPL", - "20251219", - 200, - OptionType.Put, + "SPY", + "20260116", + 440, + OptionType.Call, ); -export const sample_bond: Contract = new Bond("US279158AE95"); -export const sample_index: Contract = new Index("ES", "USD"); -export const sample_dax_index: Contract = new Index("DAX", "EUR", "EUREX"); -export const sample_crypto: Contract = new Crypto("ETH"); + +/* + Contracts with conId for tests needing IB's conID +*/ +export const aapl_contract: Contract = { + conId: 265598, + secType: SecType.STK, + symbol: "AAPL", + exchange: "SMART", + currency: "USD", +};