From a5ea7443e6eea13768d00740547fe5da563bc20f Mon Sep 17 00:00:00 2001 From: Ronan-Yann Lorin Date: Mon, 18 Sep 2023 07:53:09 +0200 Subject: [PATCH] matching symbols test added --- src/api-next/api-next.ts | 39 ++++++-- src/api-next/common/item-list-update.ts | 3 - src/core/api-next/item-list-update.ts | 1 - src/core/api-next/subscription.ts | 21 +---- src/core/io/socket.ts | 2 +- .../unit/api-next/get-account-summary.test.ts | 75 +++++++++------- src/tests/unit/api/matching-symbols.test.ts | 89 +++++++++++++++++++ .../unit/api/order/reqAllOpenOrders.test.ts | 40 +++++---- 8 files changed, 189 insertions(+), 81 deletions(-) create mode 100644 src/tests/unit/api/matching-symbols.test.ts diff --git a/src/api-next/api-next.ts b/src/api-next/api-next.ts index bda26c31..eed11d03 100644 --- a/src/api-next/api-next.ts +++ b/src/api-next/api-next.ts @@ -437,7 +437,9 @@ export class IBApiNext { ], ]); - if (hasChanged) { + if (!subscription.endEventReceived) { + subscription.lastAllValue = cached; + } else if (hasChanged) { subscription.next({ all: cached, changed: accountSummaryUpdate, @@ -450,6 +452,25 @@ export class IBApiNext { } }; + /** accountSummaryEnd event handler */ + private readonly onAccountSummaryEnd = ( + subscriptions: Map>, + reqId: number, + ): void => { + // get the subscription + const subscription = subscriptions.get(reqId); + if (!subscription) { + return; + } + + // get latest value on cache + const cached = subscription.lastAllValue ?? new MutableAccountSummaries(); + + // sent data to subscribers + subscription.endEventReceived = true; + subscription.next({ all: cached }); + }; + /** * Create subscription to receive the account summaries of all linked accounts as presented in the TWS' Account Summary tab. * @@ -507,7 +528,10 @@ export class IBApiNext { (reqId) => { this.api.cancelAccountSummary(reqId); }, - [[EventName.accountSummary, this.onAccountSummary]], + [ + [EventName.accountSummary, this.onAccountSummary], + [EventName.accountSummaryEnd, this.onAccountSummaryEnd], + ], `${group}:${tags}`, ); } @@ -713,7 +737,12 @@ export class IBApiNext { accountName: string, ): void => { this.logger.debug(LOG_TAG, `onAccountDownloadEnd(${accountName})`); - // TODO finish implementation + // notify all subscribers + subscriptions.forEach((subscription) => { + const all: AccountUpdate = subscription.lastAllValue ?? {}; + subscription.endEventReceived = true; + subscription.next({ all }); + }); }; /** @@ -2426,10 +2455,6 @@ export class IBApiNext { all: lastAllValue, }; subscription.endEventReceived = true; - - // console.log("onScannerDataEnd", updated); - - // subscription.next(updated); subscription.next(updated); }; diff --git a/src/api-next/common/item-list-update.ts b/src/api-next/common/item-list-update.ts index bb2723a3..097338ae 100644 --- a/src/api-next/common/item-list-update.ts +++ b/src/api-next/common/item-list-update.ts @@ -5,9 +5,6 @@ export interface ItemListUpdate { /** All items with its latest values, as received by TWS. */ readonly all: T; - /** all value is set with all items (ie not still being built = End message received from TWS) */ - readonly allset?: boolean; - /** Items that have been added since last [[IBApiNextUpdate]]. */ readonly added?: T; diff --git a/src/core/api-next/item-list-update.ts b/src/core/api-next/item-list-update.ts index a702dfbc..412b5752 100644 --- a/src/core/api-next/item-list-update.ts +++ b/src/core/api-next/item-list-update.ts @@ -12,6 +12,5 @@ export class IBApiNextItemListUpdate implements ItemListUpdate { public readonly added?: T, public readonly changed?: T, public readonly removed?: T, - public readonly allset?: boolean, ) {} } diff --git a/src/core/api-next/subscription.ts b/src/core/api-next/subscription.ts index 7154bd50..cd0c0d8c 100644 --- a/src/core/api-next/subscription.ts +++ b/src/core/api-next/subscription.ts @@ -4,11 +4,6 @@ import { IBApiNext, IBApiNextError, ItemListUpdate } from "../../api-next"; import { ConnectionState } from "../../api-next/common/connection-state"; import { IBApiNextItemListUpdate } from "./item-list-update"; -export type IBApiNextItemListUpdateMinimal = Omit< - IBApiNextItemListUpdate, - "added" | "changed" | "removed" ->; - /** * @internal * @@ -56,8 +51,8 @@ export class IBApiNextSubscription { /** The [[Subscription]] on the connection state. */ private connectionState$?: Subscription; - /** true when the end-event on an enumeration request has been received, false otherwise. */ - private _endEventReceived = false; + /** @internal True when the end-event on an enumeration request has been received, false otherwise. */ + public endEventReceived = false; /** Get the last 'all' value as send to subscribers. */ get lastAllValue(): T | undefined { @@ -69,16 +64,6 @@ export class IBApiNextSubscription { this._lastAllValue = value; } - /** @internal Get the end event flag. For internal use only. */ - get endEventReceived(): boolean { - return this._endEventReceived; - } - - /** @internal Set the end event flag seen. For internal use only. */ - set endEventReceived(value: boolean) { - this._endEventReceived = value; - } - /** * Send the next value to subscribers. * @@ -102,7 +87,7 @@ export class IBApiNextSubscription { */ error(error: IBApiNextError): void { delete this._lastAllValue; - this._endEventReceived = false; + this.endEventReceived = false; this.hasError = true; this.subject.error(error); this.cancelTwsSubscription(); diff --git a/src/core/io/socket.ts b/src/core/io/socket.ts index 9753a7df..316cd1f5 100644 --- a/src/core/io/socket.ts +++ b/src/core/io/socket.ts @@ -32,7 +32,7 @@ const EOL = "\0"; * @hidden * add a delay after connect before sending commands */ -const CONNECT_DELAY = 600; +// const CONNECT_DELAY = 600; /** * @internal diff --git a/src/tests/unit/api-next/get-account-summary.test.ts b/src/tests/unit/api-next/get-account-summary.test.ts index 32778a5e..44e1d093 100644 --- a/src/tests/unit/api-next/get-account-summary.test.ts +++ b/src/tests/unit/api-next/get-account-summary.test.ts @@ -3,12 +3,12 @@ */ import { take } from "rxjs/operators"; -import { IBApi, IBApiNext, IBApiNextError, EventName } from "../../.."; +import { EventName, IBApi, IBApiNext, IBApiNextError } from "../../.."; describe("RxJS Wrapper: getAccountSummary()", () => { test("Error Event", (done) => { const apiNext = new IBApiNext(); - const api = ((apiNext as unknown) as Record).api as IBApi; + const api = (apiNext as unknown as Record).api as IBApi; // emit a error event and verify RxJS result @@ -32,7 +32,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { test("Update multicast", (done) => { const apiNext = new IBApiNext(); - const api = ((apiNext as unknown) as Record).api as IBApi; + const api = (apiNext as unknown as Record).api as IBApi; // testing values @@ -55,7 +55,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { next: (update) => { expect( update.all.get(accountId1)?.get("NetLiquidation")?.get(currency) - ?.value + ?.value, ).toEqual(testValueReqId1); receivedNetLiquidation++; if (receivedNetLiquidation == 2 && receivedTotalCashValue == 2) { @@ -74,7 +74,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { next: (update) => { expect( update.all.get(accountId1)?.get("NetLiquidation")?.get(currency) - ?.value + ?.value, ).toEqual(testValueReqId1); receivedNetLiquidation++; if (receivedNetLiquidation == 2 && receivedTotalCashValue == 2) { @@ -95,7 +95,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { next: (update) => { expect( update.all.get(accountId2)?.get("TotalCashValue")?.get(currency) - ?.value + ?.value, ).toEqual(testValueReqId1); receivedTotalCashValue++; if (receivedNetLiquidation == 2 && receivedTotalCashValue == 2) { @@ -114,7 +114,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { next: (update) => { expect( update.all.get(accountId2)?.get("TotalCashValue")?.get(currency) - ?.value + ?.value, ).toEqual(testValueReqId1); receivedTotalCashValue++; if (receivedNetLiquidation == 2 && receivedTotalCashValue == 2) { @@ -132,8 +132,9 @@ describe("RxJS Wrapper: getAccountSummary()", () => { accountId1, "NetLiquidation", testValueReqId1, - currency + currency, ); + api.emit(EventName.accountSummaryEnd, 1); api.emit( EventName.accountSummary, @@ -141,13 +142,14 @@ describe("RxJS Wrapper: getAccountSummary()", () => { accountId2, "TotalCashValue", testValueReqId1, - currency + currency, ); + api.emit(EventName.accountSummaryEnd, 2); }); test("Aggregate into all", (done) => { const apiNext = new IBApiNext(); - const api = ((apiNext as unknown) as Record).api as IBApi; + const api = (apiNext as unknown as Record).api as IBApi; // testing values @@ -175,38 +177,44 @@ describe("RxJS Wrapper: getAccountSummary()", () => { update.all.forEach((tagValues) => tagValues.forEach((currencyValues) => { totalValuesCount += currencyValues.size; - }) + }), ); switch (totalValuesCount) { case 6: expect( - update.all.get(accountId2)?.get(tagName2)?.get(currency2)?.value + update.all.get(accountId2)?.get(tagName2)?.get(currency2) + ?.value, ).toEqual(currency2Value); // no break by intention case 5: expect( - update.all.get(accountId2)?.get(tagName2)?.get(currency1)?.value + update.all.get(accountId2)?.get(tagName2)?.get(currency1) + ?.value, ).toEqual(currency1Value); // no break by intention case 4: expect( - update.all.get(accountId2)?.get(tagName1)?.get(currency2)?.value + update.all.get(accountId2)?.get(tagName1)?.get(currency2) + ?.value, ).toEqual(currency2Value); // no break by intention case 3: expect( - update.all.get(accountId2)?.get(tagName1)?.get(currency1)?.value + update.all.get(accountId2)?.get(tagName1)?.get(currency1) + ?.value, ).toEqual(currency1Value); // no break by intention case 2: expect( - update.all.get(accountId1)?.get(tagName1)?.get(currency2)?.value + update.all.get(accountId1)?.get(tagName1)?.get(currency2) + ?.value, ).toEqual(currency2Value); // no break by intention case 1: expect( - update.all.get(accountId1)?.get(tagName1)?.get(currency1)?.value + update.all.get(accountId1)?.get(tagName1)?.get(currency1) + ?.value, ).toEqual(currency1Value); break; } @@ -227,8 +235,9 @@ describe("RxJS Wrapper: getAccountSummary()", () => { accountId1, tagName1, currency1Value, - currency1 + currency1, ); + api.emit(EventName.accountSummaryEnd, 1); api.emit( EventName.accountSummary, @@ -236,7 +245,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { accountId1, tagName1, currency2Value, - currency2 + currency2, ); api.emit( @@ -245,7 +254,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { accountId2, tagName1, currency1Value, - currency1 + currency1, ); api.emit( @@ -254,7 +263,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { accountId2, tagName1, currency2Value, - currency2 + currency2, ); api.emit( @@ -263,7 +272,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { accountId2, tagName2, currency1Value, - currency1 + currency1, ); api.emit( @@ -272,13 +281,13 @@ describe("RxJS Wrapper: getAccountSummary()", () => { accountId2, tagName2, currency2Value, - currency2 + currency2, ); }); test("Detected changes", (done) => { const apiNext = new IBApiNext(); - const api = ((apiNext as unknown) as Record).api as IBApi; + const api = (apiNext as unknown as Record).api as IBApi; // testing values @@ -297,11 +306,11 @@ describe("RxJS Wrapper: getAccountSummary()", () => { next: (update) => { if (update.added?.size) { expect( - update.added.get(accountId)?.get(tagName)?.get(currency)?.value + update.added.get(accountId)?.get(tagName)?.get(currency)?.value, ).toEqual(testValue1); } else if (update.changed?.size) { expect( - update.changed.get(accountId)?.get(tagName)?.get(currency)?.value + update.changed.get(accountId)?.get(tagName)?.get(currency)?.value, ).toEqual(testValue2); done(); } else { @@ -319,8 +328,9 @@ describe("RxJS Wrapper: getAccountSummary()", () => { accountId, tagName, testValue1, - currency + currency, ); + api.emit(EventName.accountSummaryEnd, 1); api.emit( EventName.accountSummary, @@ -328,7 +338,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { accountId, tagName, testValue2, - currency + currency, ); }); @@ -336,7 +346,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { // create IBApiNext and reqId counter const apiNext = new IBApiNext(); - const api = ((apiNext as unknown) as Record).api as IBApi; + const api = (apiNext as unknown as Record).api as IBApi; // testing values @@ -353,7 +363,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { .subscribe({ next: (update) => { expect( - update.added.get(accountId)?.get(tagName)?.get(currency)?.value + update.added.get(accountId)?.get(tagName)?.get(currency)?.value, ).toEqual(testValue); apiNext @@ -364,7 +374,7 @@ describe("RxJS Wrapper: getAccountSummary()", () => { next: (update) => { expect( update.added.get(accountId)?.get(tagName)?.get(currency) - ?.value + ?.value, ).toEqual(testValue); done(); }, @@ -384,7 +394,8 @@ describe("RxJS Wrapper: getAccountSummary()", () => { accountId, tagName, testValue, - currency + currency, ); + api.emit(EventName.accountSummaryEnd, 1); }); }); diff --git a/src/tests/unit/api/matching-symbols.test.ts b/src/tests/unit/api/matching-symbols.test.ts new file mode 100644 index 00000000..caf492e2 --- /dev/null +++ b/src/tests/unit/api/matching-symbols.test.ts @@ -0,0 +1,89 @@ +import { IBApi } from "../../../api/api"; +import ContractDescription from "../../../api/contract/contractDescription"; +import { EventName } from "../../../api/data/enum/event-name"; +import configuration from "../../../common/configuration"; + +describe("IBApi reqMatchingSymbols Tests", () => { + jest.setTimeout(5000); + + let ib: IBApi; + const clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client + + beforeEach(() => { + ib = new IBApi({ + host: configuration.ib_host, + port: configuration.ib_port, + clientId, + }); + }); + + afterEach(() => { + if (ib) { + ib.disconnect(); + ib = undefined; + } + }); + + test("SPY", (done) => { + let tickerId: number; + ib.once(EventName.nextValidId, (reqId) => { + tickerId = reqId; + ib.reqMatchingSymbols(reqId, "SPY"); + }) + .on(EventName.disconnected, () => { + done(); + }) + .on( + EventName.symbolSamples, + (reqId, contractDescriptions: ContractDescription[]) => { + expect(reqId).toEqual(tickerId); + expect(contractDescriptions[0].contract.symbol).toEqual("SPY"); + ib.disconnect(); + }, + ); + + ib.connect(); + }); + + test("META", (done) => { + let tickerId: number; + ib.once(EventName.nextValidId, (reqId) => { + tickerId = reqId; + ib.reqMatchingSymbols(reqId, "META"); + }) + .on(EventName.disconnected, () => { + done(); + }) + .on( + EventName.symbolSamples, + (reqId, contractDescriptions: ContractDescription[]) => { + expect(reqId).toEqual(tickerId); + expect(contractDescriptions[0].contract.symbol).toEqual("META"); + ib.disconnect(); + }, + ); + + ib.connect(); + }); + + test("AMC", (done) => { + let tickerId: number; + ib.once(EventName.nextValidId, (reqId) => { + tickerId = reqId; + ib.reqMatchingSymbols(reqId, "AMC"); + }) + .on(EventName.disconnected, () => { + done(); + }) + .on( + EventName.symbolSamples, + (reqId, contractDescriptions: ContractDescription[]) => { + expect(reqId).toEqual(tickerId); + expect(contractDescriptions[0].contract.symbol).toEqual("AMC"); + ib.disconnect(); + }, + ); + + ib.connect(); + }); +}); diff --git a/src/tests/unit/api/order/reqAllOpenOrders.test.ts b/src/tests/unit/api/order/reqAllOpenOrders.test.ts index 16242290..f8c0a173 100644 --- a/src/tests/unit/api/order/reqAllOpenOrders.test.ts +++ b/src/tests/unit/api/order/reqAllOpenOrders.test.ts @@ -10,14 +10,28 @@ const TEST_SERVER_POST = configuration.ib_port; describe("RequestAllOpenOrders", () => { jest.setTimeout(10000); - let _clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client - it("Test reqAllOpenOrders", (done) => { - const ib = new IBApi({ - host: TEST_SERVER_HOST, - port: TEST_SERVER_POST, + let ib: IBApi; + let clientId = Math.floor(Math.random() * 32766) + 1; // ensure unique client + + beforeEach(() => { + ib = new IBApi({ + host: configuration.ib_host, + port: configuration.ib_port, + clientId, }); + // logger.info("IBApi created"); + }); + + afterEach(() => { + if (ib) { + ib.disconnect(); + ib = undefined; + } + // logger.info("IBApi disconnected"); + }); + it("Test reqAllOpenOrders", (done) => { let received = false; ib.on(EventName.openOrder, (orderId, contract, order, orderState) => { @@ -26,28 +40,16 @@ describe("RequestAllOpenOrders", () => { // expect(orderId).toBeTruthy(); We sometimes get zeros }) .on(EventName.openOrderEnd, () => { - // logger.info("openOrderEnd message received"); - received = true; - // done ib.disconnect(); - }) - .on(EventName.connected, () => { - // logger.info("connected"); - ib.reqIds(); - }) - .on(EventName.disconnected, () => { - if (received) done(); - else done("We didn't received acknowledge"); + done(); }) .on(EventName.error, (err: Error, code: ErrorCode, id: number) => { done(`${err.message} - code: ${code} - id: ${id}`); }) .once(EventName.nextValidId, (orderId: number) => { - // logger.info(`nextValidId: ${orderId}, requesting orders`); - ib.reqAllOpenOrders(); }); - ib.connect(_clientId++); + ib.connect(); }); });