diff --git a/README.md b/README.md index 6dd246f..7f08a6e 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ stream.latestTradeDetail$.subscribe((v) => {}) - [ ] Bulk order - [x] One-Click Close All Positions - [ ] Cancel an Order - - [ ] Cancel a Batch of Orders + - [x] Cancel a Batch of Orders - [ ] Cancel All Orders - [ ] Query all current pending orders - [ ] Query Order diff --git a/src/bingx-client/services/trade-cancel-batch-orders.service.spec.ts b/src/bingx-client/services/trade-cancel-batch-orders.service.spec.ts new file mode 100644 index 0000000..bad63db --- /dev/null +++ b/src/bingx-client/services/trade-cancel-batch-orders.service.spec.ts @@ -0,0 +1,77 @@ +import { TradeService } from 'bingx-api/bingx-client/services/trade.service'; +import { AccountInterface } from 'bingx-api/bingx/account/account.interface'; +import { RequestExecutorInterface } from 'bingx-api/bingx/request-executor/request-executor.interface'; +import { EndpointInterface } from 'bingx-api/bingx/endpoints/endpoint.interface'; +import { BingxCancelBatchOrdersEndpoint } from 'bingx-api/bingx/endpoints/bingx-cancel-batch-orders-endpoint'; + +describe('trade cancel batch orders service', () => { + let account: AccountInterface; + let capturedEndpoints: EndpointInterface[]; + let requestExecutor: RequestExecutorInterface; + let executeSpy: jest.SpyInstance; + let nowSpy: jest.SpyInstance; + + beforeEach(() => { + account = { + getApiKey: jest.fn(() => 'api-key'), + sign: jest.fn(() => ({ + toString: () => 'signature', + secretKey: () => 'secret-key', + })), + }; + + capturedEndpoints = []; + requestExecutor = { + execute(endpoint: EndpointInterface): Promise { + capturedEndpoints.push(endpoint as EndpointInterface); + return Promise.resolve(endpoint as unknown as T); + }, + }; + + executeSpy = jest.spyOn(requestExecutor, 'execute'); + nowSpy = jest.spyOn(Date, 'now').mockReturnValue(1770000000123); + }); + + afterEach(() => { + nowSpy.mockRestore(); + }); + + it('dispatches the signed cancel batch orders endpoint', async () => { + const service = new TradeService(requestExecutor); + + const endpoint = (await service.cancelBatchOrders( + { + symbol: 'BTC-USDT', + orderIdList: ['1735924831603391122', '1735924833239172233'], + clientOrderIdList: ['abc1234567', 'abc2345678'], + recvWindow: 5000, + }, + account, + )) as unknown as BingxCancelBatchOrdersEndpoint; + + expect(executeSpy).toHaveBeenCalledTimes(1); + expect(capturedEndpoints[0]).toBe(endpoint); + expect(endpoint).toBeInstanceOf(BingxCancelBatchOrdersEndpoint); + expect(endpoint.method()).toBe('delete'); + expect(endpoint.path()).toBe('/openApi/swap/v2/trade/batchOrders'); + expect(endpoint.parameters().asRecord()).toEqual({ + symbol: 'BTC-USDT', + orderIdList: '[1735924831603391122,1735924833239172233]', + clientOrderIdList: '["abc1234567","abc2345678"]', + recvWindow: '5000', + timestamp: '1770000000123', + }); + }); + + it('omits optional order lists when only the required symbol is provided', () => { + const endpoint = new BingxCancelBatchOrdersEndpoint( + { symbol: 'BTC-USDT' }, + account, + ); + + expect(endpoint.parameters().asRecord()).toEqual({ + symbol: 'BTC-USDT', + timestamp: '1770000000123', + }); + }); +}); diff --git a/src/bingx-client/services/trade.service.ts b/src/bingx-client/services/trade.service.ts index 65fa5bd..cb00c78 100644 --- a/src/bingx-client/services/trade.service.ts +++ b/src/bingx-client/services/trade.service.ts @@ -12,6 +12,10 @@ import { BingxSwitchLeverageEndpoint } from 'bingx-api/bingx/endpoints/bingx-swi import { OrderPositionSideEnum } from 'bingx-api/bingx'; import { BingxUserHistoryOrdersEndpoint } from 'bingx-api/bingx/endpoints/bingx-user-history-orders-endpoint'; import { BingxCancelOrderEndpoint } from 'bingx-api/bingx/endpoints/bingx-cancel-order-endpoint'; +import { + BingxCancelBatchOrdersEndpoint, + BingxCancelBatchOrdersOptions, +} from 'bingx-api/bingx/endpoints/bingx-cancel-batch-orders-endpoint'; export class TradeService { constructor(private readonly requestExecutor: RequestExecutorInterface) {} @@ -44,6 +48,15 @@ export class TradeService { ); } + public async cancelBatchOrders( + options: BingxCancelBatchOrdersOptions, + account: AccountInterface, + ) { + return this.requestExecutor.execute( + new BingxCancelBatchOrdersEndpoint(options, account), + ); + } + public async getUserHistoryOrders( symbol: string, limit: number, diff --git a/src/bingx/endpoints/bingx-cancel-batch-orders-endpoint.ts b/src/bingx/endpoints/bingx-cancel-batch-orders-endpoint.ts new file mode 100644 index 0000000..22c2ed6 --- /dev/null +++ b/src/bingx/endpoints/bingx-cancel-batch-orders-endpoint.ts @@ -0,0 +1,76 @@ +import { + AccountInterface, + BingxResponse, + DefaultSignatureParameters, + EndpointInterface, + SignatureParametersInterface, +} from 'bingx-api/bingx'; +import { Endpoint } from 'bingx-api/bingx/endpoints/endpoint'; +import { BingxUserHistoryOrdersResponse } from 'bingx-api/bingx/endpoints/bingx-user-history-orders-response'; + +export interface BingxCancelBatchOrdersOptions { + symbol: string; + orderIdList?: Array; + clientOrderIdList?: string[]; + recvWindow?: string | number; +} + +export interface BingxCancelBatchOrdersFailedOrder { + orderId?: string | number; + clientOrderId?: string; + errorCode: number; + errorMessage: string; +} + +export interface BingxCancelBatchOrdersData { + success: BingxUserHistoryOrdersResponse['orders']; + failed: BingxCancelBatchOrdersFailedOrder[] | null; +} + +export class BingxCancelBatchOrdersEndpoint + extends Endpoint> + implements EndpointInterface> +{ + constructor( + private readonly options: BingxCancelBatchOrdersOptions, + account: AccountInterface, + ) { + super(account); + } + + method(): 'get' | 'post' | 'put' | 'patch' | 'delete' { + return 'delete'; + } + + parameters(): SignatureParametersInterface { + const parameters: Record = { + symbol: this.options.symbol, + }; + + if (this.options.orderIdList !== undefined) { + parameters.orderIdList = this.serializeOrderIds(this.options.orderIdList); + } + + if (this.options.clientOrderIdList !== undefined) { + parameters.clientOrderIdList = JSON.stringify( + this.options.clientOrderIdList, + ); + } + + if (this.options.recvWindow !== undefined) { + parameters.recvWindow = this.options.recvWindow.toString(); + } + + return new DefaultSignatureParameters(parameters); + } + + path(): string { + return '/openApi/swap/v2/trade/batchOrders'; + } + + private serializeOrderIds(orderIds: Array): string { + return `[${orderIds.map((orderId) => orderId.toString()).join(',')}]`; + } + + readonly t!: BingxResponse; +} diff --git a/src/bingx/endpoints/index.ts b/src/bingx/endpoints/index.ts index 6866f8b..89a75ba 100644 --- a/src/bingx/endpoints/index.ts +++ b/src/bingx/endpoints/index.ts @@ -1,4 +1,5 @@ export * from './bingx-cancel-all-orders-endpoint'; +export * from './bingx-cancel-batch-orders-endpoint'; export * from './bingx-close-all-positions-endpoint'; export * from './bingx-generate-listen-key-endpoint'; export * from './bingx-generate-listen-key-response';