diff --git a/README.md b/README.md index 6dd246f..3ff2909 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ stream.latestTradeDetail$.subscribe((v) => {}) * Trade Interface - [ ] Trade order test - [x] Trade order - - [ ] Bulk order + - [x] Bulk order - [x] One-Click Close All Positions - [ ] Cancel an Order - [ ] Cancel a Batch of Orders diff --git a/src/bingx-client/services/trade.service.spec.ts b/src/bingx-client/services/trade.service.spec.ts new file mode 100644 index 0000000..45ca668 --- /dev/null +++ b/src/bingx-client/services/trade.service.spec.ts @@ -0,0 +1,79 @@ +import { AccountInterface } from 'bingx-api/bingx/account/account.interface'; +import { EndpointInterface } from 'bingx-api/bingx/endpoints/endpoint.interface'; +import { BingxBulkOrderEndpoint } from 'bingx-api/bingx/endpoints/bingx-bulk-order-endpoint'; +import { OrderPositionSideEnum } from 'bingx-api/bingx/enums/order-position-side.enum'; +import { OrderSideEnum } from 'bingx-api/bingx/enums/order-side.enum'; +import { OrderTypeEnum } from 'bingx-api/bingx/enums/order-type.enum'; +import { BingxCreateTradeOrderInterface } from 'bingx-api/bingx/interfaces/trade-order.interface'; +import { RequestExecutorInterface } from 'bingx-api/bingx/request-executor/request-executor.interface'; +import { TradeService } from 'bingx-api/bingx-client/services/trade.service'; + +class TestRequestExecutor implements RequestExecutorInterface { + public endpoint?: EndpointInterface; + + execute(endpoint: EndpointInterface): Promise { + this.endpoint = endpoint; + return Promise.resolve({} as T); + } +} + +const account: AccountInterface = { + getApiKey: () => 'api-key', + sign: () => ({ + toString: () => 'signature', + secretKey: () => 'secret-key', + }), +}; + +describe('TradeService', () => { + describe('bulkOrder', () => { + it('dispatches the bulk order endpoint', async () => { + const executor = new TestRequestExecutor(); + const service = new TradeService(executor); + const orders: BingxCreateTradeOrderInterface[] = [ + { + symbol: 'ETH-USDT', + type: OrderTypeEnum.MARKET, + side: OrderSideEnum.BUY, + positionSide: OrderPositionSideEnum.LONG, + quantity: '1', + }, + { + symbol: 'BTC-USDT', + type: OrderTypeEnum.MARKET, + side: OrderSideEnum.BUY, + positionSide: OrderPositionSideEnum.LONG, + quantity: '0.001', + }, + ]; + + await service.bulkOrder(orders, account, 5000); + + const endpoint = executor.endpoint as BingxBulkOrderEndpoint; + const parameters = endpoint.parameters().asRecord(); + + expect(endpoint).toBeInstanceOf(BingxBulkOrderEndpoint); + expect(endpoint.method()).toBe('post'); + expect(endpoint.path()).toBe('/openApi/swap/v2/trade/batchOrders'); + expect(JSON.parse(parameters.batchOrders)).toEqual(orders); + expect(parameters.recvWindow).toBe('5000'); + expect(parameters.timestamp).toBeDefined(); + }); + + it('omits recvWindow when it is not provided', async () => { + const endpoint = new BingxBulkOrderEndpoint( + [ + { + symbol: 'ETH-USDT', + type: OrderTypeEnum.MARKET, + side: OrderSideEnum.BUY, + quantity: '1', + }, + ], + account, + ); + + expect(endpoint.parameters().asRecord().recvWindow).toBeUndefined(); + }); + }); +}); diff --git a/src/bingx-client/services/trade.service.ts b/src/bingx-client/services/trade.service.ts index 65fa5bd..7768baf 100644 --- a/src/bingx-client/services/trade.service.ts +++ b/src/bingx-client/services/trade.service.ts @@ -12,6 +12,7 @@ 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 { BingxBulkOrderEndpoint } from 'bingx-api/bingx/endpoints/bingx-bulk-order-endpoint'; export class TradeService { constructor(private readonly requestExecutor: RequestExecutorInterface) {} @@ -34,6 +35,16 @@ export class TradeService { ); } + public async bulkOrder( + orders: BingxCreateTradeOrderInterface[], + account: AccountInterface, + recvWindow?: number, + ) { + return this.requestExecutor.execute( + new BingxBulkOrderEndpoint(orders, account, recvWindow), + ); + } + public async cancelOrder( orderId: string, symbol: string, diff --git a/src/bingx/endpoints/bingx-bulk-order-endpoint.ts b/src/bingx/endpoints/bingx-bulk-order-endpoint.ts new file mode 100644 index 0000000..890f0fc --- /dev/null +++ b/src/bingx/endpoints/bingx-bulk-order-endpoint.ts @@ -0,0 +1,54 @@ +import { AccountInterface } from 'bingx-api/bingx/account/account.interface'; +import { DefaultSignatureParameters } from 'bingx-api/bingx/account/default-signature-parameters'; +import { SignatureParametersInterface } from 'bingx-api/bingx/account/signature-parameters.interface'; +import { BingxResponse } from 'bingx-api/bingx/interfaces/bingx-response'; +import { BingxCreateTradeOrderInterface } from 'bingx-api/bingx/interfaces/trade-order.interface'; +import { OrderPositionSideEnum } from 'bingx-api/bingx/enums/order-position-side.enum'; +import { OrderSideEnum } from 'bingx-api/bingx/enums/order-side.enum'; +import { OrderTypeEnum } from 'bingx-api/bingx/enums/order-type.enum'; +import { Endpoint } from 'bingx-api/bingx/endpoints/endpoint'; +import { EndpointInterface } from 'bingx-api/bingx/endpoints/endpoint.interface'; + +export interface BingxBulkOrderResponseInterface { + orders: Array<{ + symbol: string; + side: OrderSideEnum; + type: OrderTypeEnum; + positionSide: OrderPositionSideEnum; + orderId: number; + clientOrderId: string; + workingType: string; + }>; +} + +export class BingxBulkOrderEndpoint + extends Endpoint + implements EndpointInterface> +{ + constructor( + private readonly orders: BingxCreateTradeOrderInterface[], + account: AccountInterface, + private readonly recvWindow?: number, + ) { + super(account); + } + + readonly t!: BingxResponse; + + method(): 'get' | 'post' | 'put' | 'patch' | 'delete' { + return 'post'; + } + + parameters(): SignatureParametersInterface { + return new DefaultSignatureParameters({ + batchOrders: JSON.stringify(this.orders), + ...(this.recvWindow === undefined + ? {} + : { recvWindow: this.recvWindow.toString(10) }), + }); + } + + path(): string { + return '/openApi/swap/v2/trade/batchOrders'; + } +} diff --git a/src/bingx/endpoints/index.ts b/src/bingx/endpoints/index.ts index 6866f8b..76771a3 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-bulk-order-endpoint'; export * from './bingx-close-all-positions-endpoint'; export * from './bingx-generate-listen-key-endpoint'; export * from './bingx-generate-listen-key-response'; diff --git a/src/bingx/interfaces/websocket-event.ts b/src/bingx/interfaces/websocket-event.ts index a414299..3be37b3 100644 --- a/src/bingx/interfaces/websocket-event.ts +++ b/src/bingx/interfaces/websocket-event.ts @@ -1,4 +1,3 @@ -import { OrderTypeEnum } from 'bingx-api/bingx/enums/order-type.enum'; import { OrderSideEnum } from 'bingx-api/bingx/enums/order-side.enum'; import { OrderStatusEnum } from 'bingx-api/bingx/enums/order-status.enum'; import { OrderPositionSideEnum } from 'bingx-api/bingx/enums/order-position-side.enum';