diff --git a/src/spot-client.ts b/src/spot-client.ts index 3e06632..a2b7417 100644 --- a/src/spot-client.ts +++ b/src/spot-client.ts @@ -9,6 +9,15 @@ import { SymbolRules, NewSpotSubTransfer, NewSpotWithdraw, + CancelSpotOrderV2, + BatchCancelSpotOrderV2, + SpotOrderResult, + NewSpotPlanOrder, + ModifySpotPlanOrder, + CancelSpotPlanOrderParams, + GetSpotPlanOrdersParams, + SpotPlanOrder, + GetHistoricPlanOrdersParams, } from './types'; import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; @@ -246,7 +255,7 @@ export class SpotClient extends BaseRestClient { */ /** Place order */ - submitOrder(params: NewSpotOrder): Promise> { + submitOrder(params: NewSpotOrder): Promise> { return this.postPrivate('/api/spot/v1/trade/orders', params); } @@ -269,6 +278,20 @@ export class SpotClient extends BaseRestClient { }); } + /** Cancel order (v2 endpoint - supports orderId or clientOid) */ + cancelOrderV2(params?: CancelSpotOrderV2): Promise> { + return this.postPrivate('/api/spot/v1/trade/cancel-order-v2', params); + } + + /** + * Cancel all spot orders for a symbol + */ + cancelSymbolOrders(symbol: string): Promise> { + return this.postPrivate('/api/spot/v1/trade/cancel-symbol-order', { + symbol, + }); + } + /** Cancel order in batch (per symbol) */ batchCancelOrder( symbol: string, @@ -280,6 +303,16 @@ export class SpotClient extends BaseRestClient { }); } + /** Cancel order in batch (per symbol). V2 endpoint, supports orderIds or clientOids. */ + batchCancelOrderV2( + params: BatchCancelSpotOrderV2 + ): Promise> { + return this.postPrivate( + '/api/spot/v1/trade/cancel-batch-orders-v2', + params + ); + } + /** Get order details */ getOrder( symbol: string, @@ -321,4 +354,49 @@ export class SpotClient extends BaseRestClient { ...pagination, }); } + + /** Place plan order */ + submitPlanOrder( + params: NewSpotPlanOrder + ): Promise> { + return this.postPrivate('/api/spot/v1/plan/placePlan', params); + } + + /** Modify plan order */ + modifyPlanOrder( + params: ModifySpotPlanOrder + ): Promise> { + return this.postPrivate('/api/spot/v1/plan/modifyPlan', params); + } + + /** Cancel plan order */ + cancelPlanOrder( + params: CancelSpotPlanOrderParams + ): Promise> { + return this.postPrivate('/api/spot/v1/plan/cancelPlan', params); + } + + /** Get current plan orders */ + getCurrentPlanOrders(params: GetSpotPlanOrdersParams): Promise< + APIResponse<{ + nextFlag: boolean; + endId: number; + orderList: SpotPlanOrder[]; + }> + > { + return this.postPrivate('/api/spot/v1/plan/currentPlan', params); + } + + /** Get history plan orders */ + getHistoricPlanOrders(params: GetHistoricPlanOrdersParams): Promise< + APIResponse<{ + nextFlag: boolean; + endId: number; + orderList: SpotPlanOrder[]; + }> + > { + return this.postPrivate('/api/spot/v1/plan/historyPlan', params); + } + + // } diff --git a/src/types/request/spot.ts b/src/types/request/spot.ts index 10e4fb2..071f2ee 100644 --- a/src/types/request/spot.ts +++ b/src/types/request/spot.ts @@ -10,18 +10,6 @@ export interface NewWalletTransfer { clientOid?: string; } -export interface NewSpotOrder { - symbol: string; - side: 'buy' | 'sell'; - orderType: 'limit' | 'market'; - force: OrderTimeInForce; - price?: string; - quantity: string; - clientOrderId?: string; -} - -export type NewBatchSpotOrder = Omit; - export interface NewSpotSubTransfer { fromType: WalletType; toType: WalletType; @@ -41,3 +29,79 @@ export interface NewSpotWithdraw { remark?: string; clientOid?: string; } + +export interface NewSpotOrder { + symbol: string; + side: 'buy' | 'sell'; + orderType: 'limit' | 'market'; + force: OrderTimeInForce; + price?: string; + quantity: string; + clientOrderId?: string; +} + +export type NewBatchSpotOrder = Omit; + +export interface CancelSpotOrderV2 { + symbol: string; + orderId?: string; + clientOid?: string; +} + +export interface BatchCancelSpotOrderV2 { + symbol: string; + orderIds?: string[]; + clientOids?: string[]; +} + +export interface NewSpotPlanOrder { + symbol: string; + side: 'buy' | 'sell'; + triggerPrice: number; + executePrice?: number; + size: number; + triggerType: 'fill_price' | 'market_price'; + orderType: 'limit' | 'market'; + clientOid?: string; + timeInForceValue?: string; +} + +export interface NewSpotPlanOrder { + symbol: string; + side: 'buy' | 'sell'; + triggerPrice: number; + executePrice?: number; + size: number; + triggerType: 'fill_price' | 'market_price'; + orderType: 'limit' | 'market'; + clientOid?: string; + timeInForceValue?: string; +} + +export interface ModifySpotPlanOrder { + orderId?: string; + clientOid?: string; + triggerPrice: number; + executePrice?: number; + size?: string; + orderType: 'limit' | 'market'; +} + +export interface CancelSpotPlanOrderParams { + orderId?: string; + clientOid?: string; +} + +export interface GetSpotPlanOrdersParams { + symbol: string; + pageSize: string; + lastEndId?: string; +} + +export interface GetHistoricPlanOrdersParams { + symbol: string; + pageSize: string; + lastEndId?: string; + startTime: string; + endTime: string; +} diff --git a/src/types/response/spot.ts b/src/types/response/spot.ts index 4c6ea61..460fbcb 100644 --- a/src/types/response/spot.ts +++ b/src/types/response/spot.ts @@ -20,3 +20,23 @@ export interface SymbolRules { quantityScale: string; status: string; } + +export interface SpotOrderResult { + orderId: string; + clientOrderId: string; +} + +export interface SpotPlanOrder { + orderId: string; + clientOid: string; + symbol: string; + size: string; + executePrice: string; + triggerPrice: string; + status: string; + orderType: string; + side: string; + triggerType: string; + enterPointSource: string; + cTime: number; +} diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index 425ce96..b2c8a90 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -309,7 +309,6 @@ export default abstract class BaseRestClient { 'ACCESS-PASSPHRASE': this.apiPass, 'ACCESS-TIMESTAMP': signResult.timestamp, 'ACCESS-SIGN': signResult.sign, - 'Content-Type': 'application/json', }; if (method === 'GET') { diff --git a/test/spot/private.read.test.ts b/test/spot/private.read.test.ts index e7ac101..784ce02 100644 --- a/test/spot/private.read.test.ts +++ b/test/spot/private.read.test.ts @@ -159,4 +159,45 @@ describe('Private Spot REST API GET Endpoints', () => { expect(e).toBeNull(); } }); + + it('getCurrentPlanOrders()', async () => { + try { + expect( + await api.getCurrentPlanOrders({ symbol, pageSize: '20' }) + ).toMatchObject({ + ...sucessEmptyResponseObject(), + data: { + endId: null, + nextFlag: false, + orderList: expect.any(Array), + }, + }); + } catch (e) { + console.error('getCurrentPlanOrders: ', e); + expect(e).toBeNull(); + } + }); + + it('getHistoricPlanOrders()', async () => { + try { + expect( + await api.getHistoricPlanOrders({ + symbol, + pageSize: '20', + startTime: '1667889483000', + endTime: '1668134732000', + }) + ).toMatchObject({ + ...sucessEmptyResponseObject(), + data: { + endId: null, + nextFlag: false, + orderList: expect.any(Array), + }, + }); + } catch (e) { + console.error('getHistoricPlanOrders: ', e); + expect(e).toBeNull(); + } + }); }); diff --git a/test/spot/private.write.test.ts b/test/spot/private.write.test.ts index 3ed2e8d..8be72ce 100644 --- a/test/spot/private.write.test.ts +++ b/test/spot/private.write.test.ts @@ -20,203 +20,263 @@ describe('Private Spot REST API POST Endpoints', () => { const symbol = 'BTCUSDT_SPBL'; const coin = 'USDT'; - const timestampOneHourAgo = new Date().getTime() - 1000 * 60 * 60; - const from = timestampOneHourAgo.toFixed(0); - const to = String(Number(from) + 1000 * 60 * 30); // 30 minutes - it('transfer()', async () => { - try { - expect( - await api.transfer({ - amount: '100', - coin, - fromType: 'spot', - toType: 'mix_usdt', - }) - ).toStrictEqual(''); - } catch (e) { - // console.error('transfer: ', e); - expect(e.body).toMatchObject({ - // not sure what this error means, probably no balance. Seems to change? - code: expect.stringMatching(/42013|43117/gim), - }); - } + describe('transfers', () => { + it('transfer()', async () => { + try { + expect( + await api.transfer({ + amount: '100', + coin, + fromType: 'spot', + toType: 'mix_usdt', + }) + ).toStrictEqual(''); + } catch (e) { + // console.error('transfer: ', e); + expect(e.body).toMatchObject({ + // not sure what this error means, probably no balance. Seems to change? + code: expect.stringMatching(/42013|43117/gim), + }); + } + }); + + it('transferV2()', async () => { + try { + expect( + await api.transferV2({ + amount: '100', + coin, + fromType: 'spot', + toType: 'mix_usdt', + }) + ).toStrictEqual(''); + } catch (e) { + // console.error('transferV2: ', e); + expect(e.body).toMatchObject({ + // not sure what this error means, probably no balance. Seems to change? + code: expect.stringMatching(/42013|43117/gim), + }); + } + }); + + it('subTransfer()', async () => { + try { + expect( + await api.subTransfer({ + fromUserId: '123', + toUserId: '456', + amount: '100', + clientOid: '123456', + coin, + fromType: 'spot', + toType: 'mix_usdt', + }) + ).toStrictEqual(''); + } catch (e) { + // console.error('transferV2: ', e); + expect(e.body).toMatchObject({ + // not sure what this error means, probably no balance. Seems to change? + code: expect.stringMatching(/42013|43117|40018/gim), + }); + } + }); + + it('withdraw()', async () => { + try { + expect( + await api.withdraw({ + amount: '100', + coin, + chain: 'TRC20', + address: `123456`, + }) + ).toMatchObject({ + ...sucessEmptyResponseObject(), + data: expect.any(Array), + }); + } catch (e) { + expect(e.body).toMatchObject({ + code: API_ERROR_CODE.INCORRECT_PERMISSIONS, + }); + } + }); + + it('withdrawV2()', async () => { + try { + expect( + await api.withdrawV2({ + amount: '100', + coin, + chain: 'TRC20', + address: `123456`, + }) + ).toMatchObject({ + ...sucessEmptyResponseObject(), + data: expect.any(Array), + }); + } catch (e) { + expect(e.body).toMatchObject({ + code: API_ERROR_CODE.INCORRECT_PERMISSIONS, + }); + } + }); + + it('innerWithdraw()', async () => { + try { + expect(await api.innerWithdraw(coin, '12345', '1')).toMatchObject({ + ...sucessEmptyResponseObject(), + data: expect.any(Array), + }); + } catch (e) { + expect(e.body).toMatchObject({ + code: API_ERROR_CODE.INCORRECT_PERMISSIONS, + }); + } + }); + + it('innerWithdrawV2()', async () => { + try { + expect(await api.innerWithdrawV2(coin, '12345', '1')).toMatchObject({ + ...sucessEmptyResponseObject(), + data: expect.any(Array), + }); + } catch (e) { + expect(e.body).toMatchObject({ + code: API_ERROR_CODE.INCORRECT_PERMISSIONS, + }); + } + }); }); - - it('transferV2()', async () => { - try { - expect( - await api.transferV2({ - amount: '100', - coin, - fromType: 'spot', - toType: 'mix_usdt', - }) - ).toStrictEqual(''); - } catch (e) { - // console.error('transferV2: ', e); - expect(e.body).toMatchObject({ - // not sure what this error means, probably no balance. Seems to change? - code: expect.stringMatching(/42013|43117/gim), - }); - } - }); - - it('subTransfer()', async () => { - try { - expect( - await api.subTransfer({ - fromUserId: '123', - toUserId: '456', - amount: '100', - clientOid: '123456', - coin, - fromType: 'spot', - toType: 'mix_usdt', - }) - ).toStrictEqual(''); - } catch (e) { - // console.error('transferV2: ', e); - expect(e.body).toMatchObject({ - // not sure what this error means, probably no balance. Seems to change? - code: expect.stringMatching(/42013|43117/gim), - }); - } - }); - - it('withdraw()', async () => { - try { - expect( - await api.withdraw({ - amount: '100', - coin, - chain: 'TRC20', - address: `123456`, - }) - ).toMatchObject({ - ...sucessEmptyResponseObject(), - data: expect.any(Array), - }); - } catch (e) { - expect(e.body).toMatchObject({ - code: API_ERROR_CODE.INCORRECT_PERMISSIONS, - }); - } - }); - - it('withdrawV2()', async () => { - try { - expect( - await api.withdrawV2({ - amount: '100', - coin, - chain: 'TRC20', - address: `123456`, - }) - ).toMatchObject({ - ...sucessEmptyResponseObject(), - data: expect.any(Array), - }); - } catch (e) { - expect(e.body).toMatchObject({ - code: API_ERROR_CODE.INCORRECT_PERMISSIONS, - }); - } - }); - - it('innerWithdraw()', async () => { - try { - expect(await api.innerWithdraw(coin, '12345', '1')).toMatchObject({ - ...sucessEmptyResponseObject(), - data: expect.any(Array), - }); - } catch (e) { - expect(e.body).toMatchObject({ - code: API_ERROR_CODE.INCORRECT_PERMISSIONS, - }); - } - }); - - it('innerWithdrawV2()', async () => { - try { - expect(await api.innerWithdrawV2(coin, '12345', '1')).toMatchObject({ - ...sucessEmptyResponseObject(), - data: expect.any(Array), - }); - } catch (e) { - expect(e.body).toMatchObject({ - code: API_ERROR_CODE.INCORRECT_PERMISSIONS, - }); - } - }); - - it('submitOrder()', async () => { - try { - expect( - await api.submitOrder({ - symbol, - side: 'buy', - orderType: 'market', - quantity: '1', - force: 'normal', - }) - ).toMatchObject({ - ...sucessEmptyResponseObject(), - data: expect.any(Array), - }); - } catch (e) { - expect(e.body).toMatchObject({ - code: API_ERROR_CODE.QTY_LESS_THAN_MINIMUM, - }); - } - }); - - it('batchSubmitOrder()', async () => { - try { - expect( - await api.batchSubmitOrder(symbol, [ - { + describe('orders', () => { + it('submitOrder()', async () => { + try { + expect( + await api.submitOrder({ + symbol, side: 'buy', orderType: 'market', quantity: '1', force: 'normal', + }) + ).toMatchObject({ + ...sucessEmptyResponseObject(), + data: expect.any(Array), + }); + } catch (e) { + expect(e.body).toMatchObject({ + code: API_ERROR_CODE.QTY_LESS_THAN_MINIMUM, + }); + } + }); + + it('batchSubmitOrder()', async () => { + try { + expect( + await api.batchSubmitOrder(symbol, [ + { + side: 'buy', + orderType: 'market', + quantity: '1', + force: 'normal', + }, + ]) + ).toMatchObject({ + ...sucessEmptyResponseObject(), + data: { + resultList: expect.any(Array), + failure: [{ errorCode: API_ERROR_CODE.QTY_LESS_THAN_MINIMUM }], }, - ]) - ).toMatchObject({ - ...sucessEmptyResponseObject(), - data: { - resultList: expect.any(Array), - failure: [{ errorCode: API_ERROR_CODE.QTY_LESS_THAN_MINIMUM }], - }, - }); - } catch (e) { - expect(e).toBeNull(); - } + }); + } catch (e) { + expect(e).toBeNull(); + } + }); + + it('cancelOrder()', async () => { + try { + expect(await api.cancelOrder(symbol, '123456')).toMatchObject({ + ...sucessEmptyResponseObject(), + data: expect.any(Array), + }); + } catch (e) { + expect(e.body).toMatchObject({ + code: API_ERROR_CODE.ORDER_NOT_FOUND, + }); + } + }); + + it('batchCancelOrder()', async () => { + try { + expect(await api.batchCancelOrder(symbol, ['123456'])).toMatchObject({ + ...sucessEmptyResponseObject(), + data: expect.any(Array), + }); + } catch (e) { + expect(e.body).toMatchObject({ + code: API_ERROR_CODE.ORDER_NOT_FOUND, + }); + } + }); }); - it('cancelOrder()', async () => { - try { - expect(await api.cancelOrder(symbol, '123456')).toMatchObject({ - ...sucessEmptyResponseObject(), - data: expect.any(Array), - }); - } catch (e) { - expect(e.body).toMatchObject({ - code: API_ERROR_CODE.ORDER_NOT_FOUND, - }); - } - }); + describe('plan orders', () => { + let planOrderId: string; - it('batchCancelOrder()', async () => { - try { - expect(await api.batchCancelOrder(symbol, ['123456'])).toMatchObject({ - ...sucessEmptyResponseObject(), - data: expect.any(Array), - }); - } catch (e) { - expect(e.body).toMatchObject({ - code: API_ERROR_CODE.ORDER_NOT_FOUND, - }); - } + it('submitPlanOrder()', async () => { + try { + const result = await api.submitPlanOrder({ + symbol, + side: 'buy', + orderType: 'market', + size: 100, + triggerPrice: 100, + triggerType: 'fill_price', + }); + + planOrderId = result.data.orderId; + expect(result).toMatchObject({ + ...sucessEmptyResponseObject(), + }); + } catch (e) { + console.error('submitPlanOrder(): ', e); + expect(e).toBeNull(); + } + }); + + it('modifyPlanOrder()', async () => { + try { + expect( + await api.modifyPlanOrder({ + orderType: 'market', + triggerPrice: 100, + orderId: '123456', + }) + ).toMatchObject({ + ...sucessEmptyResponseObject(), + data: expect.any(Array), + }); + } catch (e) { + expect(e.body).toMatchObject({ + code: API_ERROR_CODE.PLAN_ORDER_NOT_FOUND, + }); + } + }); + + it('cancelPlanOrder()', async () => { + try { + expect( + await api.cancelPlanOrder({ + orderId: planOrderId || '123456', + }) + ).toMatchObject({ + ...sucessEmptyResponseObject(), + data: expect.any(String), + }); + } catch (e) { + console.error('cancelPlanOrder(): ', e); + expect(e).toBeNull(); + } + }); }); });