feat(): add plan order endpoints

This commit is contained in:
Tiago Siebler
2023-03-22 15:00:40 +00:00
parent bf2e545d09
commit 31aee2a0e5
6 changed files with 464 additions and 202 deletions

View File

@@ -9,6 +9,15 @@ import {
SymbolRules, SymbolRules,
NewSpotSubTransfer, NewSpotSubTransfer,
NewSpotWithdraw, NewSpotWithdraw,
CancelSpotOrderV2,
BatchCancelSpotOrderV2,
SpotOrderResult,
NewSpotPlanOrder,
ModifySpotPlanOrder,
CancelSpotPlanOrderParams,
GetSpotPlanOrdersParams,
SpotPlanOrder,
GetHistoricPlanOrdersParams,
} from './types'; } from './types';
import { REST_CLIENT_TYPE_ENUM } from './util'; import { REST_CLIENT_TYPE_ENUM } from './util';
import BaseRestClient from './util/BaseRestClient'; import BaseRestClient from './util/BaseRestClient';
@@ -246,7 +255,7 @@ export class SpotClient extends BaseRestClient {
*/ */
/** Place order */ /** Place order */
submitOrder(params: NewSpotOrder): Promise<APIResponse<any>> { submitOrder(params: NewSpotOrder): Promise<APIResponse<SpotOrderResult>> {
return this.postPrivate('/api/spot/v1/trade/orders', params); 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<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/trade/cancel-order-v2', params);
}
/**
* Cancel all spot orders for a symbol
*/
cancelSymbolOrders(symbol: string): Promise<APIResponse<any>> {
return this.postPrivate('/api/spot/v1/trade/cancel-symbol-order', {
symbol,
});
}
/** Cancel order in batch (per symbol) */ /** Cancel order in batch (per symbol) */
batchCancelOrder( batchCancelOrder(
symbol: string, 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<APIResponse<any>> {
return this.postPrivate(
'/api/spot/v1/trade/cancel-batch-orders-v2',
params
);
}
/** Get order details */ /** Get order details */
getOrder( getOrder(
symbol: string, symbol: string,
@@ -321,4 +354,49 @@ export class SpotClient extends BaseRestClient {
...pagination, ...pagination,
}); });
} }
/** Place plan order */
submitPlanOrder(
params: NewSpotPlanOrder
): Promise<APIResponse<SpotOrderResult>> {
return this.postPrivate('/api/spot/v1/plan/placePlan', params);
}
/** Modify plan order */
modifyPlanOrder(
params: ModifySpotPlanOrder
): Promise<APIResponse<SpotOrderResult>> {
return this.postPrivate('/api/spot/v1/plan/modifyPlan', params);
}
/** Cancel plan order */
cancelPlanOrder(
params: CancelSpotPlanOrderParams
): Promise<APIResponse<string>> {
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);
}
//
} }

View File

@@ -10,18 +10,6 @@ export interface NewWalletTransfer {
clientOid?: 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<NewSpotOrder, 'symbol'>;
export interface NewSpotSubTransfer { export interface NewSpotSubTransfer {
fromType: WalletType; fromType: WalletType;
toType: WalletType; toType: WalletType;
@@ -41,3 +29,79 @@ export interface NewSpotWithdraw {
remark?: string; remark?: string;
clientOid?: 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<NewSpotOrder, 'symbol'>;
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;
}

View File

@@ -20,3 +20,23 @@ export interface SymbolRules {
quantityScale: string; quantityScale: string;
status: 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;
}

View File

@@ -309,7 +309,6 @@ export default abstract class BaseRestClient {
'ACCESS-PASSPHRASE': this.apiPass, 'ACCESS-PASSPHRASE': this.apiPass,
'ACCESS-TIMESTAMP': signResult.timestamp, 'ACCESS-TIMESTAMP': signResult.timestamp,
'ACCESS-SIGN': signResult.sign, 'ACCESS-SIGN': signResult.sign,
'Content-Type': 'application/json',
}; };
if (method === 'GET') { if (method === 'GET') {

View File

@@ -159,4 +159,45 @@ describe('Private Spot REST API GET Endpoints', () => {
expect(e).toBeNull(); 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();
}
});
}); });

View File

@@ -20,203 +20,263 @@ describe('Private Spot REST API POST Endpoints', () => {
const symbol = 'BTCUSDT_SPBL'; const symbol = 'BTCUSDT_SPBL';
const coin = 'USDT'; 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 () => { describe('transfers', () => {
try { it('transfer()', async () => {
expect( try {
await api.transfer({ expect(
amount: '100', await api.transfer({
coin, amount: '100',
fromType: 'spot', coin,
toType: 'mix_usdt', fromType: 'spot',
}) toType: 'mix_usdt',
).toStrictEqual(''); })
} catch (e) { ).toStrictEqual('');
// console.error('transfer: ', e); } catch (e) {
expect(e.body).toMatchObject({ // console.error('transfer: ', e);
// not sure what this error means, probably no balance. Seems to change? expect(e.body).toMatchObject({
code: expect.stringMatching(/42013|43117/gim), // 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,
});
}
});
}); });
describe('orders', () => {
it('transferV2()', async () => { it('submitOrder()', async () => {
try { try {
expect( expect(
await api.transferV2({ await api.submitOrder({
amount: '100', symbol,
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, [
{
side: 'buy', side: 'buy',
orderType: 'market', orderType: 'market',
quantity: '1', quantity: '1',
force: 'normal', 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({ } catch (e) {
...sucessEmptyResponseObject(), expect(e).toBeNull();
data: { }
resultList: expect.any(Array), });
failure: [{ errorCode: API_ERROR_CODE.QTY_LESS_THAN_MINIMUM }],
}, it('cancelOrder()', async () => {
}); try {
} catch (e) { expect(await api.cancelOrder(symbol, '123456')).toMatchObject({
expect(e).toBeNull(); ...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 () => { describe('plan orders', () => {
try { let planOrderId: string;
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 () => { it('submitPlanOrder()', async () => {
try { try {
expect(await api.batchCancelOrder(symbol, ['123456'])).toMatchObject({ const result = await api.submitPlanOrder({
...sucessEmptyResponseObject(), symbol,
data: expect.any(Array), side: 'buy',
}); orderType: 'market',
} catch (e) { size: 100,
expect(e.body).toMatchObject({ triggerPrice: 100,
code: API_ERROR_CODE.ORDER_NOT_FOUND, 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();
}
});
}); });
}); });