From 518735087849c2705b9c13acc0021f6b35cbce7d Mon Sep 17 00:00:00 2001 From: tiagosiebler Date: Thu, 8 Sep 2022 16:48:33 +0100 Subject: [PATCH] add USDC perp client with tests --- src/index.ts | 1 + src/types/request/index.ts | 1 + src/types/request/usdc-perp.ts | 19 ++ src/types/shared.ts | 14 +- src/usdc-options-client.ts | 2 +- src/usdc-perpetual-client.ts | 287 ++++++++++++++++++++++ src/util/BaseRestClient.ts | 2 +- src/util/requestUtils.ts | 19 +- test/response.util.ts | 8 +- test/usdc/options/private.write.test.ts | 13 +- test/usdc/perpetual/private.read.test.ts | 74 ++++++ test/usdc/perpetual/private.write.test.ts | 85 +++++++ test/usdc/perpetual/public.read.test.ts | 95 +++++++ 13 files changed, 583 insertions(+), 37 deletions(-) create mode 100644 src/types/request/usdc-perp.ts create mode 100644 src/usdc-perpetual-client.ts create mode 100644 test/usdc/perpetual/private.read.test.ts create mode 100644 test/usdc/perpetual/private.write.test.ts create mode 100644 test/usdc/perpetual/public.read.test.ts diff --git a/src/index.ts b/src/index.ts index c7e61c8..7e4c856 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export * from './inverse-futures-client'; export * from './linear-client'; export * from './spot-client'; export * from './usdc-options-client'; +export * from './usdc-perpetual-client'; export * from './websocket-client'; export * from './logger'; export * from './types'; diff --git a/src/types/request/index.ts b/src/types/request/index.ts index bebf291..4194535 100644 --- a/src/types/request/index.ts +++ b/src/types/request/index.ts @@ -1,2 +1,3 @@ export * from './account-asset'; export * from './usdt-perp'; +export * from './usdc-perp'; diff --git a/src/types/request/usdc-perp.ts b/src/types/request/usdc-perp.ts new file mode 100644 index 0000000..dc2b209 --- /dev/null +++ b/src/types/request/usdc-perp.ts @@ -0,0 +1,19 @@ +export interface USDCKlineRequest { + symbol: string; + period: string; + startTime: number; + limit?: string; +} + +export interface USDCOpenInterestRequest { + symbol: string; + period: string; + limit?: number; +} + +export interface USDCLast500TradesRequest { + category: string; + symbol?: string; + baseCoin?: string; + limit?: string; +} diff --git a/src/types/shared.ts b/src/types/shared.ts index 23c0908..7312a58 100644 --- a/src/types/shared.ts +++ b/src/types/shared.ts @@ -25,6 +25,12 @@ export interface APIResponse { result: T; } +export interface USDCAPIResponse { + retCode: number; + retMsg: 'OK' | string; + result: T; +} + export interface APIResponseWithTime extends APIResponse { /** UTC timestamp */ time_now: numberInString; @@ -37,15 +43,15 @@ export interface SymbolParam { symbol: string; } -export interface SymbolLimitParam { +export interface SymbolLimitParam { symbol: string; - limit?: number; + limit?: TLimit; } -export interface SymbolPeriodLimitParam { +export interface SymbolPeriodLimitParam { symbol: string; period: string; - limit?: number; + limit?: TLimit; } export interface SymbolFromLimitParam { diff --git a/src/usdc-options-client.ts b/src/usdc-options-client.ts index 0bb3f9a..bfb190a 100644 --- a/src/usdc-options-client.ts +++ b/src/usdc-options-client.ts @@ -7,7 +7,7 @@ import BaseRestClient from './util/BaseRestClient'; */ export class USDCOptionsClient extends BaseRestClient { getClientType() { - return REST_CLIENT_TYPE_ENUM.usdcOptions; + return REST_CLIENT_TYPE_ENUM.usdc; } async fetchServerTime(): Promise { diff --git a/src/usdc-perpetual-client.ts b/src/usdc-perpetual-client.ts new file mode 100644 index 0000000..55b2743 --- /dev/null +++ b/src/usdc-perpetual-client.ts @@ -0,0 +1,287 @@ +import { + APIResponseWithTime, + SymbolLimitParam, + SymbolPeriodLimitParam, + USDCAPIResponse, + USDCKlineRequest, + USDCLast500TradesRequest, + USDCOpenInterestRequest, +} from './types'; +import { REST_CLIENT_TYPE_ENUM } from './util'; +import BaseRestClient from './util/BaseRestClient'; + +/** + * REST API client for USDC Perpetual APIs + */ +export class USDCPerpetualClient extends BaseRestClient { + getClientType() { + return REST_CLIENT_TYPE_ENUM.usdc; + } + + async fetchServerTime(): Promise { + const res = await this.getServerTime(); + return Number(res.time_now); + } + + /** + * + * Market Data Endpoints + * + */ + + getOrderBook(symbol: string): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/order-book', { symbol }); + } + + /** Fetch trading rules (such as min/max qty). Query for all if blank. */ + getContractInfo(params?: unknown): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/symbols', params); + } + + /** Get a symbol price/statistics ticker */ + getSymbolTicker(symbol: string): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/tick', { symbol }); + } + + getKline(params: USDCKlineRequest): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/kline/list', params); + } + + getMarkPrice(params: USDCKlineRequest): Promise> { + return this.get( + '/perpetual/usdc/openapi/public/v1/mark-price-kline', + params + ); + } + + getIndexPrice(params: USDCKlineRequest): Promise> { + return this.get( + '/perpetual/usdc/openapi/public/v1/index-price-kline', + params + ); + } + + getIndexPremium(params: USDCKlineRequest): Promise> { + return this.get( + '/perpetual/usdc/openapi/public/v1/premium-index-kline', + params + ); + } + + getOpenInterest( + params: USDCOpenInterestRequest + ): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/open-interest', params); + } + + getLargeOrders( + params: SymbolLimitParam + ): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/big-deal', params); + } + + getLongShortRatio( + params: SymbolPeriodLimitParam + ): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/account-ratio', params); + } + + getLast500Trades( + params: USDCLast500TradesRequest + ): Promise> { + return this.get( + '/option/usdc/openapi/public/v1/query-trade-latest', + params + ); + } + + /** + * + * Account Data Endpoints + * + */ + + /** -> Order API */ + + /** + * Place an order using the USDC Derivatives Account. + * The request status can be queried in real-time. + * The response parameters must be queried through a query or a WebSocket response. + */ + submitOrder(params: unknown): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/place-order', + params + ); + } + + /** Active order parameters (such as quantity, price) and stop order parameters cannot be modified in one request at the same time. Please request modification separately. */ + modifyOrder(params: unknown): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/replace-order', + params + ); + } + + /** Cancel order */ + cancelOrder(params: unknown): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/cancel-order', + params + ); + } + + /** Cancel all active orders. The real-time response indicates whether the request is successful, depending on retCode. */ + cancelActiveOrders( + symbol: string, + orderFilter: string + ): Promise> { + return this.postPrivate('/perpetual/usdc/openapi/private/v1/cancel-all', { + symbol, + orderFilter, + }); + } + + /** Query Unfilled/Partially Filled Orders */ + getActiveOrders(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-active-orders', + params + ); + } + + /** Query order history. The endpoint only supports up to 30 days of queried records */ + getHistoricOrders(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-order-history', + params + ); + } + + /** Query trade history. The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ + getOrderExecutionHistory(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/execution-list', + params + ); + } + + /** -> Account API */ + + /** The endpoint only supports up to 30 days of queried records. An error will be returned if startTime is more than 30 days. */ + getTransactionLog(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-transaction-log', + params + ); + } + + /** Wallet info for USDC account. */ + getBalance(params?: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-wallet-balance', + params + ); + } + + /** Asset Info */ + getAssetInfo(baseCoin?: string): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-asset-info', + { baseCoin } + ); + } + + /** + * If USDC derivatives account balance is greater than X, you can open PORTFOLIO_MARGIN, and if it is less than Y, it will automatically close PORTFOLIO_MARGIN and change back to REGULAR_MARGIN. X and Y will be adjusted according to operational requirements. + * Rest API returns the result of checking prerequisites. You could get the real status of margin mode change by subscribing margin mode. + */ + setMarginMode( + newMarginMode: 'REGULAR_MARGIN' | 'PORTFOLIO_MARGIN' + ): Promise> { + return this.postPrivate( + '/option/usdc/private/asset/account/setMarginMode', + { setMarginMode: newMarginMode } + ); + } + + /** Query margin mode for USDC account. */ + getMarginMode(): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-margin-info' + ); + } + + /** -> Positions API */ + + /** Query my positions */ + getPositions(params: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/query-position', + params + ); + } + + /** Only for REGULAR_MARGIN */ + setLeverage(symbol: string, leverage: string): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/position/leverage/save', + { symbol, leverage } + ); + } + + /** Query Settlement History */ + getSettlementHistory(params?: unknown): Promise> { + return this.postPrivate( + '/option/usdc/openapi/private/v1/session-settlement', + params + ); + } + + /** -> Risk Limit API */ + + /** Query risk limit */ + getRiskLimit(symbol: string): Promise> { + return this.getPrivate( + '/perpetual/usdc/openapi/public/v1/risk-limit/list', + { + symbol, + } + ); + } + + /** Set risk limit */ + setRiskLimit(symbol: string, riskId: number): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/position/set-risk-limit', + { symbol, riskId } + ); + } + + /** -> Funding API */ + + /** Funding settlement occurs every 8 hours at 00:00 UTC, 08:00 UTC and 16:00 UTC. The current interval's fund fee settlement is based on the previous interval's fund rate. For example, at 16:00, the settlement is based on the fund rate generated at 8:00. The fund rate generated at 16:00 will be used at 0:00 the next day. */ + getLastFundingRate(symbol: string): Promise> { + return this.get('/perpetual/usdc/openapi/public/v1/prev-funding-rate', { + symbol, + }); + } + + /** Get predicted funding rate and my predicted funding fee */ + getPredictedFundingRate(symbol: string): Promise> { + return this.postPrivate( + '/perpetual/usdc/openapi/private/v1/predicted-funding', + { symbol } + ); + } + + /** + * + * API Data Endpoints + * + */ + + getServerTime(): Promise { + return this.get('/v2/public/time'); + } +} diff --git a/src/util/BaseRestClient.ts b/src/util/BaseRestClient.ts index 850cbc5..eb4dee5 100644 --- a/src/util/BaseRestClient.ts +++ b/src/util/BaseRestClient.ts @@ -124,7 +124,7 @@ export default abstract class BaseRestClient { } private isUSDCClient() { - return this.clientType === REST_CLIENT_TYPE_ENUM.usdcOptions; + return this.clientType === REST_CLIENT_TYPE_ENUM.usdc; } get(endpoint: string, params?: any) { diff --git a/src/util/requestUtils.ts b/src/util/requestUtils.ts index 610f074..9a79db6 100644 --- a/src/util/requestUtils.ts +++ b/src/util/requestUtils.ts @@ -61,23 +61,6 @@ export function getRestBaseUrl( return exchangeBaseUrls.testnet; } -export function isPublicEndpoint(endpoint: string): boolean { - const publicPrefixes = [ - 'v2/public', - 'public/linear', - 'spot/quote/v1', - 'spot/v1/symbols', - 'spot/v1/time', - ]; - - for (const prefix of publicPrefixes) { - if (endpoint.startsWith(prefix)) { - return true; - } - } - return false; -} - export function isWsPong(response: any) { if (response.pong || response.ping) { return true; @@ -101,7 +84,7 @@ export const REST_CLIENT_TYPE_ENUM = { inverseFutures: 'inverseFutures', linear: 'linear', spot: 'spot', - usdcOptions: 'usdcOptions', + usdc: 'usdc', } as const; export type RestClientType = diff --git a/test/response.util.ts b/test/response.util.ts index 5d6ba7d..18b3010 100644 --- a/test/response.util.ts +++ b/test/response.util.ts @@ -19,9 +19,15 @@ export function successResponseObject(successMsg: string | null = 'OK') { export function successUSDCResponseObject() { return { result: expect.any(Object), + ...successUSDCEmptyResponseObject(), + }; +} + +export function successUSDCEmptyResponseObject() { + return { retCode: API_ERROR_CODE.SUCCESS, retMsg: expect.stringMatching( - /OK|SUCCESS|success|success\.|Request accepted/gim + /OK|SUCCESS|success|success\.|Request accepted|/gim ), }; } diff --git a/test/usdc/options/private.write.test.ts b/test/usdc/options/private.write.test.ts index ed172fd..d04e9f7 100644 --- a/test/usdc/options/private.write.test.ts +++ b/test/usdc/options/private.write.test.ts @@ -1,8 +1,5 @@ import { API_ERROR_CODE, USDCOptionsClient } from '../../../src'; -import { - successResponseObject, - successUSDCResponseObject, -} from '../../response.util'; +import { successUSDCResponseObject } from '../../response.util'; describe('Private Account Asset REST API Endpoints', () => { const useLivenet = true; @@ -16,7 +13,6 @@ describe('Private Account Asset REST API Endpoints', () => { const api = new USDCOptionsClient(API_KEY, API_SECRET, useLivenet); - const category = 'OPTION'; const currency = 'USDC'; const symbol = 'BTC-30SEP22-400000-C'; @@ -159,11 +155,4 @@ describe('Private Account Asset REST API Endpoints', () => { retCode: API_ERROR_CODE.INSTITION_MMP_PROFILE_NOT_FOUND, }); }); - - /** - - it('asdfasfasdfasdf()', async () => { - expect(await api.asadfasdfasdfasf()).toStrictEqual(''); - }); - */ }); diff --git a/test/usdc/perpetual/private.read.test.ts b/test/usdc/perpetual/private.read.test.ts new file mode 100644 index 0000000..02f8a5c --- /dev/null +++ b/test/usdc/perpetual/private.read.test.ts @@ -0,0 +1,74 @@ +import { USDCPerpetualClient } from '../../../src'; +import { successUSDCResponseObject } from '../../response.util'; + +describe('Private Account Asset REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + + const symbol = 'BTCPERP'; + const category = 'PERPETUAL'; + + it('getActiveOrders()', async () => { + expect(await api.getActiveOrders({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getHistoricOrders()', async () => { + expect(await api.getHistoricOrders({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getOrderExecutionHistory()', async () => { + expect(await api.getOrderExecutionHistory({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getTransactionLog()', async () => { + expect(await api.getTransactionLog({ type: 'TRADE' })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getBalance()', async () => { + expect(await api.getBalance()).toMatchObject(successUSDCResponseObject()); + }); + + it('getAssetInfo()', async () => { + expect(await api.getAssetInfo()).toMatchObject(successUSDCResponseObject()); + }); + + it('getMarginMode()', async () => { + expect(await api.getMarginMode()).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getPositions()', async () => { + expect(await api.getPositions({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getSettlementHistory()', async () => { + expect(await api.getSettlementHistory({ symbol })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getPredictedFundingRate()', async () => { + expect(await api.getPredictedFundingRate(symbol)).toMatchObject( + successUSDCResponseObject() + ); + }); +}); diff --git a/test/usdc/perpetual/private.write.test.ts b/test/usdc/perpetual/private.write.test.ts new file mode 100644 index 0000000..f2c11da --- /dev/null +++ b/test/usdc/perpetual/private.write.test.ts @@ -0,0 +1,85 @@ +import { API_ERROR_CODE, USDCPerpetualClient } from '../../../src'; +import { + successUSDCEmptyResponseObject, + successUSDCResponseObject, +} from '../../response.util'; + +describe('Private Account Asset REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = process.env.API_KEY_COM; + const API_SECRET = process.env.API_SECRET_COM; + + it('should have api credentials to test with', () => { + expect(API_KEY).toStrictEqual(expect.any(String)); + expect(API_SECRET).toStrictEqual(expect.any(String)); + }); + + const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + + const symbol = 'BTCPERP'; + + it('submitOrder()', async () => { + expect( + await api.submitOrder({ + symbol, + side: 'Sell', + orderType: 'Limit', + orderFilter: 'Order', + orderQty: '1', + orderPrice: '20000', + orderLinkId: Date.now().toString(), + timeInForce: 'GoodTillCancel', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.INSUFFICIENT_BALANCE_FOR_ORDER_COST, + }); + }); + + it('modifyOrder()', async () => { + expect( + await api.modifyOrder({ + symbol, + orderId: 'somethingFake', + orderPrice: '20000', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, + }); + }); + + it('cancelOrder()', async () => { + expect( + await api.cancelOrder({ + symbol, + orderId: 'somethingFake1', + orderFilter: 'Order', + }) + ).toMatchObject({ + retCode: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE, + }); + }); + + it('cancelActiveOrders()', async () => { + expect(await api.cancelActiveOrders(symbol, 'Order')).toMatchObject( + successUSDCEmptyResponseObject() + ); + }); + + it('setMarginMode()', async () => { + expect(await api.setMarginMode('REGULAR_MARGIN')).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('setLeverage()', async () => { + expect(await api.setLeverage(symbol, '10')).toMatchObject({ + retCode: API_ERROR_CODE.LEVERAGE_NOT_MODIFIED, + }); + }); + + it('setRiskLimit()', async () => { + expect(await api.setRiskLimit(symbol, 1)).toMatchObject({ + retCode: API_ERROR_CODE.RISK_LIMIT_NOT_EXISTS, + }); + }); +}); diff --git a/test/usdc/perpetual/public.read.test.ts b/test/usdc/perpetual/public.read.test.ts new file mode 100644 index 0000000..4f90a62 --- /dev/null +++ b/test/usdc/perpetual/public.read.test.ts @@ -0,0 +1,95 @@ +import { USDCKlineRequest, USDCPerpetualClient } from '../../../src'; +import { + successResponseObject, + successUSDCResponseObject, +} from '../../response.util'; + +describe('Public USDC Options REST API Endpoints', () => { + const useLivenet = true; + const API_KEY = undefined; + const API_SECRET = undefined; + + const api = new USDCPerpetualClient(API_KEY, API_SECRET, useLivenet); + + const symbol = 'BTCPERP'; + const category = 'PERPETUAL'; + const startTime = Number((Date.now() / 1000).toFixed(0)); + + const candleRequest: USDCKlineRequest = { symbol, period: '1m', startTime }; + + it('getOrderBook()', async () => { + expect(await api.getOrderBook(symbol)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getContractInfo()', async () => { + expect(await api.getContractInfo()).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getSymbolTicker()', async () => { + expect(await api.getSymbolTicker(symbol)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getKline()', async () => { + expect(await api.getKline(candleRequest)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getMarkPrice()', async () => { + expect(await api.getMarkPrice(candleRequest)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getIndexPrice()', async () => { + expect(await api.getIndexPrice(candleRequest)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getIndexPremium()', async () => { + expect(await api.getIndexPremium(candleRequest)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getOpenInterest()', async () => { + expect(await api.getOpenInterest({ symbol, period: '1m' })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getLargeOrders()', async () => { + expect(await api.getLargeOrders({ symbol })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getLongShortRatio()', async () => { + expect(await api.getLongShortRatio({ symbol, period: '1m' })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getLast500Trades()', async () => { + expect(await api.getLast500Trades({ category })).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getLastFundingRate()', async () => { + expect(await api.getLastFundingRate(symbol)).toMatchObject( + successUSDCResponseObject() + ); + }); + + it('getServerTime()', async () => { + expect(await api.getServerTime()).toMatchObject(successResponseObject()); + }); +});