Merge pull request #150 from tiagosiebler/deprecateReqWrapper

v2.2.0: Extensive integration tests, revise base REST client, fix Spot REST client, type improvements
This commit is contained in:
Tiago
2022-05-21 23:20:41 +01:00
committed by GitHub
32 changed files with 10134 additions and 1009 deletions

7581
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "bybit-api",
"version": "2.1.10",
"version": "2.2.0",
"description": "Node.js connector for Bybit's REST APIs and WebSockets, with TypeScript & integration tests.",
"main": "lib/index.js",
"types": "lib/index.d.ts",

View File

@@ -9,3 +9,34 @@ export const positionTpSlModeEnum = {
/** Partial take profit/stop loss mode (multiple TP and SL orders can be placed, covering portions of the position) */
Partial: 'Partial',
} as const;
export const API_ERROR_CODE = {
BALANCE_INSUFFICIENT_SPOT: -1131,
ORDER_NOT_FOUND_OR_TOO_LATE_SPOT: -2013,
/** This could mean bad request, incorrect value types or even incorrect/missing values */
PARAMS_MISSING_OR_WRONG: 10001,
ORDER_NOT_FOUND_OR_TOO_LATE: 20001,
POSITION_STATUS_NOT_NORMAL: 30013,
CANNOT_SET_TRADING_STOP_FOR_ZERO_POS: 30024,
/** Seen when placing an order */
INSUFFICIENT_BALANCE_FOR_ORDER_COST: 30031,
POSITION_IDX_NOT_MATCH_POSITION_MODE: 30041,
/** Seen if a conditional order is too large */
INSUFFICIENT_BALANCE: 30042,
/** E.g. trying to change position margin while on cross */
POSITION_IS_CROSS_MARGIN: 30056,
POSITION_MODE_NOT_MODIFIED: 30083,
ISOLATED_NOT_MODIFIED: 30084,
RISK_LIMIT_NOT_EXISTS: 30090,
LEVERAGE_NOT_MODIFIED: 34036,
SAME_SLTP_MODE: 37002,
ORDER_NOT_FOUND_OR_TOO_LATE_LINEAR: 130010,
ORDER_COST_NOT_AVAILABLE: 130021,
CANNOT_SET_LINEAR_TRADING_STOP_FOR_ZERO_POS: 130024,
ISOLATED_NOT_MODIFIED_LINEAR: 130056,
POSITION_SIZE_IS_ZERO: 130057,
AUTO_ADD_MARGIN_NOT_MODIFIED: 130060,
INSUFFICIENT_BALANCE_FOR_ORDER_COST_LINEAR: 130080,
SAME_SLTP_MODE_LINEAR: 130150,
RISK_ID_NOT_MODIFIED: 134026,
} as const;

View File

@@ -1,10 +1,10 @@
export * from "./inverse-client";
export * from "./inverse-futures-client";
export * from "./linear-client";
export * from "./spot-client";
export * from "./websocket-client";
export * from "./logger";
export * from "./types/shared";
export * from "./types/spot";
export * from "./util/WsStore";
export * from "./constants/enum";
export * from './inverse-client';
export * from './inverse-futures-client';
export * from './linear-client';
export * from './spot-client';
export * from './websocket-client';
export * from './logger';
export * from './types/shared';
export * from './types/spot';
export * from './util/WsStore';
export * from './constants/enum';

View File

@@ -1,12 +1,24 @@
import { AxiosRequestConfig } from 'axios';
import { GenericAPIResponse, getRestBaseUrl, RestClientOptions } from './util/requestUtils';
import RequestWrapper from './util/requestWrapper';
import SharedEndpoints from './shared-endpoints';
import { SymbolFromLimitParam, SymbolIntervalFromLimitParam, SymbolParam } from './types/shared';
export class InverseClient extends SharedEndpoints {
protected requestWrapper: RequestWrapper;
import {
getRestBaseUrl,
RestClientOptions,
REST_CLIENT_TYPE_ENUM,
} from './util/requestUtils';
import {
APIResponseWithTime,
AssetExchangeRecordsReq,
CoinParam,
SymbolInfo,
SymbolIntervalFromLimitParam,
SymbolLimitParam,
SymbolParam,
SymbolPeriodLimitParam,
WalletFundRecordsReq,
WithdrawRecordsReq,
} from './types/shared';
import BaseRestClient from './util/BaseRestClient';
export class InverseClient extends BaseRestClient {
/**
* @public Creates an instance of the inverse REST API client.
*
@@ -23,56 +35,149 @@ export class InverseClient extends SharedEndpoints {
restClientOptions: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {}
) {
super();
this.requestWrapper = new RequestWrapper(
super(
key,
secret,
getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions,
requestOptions
requestOptions,
REST_CLIENT_TYPE_ENUM.inverse
);
return this;
}
async fetchServerTime(): Promise<number> {
const res = await this.getServerTime();
return Number(res.time_now);
}
/**
*
* Market Data Endpoints
*
*/
getKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/kline/list', params);
getOrderBook(params: SymbolParam): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/orderBook/L2', params);
}
getKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/kline/list', params);
}
/**
* @deprecated use getTickers() instead
* Get latest information for symbol
*/
getLatestInformation(params?: Partial<SymbolParam>): GenericAPIResponse {
return this.getTickers(params);
getTickers(
params?: Partial<SymbolParam>
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/tickers', params);
}
getTrades(params: SymbolLimitParam): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/trading-records', params);
}
getSymbols(): Promise<APIResponseWithTime<SymbolInfo[]>> {
return this.get('v2/public/symbols');
}
getMarkPriceKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/mark-price-kline', params);
}
getIndexPriceKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/index-price-kline', params);
}
getPremiumIndexKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/premium-index-kline', params);
}
/**
* @deprecated use getTrades() instead
*
* Market Data : Advanced
*
*/
getPublicTradingRecords(params: SymbolFromLimitParam): GenericAPIResponse {
return this.getTrades(params);
getOpenInterest(
params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/open-interest', params);
}
getTrades(params: SymbolFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/trading-records', params);
getLatestBigDeal(
params: SymbolLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/big-deal', params);
}
getMarkPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/mark-price-kline', params);
getLongShortRatio(
params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/account-ratio', params);
}
getIndexPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/index-price-kline', params);
/**
*
* Account Data Endpoints
*
*/
getApiKeyInfo(): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/account/api-key');
}
getPremiumIndexKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/premium-index-kline', params);
/**
*
* Wallet Data Endpoints
*
*/
getWalletBalance(
params?: Partial<CoinParam>
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/balance', params);
}
getWalletFundRecords(
params?: WalletFundRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/fund/records', params);
}
getWithdrawRecords(
params?: WithdrawRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/withdraw/list', params);
}
getAssetExchangeRecords(
params?: AssetExchangeRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/exchange-order/list', params);
}
/**
*
* API Data Endpoints
*
*/
getServerTime(): Promise<APIResponseWithTime<{}>> {
return this.get('v2/public/time');
}
getApiAnnouncements(): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/announcement');
}
/**
@@ -99,8 +204,8 @@ export class InverseClient extends SharedEndpoints {
sl_trigger_by?: 'LastPrice' | 'MarkPrice' | 'IndexPrice';
close_on_trigger?: boolean;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/order/create', orderRequest);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/order/create', orderRequest);
}
getActiveOrderList(params: {
@@ -109,20 +214,22 @@ export class InverseClient extends SharedEndpoints {
direction?: string;
limit?: number;
cursor?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/order/list', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/order/list', params);
}
cancelActiveOrder(params: {
symbol: string;
order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/order/cancel', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/order/cancel', params);
}
cancelAllActiveOrders(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.post('v2/private/order/cancelAll', params);
cancelAllActiveOrders(
params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/order/cancelAll', params);
}
replaceActiveOrder(params: {
@@ -133,18 +240,18 @@ export class InverseClient extends SharedEndpoints {
p_r_price?: string;
take_profit?: number;
stop_loss?: number;
tp_trigger_by?:string;
sl_trigger_by?:string;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/order/replace', params);
tp_trigger_by?: string;
sl_trigger_by?: string;
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/order/replace', params);
}
queryActiveOrder(params: {
order_id?: string;
order_link_id?: string;
symbol: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/order', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/order', params);
}
/**
@@ -163,30 +270,33 @@ export class InverseClient extends SharedEndpoints {
trigger_by?: string;
close_on_trigger?: boolean;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/stop-order/create', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/stop-order/create', params);
}
/** get conditional order list. This may see delays, use queryConditionalOrder() for real-time queries */
getConditionalOrder(params: {
symbol: string;
stop_order_status?: string;
direction?: string;
limit?: number;
cursor?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/stop-order/list', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/stop-order/list', params);
}
cancelConditionalOrder(params: {
symbol: string;
stop_order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/stop-order/cancel', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/stop-order/cancel', params);
}
cancelAllConditionalOrders(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.post('v2/private/stop-order/cancelAll', params);
cancelAllConditionalOrders(
params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/stop-order/cancelAll', params);
}
replaceConditionalOrder(params: {
@@ -196,45 +306,33 @@ export class InverseClient extends SharedEndpoints {
p_r_qty?: number;
p_r_price?: string;
p_r_trigger_price?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/stop-order/replace', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/stop-order/replace', params);
}
queryConditionalOrder(params: {
symbol: string;
stop_order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/stop-order', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/stop-order', params);
}
/**
* Position
*/
/**
* @deprecated use getPosition() instead
*/
getUserLeverage(): GenericAPIResponse {
return this.requestWrapper.get('user/leverage');
}
getPosition(params?: Partial<SymbolParam>): GenericAPIResponse {
return this.requestWrapper.get('v2/private/position/list', params);
}
/**
* @deprecated use getPosition() instead
*/
getPositions(): GenericAPIResponse {
return this.requestWrapper.get('position/list');
getPosition(
params?: Partial<SymbolParam>
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/position/list', params);
}
changePositionMargin(params: {
symbol: string;
margin: string;
}): GenericAPIResponse {
return this.requestWrapper.post('position/change-position-margin', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('position/change-position-margin', params);
}
setTradingStop(params: {
@@ -245,23 +343,16 @@ export class InverseClient extends SharedEndpoints {
tp_trigger_by?: string;
sl_trigger_by?: string;
new_trailing_active?: number;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/position/trading-stop', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/position/trading-stop', params);
}
setUserLeverage(params: {
symbol: string;
leverage: number;
leverage_only?: boolean;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/position/leverage/save', params);
}
/**
* @deprecated use setUserLeverage() instead
*/
changeUserLeverage(params: any): GenericAPIResponse {
return this.setUserLeverage(params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/position/leverage/save', params);
}
getTradeRecords(params: {
@@ -271,8 +362,8 @@ export class InverseClient extends SharedEndpoints {
page?: number;
limit?: number;
order?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/execution/list', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/execution/list', params);
}
getClosedPnl(params: {
@@ -282,22 +373,15 @@ export class InverseClient extends SharedEndpoints {
exec_type?: string;
page?: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('v2/private/trade/closed-pnl/list', params);
}
setPositionMode(params: {
symbol: string;
mode: 0 | 3;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/position/switch-mode', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/trade/closed-pnl/list', params);
}
setSlTpPositionMode(params: {
symbol: string;
tp_sl_mode: 'Full' | 'Partial';
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/tpsl/switch-mode', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/tpsl/switch-mode', params);
}
setMarginType(params: {
@@ -305,52 +389,46 @@ export class InverseClient extends SharedEndpoints {
is_isolated: boolean;
buy_leverage: number;
sell_leverage: number;
}): GenericAPIResponse {
return this.requestWrapper.post('v2/private/position/switch-isolated', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('v2/private/position/switch-isolated', params);
}
/**
* Risk Limit
*/
getRiskLimitList(): GenericAPIResponse {
return this.requestWrapper.get('open-api/wallet/risk-limit/list');
getRiskLimitList(): Promise<APIResponseWithTime<any>> {
return this.getPrivate('open-api/wallet/risk-limit/list');
}
setRiskLimit(params: {
symbol: string;
risk_id: string;
}): GenericAPIResponse {
return this.requestWrapper.post('open-api/wallet/risk-limit', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('open-api/wallet/risk-limit', params);
}
/**
* Funding
*/
getLastFundingRate(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/funding/prev-funding-rate', params);
getLastFundingRate(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.get('v2/public/funding/prev-funding-rate', params);
}
getMyLastFundingFee(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get('v2/private/funding/prev-funding', params);
getMyLastFundingFee(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/funding/prev-funding', params);
}
getPredictedFunding(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get('v2/private/funding/predicted-funding', params);
getPredictedFunding(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/funding/predicted-funding', params);
}
/**
* LCP Info
*/
getLcpInfo(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get('v2/private/account/lcp', params);
getLcpInfo(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/account/lcp', params);
}
//API Key Info
getAPIKeyInfo(): GenericAPIResponse {
return this.requestWrapper.get('v2/private/account/api-key');
}
};
}

View File

@@ -1,12 +1,24 @@
import { AxiosRequestConfig } from 'axios';
import { GenericAPIResponse, getRestBaseUrl, RestClientOptions } from './util/requestUtils';
import RequestWrapper from './util/requestWrapper';
import SharedEndpoints from './shared-endpoints';
import { SymbolFromLimitParam, SymbolIntervalFromLimitParam, SymbolParam } from './types/shared';
export class InverseFuturesClient extends SharedEndpoints {
protected requestWrapper: RequestWrapper;
import {
getRestBaseUrl,
RestClientOptions,
REST_CLIENT_TYPE_ENUM,
} from './util/requestUtils';
import {
APIResponseWithTime,
AssetExchangeRecordsReq,
CoinParam,
SymbolInfo,
SymbolIntervalFromLimitParam,
SymbolLimitParam,
SymbolParam,
SymbolPeriodLimitParam,
WalletFundRecordsReq,
WithdrawRecordsReq,
} from './types/shared';
import BaseRestClient from './util/BaseRestClient';
export class InverseFuturesClient extends BaseRestClient {
/**
* @public Creates an instance of the inverse futures REST API client.
*
@@ -23,45 +35,98 @@ export class InverseFuturesClient extends SharedEndpoints {
restClientOptions: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {}
) {
super();
this.requestWrapper = new RequestWrapper(
super(
key,
secret,
getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions,
requestOptions
requestOptions,
REST_CLIENT_TYPE_ENUM.inverseFutures
);
return this;
}
async fetchServerTime(): Promise<number> {
const res = await this.getServerTime();
return Number(res.time_now);
}
/**
*
* Market Data Endpoints
* Note: These are currently the same as the inverse client
*
*/
getKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/kline/list', params);
getOrderBook(params: SymbolParam): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/orderBook/L2', params);
}
getKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/kline/list', params);
}
/**
* Get latest information for symbol
*/
getTickers(
params?: Partial<SymbolParam>
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/tickers', params);
}
/**
* Public trading records
*/
getTrades(params: SymbolFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/trading-records', params);
getTrades(params: SymbolLimitParam): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/trading-records', params);
}
getMarkPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/mark-price-kline', params);
getSymbols(): Promise<APIResponseWithTime<SymbolInfo[]>> {
return this.get('v2/public/symbols');
}
getIndexPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/index-price-kline', params);
getMarkPriceKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/mark-price-kline', params);
}
getPremiumIndexKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/premium-index-kline', params);
getIndexPriceKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/index-price-kline', params);
}
getPremiumIndexKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/premium-index-kline', params);
}
/**
*
* Market Data : Advanced
*
*/
getOpenInterest(
params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/open-interest', params);
}
getLatestBigDeal(
params: SymbolLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/big-deal', params);
}
getLongShortRatio(
params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/account-ratio', params);
}
/**
@@ -70,7 +135,61 @@ export class InverseFuturesClient extends SharedEndpoints {
*
*/
/**
getApiKeyInfo(): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/account/api-key');
}
/**
*
* Wallet Data Endpoints
*
*/
getWalletBalance(
params?: Partial<CoinParam>
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/balance', params);
}
getWalletFundRecords(
params?: WalletFundRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/fund/records', params);
}
getWithdrawRecords(
params?: WithdrawRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/withdraw/list', params);
}
getAssetExchangeRecords(
params?: AssetExchangeRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/exchange-order/list', params);
}
/**
*
* API Data Endpoints
*
*/
getServerTime(): Promise<APIResponseWithTime<{}>> {
return this.get('v2/public/time');
}
getApiAnnouncements(): Promise<APIResponseWithTime<any>> {
return this.get('v2/public/announcement');
}
/**
*
* Account Data Endpoints
*
*/
/**
* Active orders
*/
@@ -86,8 +205,8 @@ export class InverseFuturesClient extends SharedEndpoints {
reduce_only?: boolean;
close_on_trigger?: boolean;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('futures/private/order/create', orderRequest);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/order/create', orderRequest);
}
getActiveOrderList(params: {
@@ -96,20 +215,22 @@ export class InverseFuturesClient extends SharedEndpoints {
direction?: string;
limit?: number;
cursor?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('futures/private/order/list', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('futures/private/order/list', params);
}
cancelActiveOrder(params: {
symbol: string;
order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('futures/private/order/cancel', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/order/cancel', params);
}
cancelAllActiveOrders(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.post('futures/private/order/cancelAll', params);
cancelAllActiveOrders(
params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/order/cancelAll', params);
}
replaceActiveOrder(params: {
@@ -118,19 +239,19 @@ export class InverseFuturesClient extends SharedEndpoints {
symbol: string;
p_r_qty?: string;
p_r_price?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('futures/private/order/replace', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/order/replace', params);
}
queryActiveOrder(params: {
order_id?: string;
order_link_id?: string;
symbol: string;
}): GenericAPIResponse {
return this.requestWrapper.get('futures/private/order', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('futures/private/order', params);
}
/**
/**
* Conditional orders
*/
@@ -146,8 +267,8 @@ export class InverseFuturesClient extends SharedEndpoints {
trigger_by?: string;
close_on_trigger?: boolean;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('futures/private/stop-order/create', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/stop-order/create', params);
}
getConditionalOrder(params: {
@@ -156,20 +277,22 @@ export class InverseFuturesClient extends SharedEndpoints {
direction?: string;
limit?: number;
cursor?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('futures/private/stop-order/list', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('futures/private/stop-order/list', params);
}
cancelConditionalOrder(params: {
symbol: string;
stop_order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('futures/private/stop-order/cancel', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/stop-order/cancel', params);
}
cancelAllConditionalOrders(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.post('futures/private/stop-order/cancelAll', params);
cancelAllConditionalOrders(
params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/stop-order/cancelAll', params);
}
replaceConditionalOrder(params: {
@@ -179,35 +302,39 @@ export class InverseFuturesClient extends SharedEndpoints {
p_r_qty?: number;
p_r_price?: string;
p_r_trigger_price?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('futures/private/stop-order/replace', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/stop-order/replace', params);
}
queryConditionalOrder(params: {
symbol: string;
stop_order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('futures/private/stop-order', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('futures/private/stop-order', params);
}
/**
/**
* Position
*/
/**
* Get position list
*/
getPosition(params?: Partial<SymbolParam>): GenericAPIResponse {
return this.requestWrapper.get('futures/private/position/list', params);
getPosition(
params?: Partial<SymbolParam>
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('futures/private/position/list', params);
}
changePositionMargin(params: {
symbol: string;
margin: string;
}): GenericAPIResponse {
return this.requestWrapper.post('futures/private/position/change-position-margin', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate(
'futures/private/position/change-position-margin',
params
);
}
setTradingStop(params: {
@@ -218,16 +345,16 @@ export class InverseFuturesClient extends SharedEndpoints {
tp_trigger_by?: string;
sl_trigger_by?: string;
new_trailing_active?: number;
}): GenericAPIResponse {
return this.requestWrapper.post('futures/private/position/trading-stop', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/position/trading-stop', params);
}
setUserLeverage(params: {
symbol: string;
buy_leverage: number;
sell_leverage: number;
}): GenericAPIResponse {
return this.requestWrapper.post('futures/private/position/leverage/save', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/position/leverage/save', params);
}
/**
@@ -236,8 +363,8 @@ export class InverseFuturesClient extends SharedEndpoints {
setPositionMode(params: {
symbol: string;
mode: number;
}): GenericAPIResponse {
return this.requestWrapper.post('futures/private/position/switch-mode', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/position/switch-mode', params);
}
/**
@@ -248,8 +375,8 @@ export class InverseFuturesClient extends SharedEndpoints {
is_isolated: boolean;
buy_leverage: number;
sell_leverage: number;
}): GenericAPIResponse {
return this.requestWrapper.post('futures/private/position/switch-isolated', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('futures/private/position/switch-isolated', params);
}
getTradeRecords(params: {
@@ -259,8 +386,8 @@ export class InverseFuturesClient extends SharedEndpoints {
page?: number;
limit?: number;
order?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('futures/private/execution/list', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('futures/private/execution/list', params);
}
getClosedPnl(params: {
@@ -270,49 +397,49 @@ export class InverseFuturesClient extends SharedEndpoints {
exec_type?: string;
page?: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('futures/private/trade/closed-pnl/list', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('futures/private/trade/closed-pnl/list', params);
}
/**
**** The following are all the same as the inverse client ****
*/
/**
/**
* Risk Limit
*/
getRiskLimitList(): GenericAPIResponse {
return this.requestWrapper.get('open-api/wallet/risk-limit/list');
getRiskLimitList(): Promise<APIResponseWithTime<any>> {
return this.getPrivate('open-api/wallet/risk-limit/list');
}
setRiskLimit(params: {
symbol: string;
risk_id: string;
}): GenericAPIResponse {
return this.requestWrapper.post('open-api/wallet/risk-limit', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('open-api/wallet/risk-limit', params);
}
/**
/**
* Funding
*/
getLastFundingRate(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/funding/prev-funding-rate', params);
getLastFundingRate(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.get('v2/public/funding/prev-funding-rate', params);
}
getMyLastFundingFee(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get('v2/private/funding/prev-funding', params);
getMyLastFundingFee(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/funding/prev-funding', params);
}
getPredictedFunding(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get('v2/private/funding/predicted-funding', params);
getPredictedFunding(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/funding/predicted-funding', params);
}
/**
/**
* LCP Info
*/
getLcpInfo(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get('v2/private/account/lcp', params);
getLcpInfo(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/account/lcp', params);
}
};
}

View File

@@ -1,23 +1,28 @@
import { AxiosRequestConfig } from 'axios';
import {
GenericAPIResponse,
getRestBaseUrl,
RestClientOptions,
REST_CLIENT_TYPE_ENUM,
} from './util/requestUtils';
import RequestWrapper from './util/requestWrapper';
import SharedEndpoints from './shared-endpoints';
import {
APIResponse,
APIResponseWithTime,
AssetExchangeRecordsReq,
CoinParam,
SymbolInfo,
SymbolIntervalFromLimitParam,
SymbolLimitParam,
SymbolParam,
SymbolPeriodLimitParam,
WalletFundRecordsReq,
WithdrawRecordsReq,
} from './types/shared';
import { linearPositionModeEnum, positionTpSlModeEnum } from './constants/enum';
import BaseRestClient from './util/BaseRestClient';
export class LinearClient extends SharedEndpoints {
protected requestWrapper: RequestWrapper;
export class LinearClient extends BaseRestClient {
/**
* @public Creates an instance of the linear REST API client.
* @public Creates an instance of the linear (USD Perps) REST API client.
*
* @param {string} key - your API key
* @param {string} secret - your API secret
@@ -32,54 +37,153 @@ export class LinearClient extends SharedEndpoints {
restClientOptions: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {}
) {
super();
this.requestWrapper = new RequestWrapper(
super(
key,
secret,
getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions,
requestOptions
requestOptions,
REST_CLIENT_TYPE_ENUM.linear
);
return this;
}
async fetchServerTime(): Promise<number> {
const timeRes = await this.getServerTime();
return Number(timeRes.time_now);
}
/**
*
* Market Data Endpoints
*
*/
getKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('public/linear/kline', params);
getOrderBook(params: SymbolParam): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/orderBook/L2', params);
}
getTrades(params: SymbolLimitParam): GenericAPIResponse {
return this.requestWrapper.get(
'public/linear/recent-trading-records',
params
);
getKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('public/linear/kline', params);
}
getLastFundingRate(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get(
'public/linear/funding/prev-funding-rate',
params
);
/**
* Get latest information for symbol
*/
getTickers(
params?: Partial<SymbolParam>
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/tickers', params);
}
getMarkPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('public/linear/mark-price-kline', params);
getTrades(params: SymbolLimitParam): Promise<APIResponseWithTime<any[]>> {
return this.get('public/linear/recent-trading-records', params);
}
getIndexPriceKline(params: SymbolIntervalFromLimitParam): GenericAPIResponse {
return this.requestWrapper.get('public/linear/index-price-kline', params);
getSymbols(): Promise<APIResponse<SymbolInfo[]>> {
return this.get('v2/public/symbols');
}
getLastFundingRate(params: SymbolParam): Promise<APIResponseWithTime<any[]>> {
return this.get('public/linear/funding/prev-funding-rate', params);
}
getMarkPriceKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('public/linear/mark-price-kline', params);
}
getIndexPriceKline(
params: SymbolIntervalFromLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('public/linear/index-price-kline', params);
}
getPremiumIndexKline(
params: SymbolIntervalFromLimitParam
): GenericAPIResponse {
return this.requestWrapper.get('public/linear/premium-index-kline', params);
): Promise<APIResponseWithTime<any[]>> {
return this.get('public/linear/premium-index-kline', params);
}
/**
*
* Market Data : Advanced
*
*/
getOpenInterest(
params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/open-interest', params);
}
getLatestBigDeal(
params: SymbolLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/big-deal', params);
}
getLongShortRatio(
params: SymbolPeriodLimitParam
): Promise<APIResponseWithTime<any[]>> {
return this.get('v2/public/account-ratio', params);
}
/**
*
* Account Data Endpoints
*
*/
getApiKeyInfo(): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/account/api-key');
}
/**
*
* Wallet Data Endpoints
*
*/
getWalletBalance(
params?: Partial<CoinParam>
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/balance', params);
}
getWalletFundRecords(
params?: WalletFundRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/fund/records', params);
}
getWithdrawRecords(
params?: WithdrawRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/wallet/withdraw/list', params);
}
getAssetExchangeRecords(
params?: AssetExchangeRecordsReq
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('v2/private/exchange-order/list', params);
}
/**
*
* API Data Endpoints
*
*/
getServerTime(): Promise<APIResponseWithTime<{}>> {
return this.get('v2/public/time');
}
getApiAnnouncements(): Promise<APIResponseWithTime<any>> {
return this.get('v2/public/announcement');
}
/**
@@ -103,8 +207,8 @@ export class LinearClient extends SharedEndpoints {
close_on_trigger: boolean;
order_link_id?: string;
position_idx?: number;
}): GenericAPIResponse {
return this.requestWrapper.post('private/linear/order/create', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/order/create', params);
}
getActiveOrderList(params: {
@@ -115,20 +219,22 @@ export class LinearClient extends SharedEndpoints {
page?: number;
limit?: number;
order_status?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('private/linear/order/list', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('private/linear/order/list', params);
}
cancelActiveOrder(params: {
symbol: string;
order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('private/linear/order/cancel', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/order/cancel', params);
}
cancelAllActiveOrders(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.post('private/linear/order/cancel-all', params);
cancelAllActiveOrders(
params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/order/cancel-all', params);
}
replaceActiveOrder(params: {
@@ -141,16 +247,16 @@ export class LinearClient extends SharedEndpoints {
stop_loss?: number;
tp_trigger_by?: string;
sl_trigger_by?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('private/linear/order/replace', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/order/replace', params);
}
queryActiveOrder(params: {
order_id?: string;
order_link_id?: string;
symbol: string;
}): GenericAPIResponse {
return this.requestWrapper.get('private/linear/order/search', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('private/linear/order/search', params);
}
/**
@@ -174,8 +280,8 @@ export class LinearClient extends SharedEndpoints {
stop_loss?: number;
tp_trigger_by?: string;
sl_trigger_by?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('private/linear/stop-order/create', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/stop-order/create', params);
}
getConditionalOrder(params: {
@@ -186,23 +292,22 @@ export class LinearClient extends SharedEndpoints {
order?: string;
page?: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get('private/linear/stop-order/list', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('private/linear/stop-order/list', params);
}
cancelConditionalOrder(params: {
symbol: string;
stop_order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.post('private/linear/stop-order/cancel', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/stop-order/cancel', params);
}
cancelAllConditionalOrders(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.post(
'private/linear/stop-order/cancel-all',
params
);
cancelAllConditionalOrders(
params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/stop-order/cancel-all', params);
}
replaceConditionalOrder(params: {
@@ -216,35 +321,34 @@ export class LinearClient extends SharedEndpoints {
stop_loss?: number;
tp_trigger_by?: string;
sl_trigger_by?: string;
}): GenericAPIResponse {
return this.requestWrapper.post(
'private/linear/stop-order/replace',
params
);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/stop-order/replace', params);
}
queryConditionalOrder(params: {
symbol: string;
stop_order_id?: string;
order_link_id?: string;
}): GenericAPIResponse {
return this.requestWrapper.get('private/linear/stop-order/search', params);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('private/linear/stop-order/search', params);
}
/**
* Position
*/
getPosition(params?: Partial<SymbolParam>): GenericAPIResponse {
return this.requestWrapper.get('private/linear/position/list', params);
getPosition(
params?: Partial<SymbolParam>
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('private/linear/position/list', params);
}
setAutoAddMargin(params?: {
symbol: string;
side: string;
auto_add_margin: boolean;
}): GenericAPIResponse {
return this.requestWrapper.post(
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate(
'private/linear/position/set-auto-add-margin',
params
);
@@ -255,11 +359,8 @@ export class LinearClient extends SharedEndpoints {
is_isolated: boolean;
buy_leverage: number;
sell_leverage: number;
}): GenericAPIResponse {
return this.requestWrapper.post(
'private/linear/position/switch-isolated',
params
);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/position/switch-isolated', params);
}
/**
@@ -268,19 +369,8 @@ export class LinearClient extends SharedEndpoints {
setPositionMode(params: {
symbol: string;
mode: typeof linearPositionModeEnum[keyof typeof linearPositionModeEnum];
}): GenericAPIResponse {
return this.requestWrapper.post(
'private/linear/position/switch-mode',
params
);
}
/** @deprecated use setPositionTpSlMode() instead */
setSwitchMode(params?: {
symbol: string;
tp_sl_mode: typeof positionTpSlModeEnum[keyof typeof positionTpSlModeEnum];
}): GenericAPIResponse {
return this.requestWrapper.post('private/linear/tpsl/switch-mode', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/position/switch-mode', params);
}
/**
@@ -290,30 +380,24 @@ export class LinearClient extends SharedEndpoints {
setPositionTpSlMode(params: {
symbol: string;
tp_sl_mode: typeof positionTpSlModeEnum[keyof typeof positionTpSlModeEnum];
}): GenericAPIResponse {
return this.requestWrapper.post('private/linear/tpsl/switch-mode', params);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/tpsl/switch-mode', params);
}
setAddReduceMargin(params?: {
symbol: string;
side: string;
margin: number;
}): GenericAPIResponse {
return this.requestWrapper.post(
'private/linear/position/add-margin',
params
);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/position/add-margin', params);
}
setUserLeverage(params: {
symbol: string;
buy_leverage: number;
sell_leverage: number;
}): GenericAPIResponse {
return this.requestWrapper.post(
'private/linear/position/set-leverage',
params
);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/position/set-leverage', params);
}
setTradingStop(params: {
@@ -326,11 +410,8 @@ export class LinearClient extends SharedEndpoints {
sl_trigger_by?: string;
sl_size?: number;
tp_size?: number;
}): GenericAPIResponse {
return this.requestWrapper.post(
'private/linear/position/trading-stop',
params
);
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/position/trading-stop', params);
}
getTradeRecords(params: {
@@ -340,11 +421,8 @@ export class LinearClient extends SharedEndpoints {
exec_type?: string;
page?: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get(
'private/linear/trade/execution/list',
params
);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('private/linear/trade/execution/list', params);
}
getClosedPnl(params: {
@@ -354,44 +432,37 @@ export class LinearClient extends SharedEndpoints {
exec_type?: string;
page?: number;
limit?: number;
}): GenericAPIResponse {
return this.requestWrapper.get(
'private/linear/trade/closed-pnl/list',
params
);
}): Promise<APIResponseWithTime<any>> {
return this.getPrivate('private/linear/trade/closed-pnl/list', params);
}
/**
* Risk Limit
*/
getRiskLimitList(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get('public/linear/risk-limit', params);
getRiskLimitList(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.getPrivate('public/linear/risk-limit', params);
}
setRiskLimit(params: {
symbol: string;
side: string;
risk_id: string;
}): GenericAPIResponse {
return this.requestWrapper.get('private/linear/position/set-risk', params);
risk_id: number;
}): Promise<APIResponseWithTime<any>> {
return this.postPrivate('private/linear/position/set-risk', params);
}
/**
* Funding
*/
getPredictedFundingFee(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get(
'private/linear/funding/predicted-funding',
params
);
getPredictedFundingFee(
params: SymbolParam
): Promise<APIResponseWithTime<any>> {
return this.getPrivate('private/linear/funding/predicted-funding', params);
}
getLastFundingFee(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get(
'private/linear/funding/prev-funding',
params
);
getLastFundingFee(params: SymbolParam): Promise<APIResponseWithTime<any>> {
return this.getPrivate('private/linear/funding/prev-funding', params);
}
}

View File

@@ -18,5 +18,5 @@ export const DefaultLogger = {
},
error: (...params: LogParams): void => {
console.error(params);
}
};
},
};

View File

@@ -1,111 +0,0 @@
import {
APIResponse,
AssetExchangeRecordsReq,
CoinParam,
SymbolInfo,
SymbolLimitParam,
SymbolParam,
SymbolPeriodLimitParam,
WalletFundRecordsReq,
WithdrawRecordsReq,
} from './types/shared';
import { GenericAPIResponse } from './util/requestUtils';
import RequestWrapper from './util/requestWrapper';
export default class SharedEndpoints {
// TODO: Is there a way to say that Base has to provide this?
protected requestWrapper: RequestWrapper;
/**
*
* Market Data Endpoints
*
*/
getOrderBook(params: SymbolParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/orderBook/L2', params);
}
/**
* Get latest information for symbol
*/
getTickers(params?: Partial<SymbolParam>): GenericAPIResponse {
return this.requestWrapper.get('v2/public/tickers', params);
}
getSymbols(): Promise<APIResponse<SymbolInfo[]>> {
return this.requestWrapper.get('v2/public/symbols');
}
/**
*
* Market Data : Advanced
*
*/
getOpenInterest(params: SymbolPeriodLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/open-interest', params);
}
getLatestBigDeal(params: SymbolLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/big-deal', params);
}
getLongShortRatio(params: SymbolPeriodLimitParam): GenericAPIResponse {
return this.requestWrapper.get('v2/public/account-ratio', params);
}
/**
*
* Account Data Endpoints
*
*/
getApiKeyInfo(): GenericAPIResponse {
return this.requestWrapper.get('v2/private/account/api-key');
}
/**
*
* Wallet Data Endpoints
*
*/
getWalletBalance(params?: Partial<CoinParam>): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/balance', params)
}
getWalletFundRecords(params?: WalletFundRecordsReq): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/fund/records', params);
}
getWithdrawRecords(params: WithdrawRecordsReq): GenericAPIResponse {
return this.requestWrapper.get('v2/private/wallet/withdraw/list', params);
}
getAssetExchangeRecords(params?: AssetExchangeRecordsReq): GenericAPIResponse {
return this.requestWrapper.get('v2/private/exchange-order/list', params);
}
/**
*
* API Data Endpoints
*
*/
getServerTime(): GenericAPIResponse {
return this.requestWrapper.get('v2/public/time');
}
getApiAnnouncements(): GenericAPIResponse {
return this.requestWrapper.get('v2/public/announcement');
}
async getTimeOffset(): Promise<number> {
const start = Date.now();
return this.getServerTime().then(result => {
const end = Date.now();
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
});
}
}

View File

@@ -1,13 +1,21 @@
import { AxiosRequestConfig } from 'axios';
import { KlineInterval } from './types/shared';
import { NewSpotOrder, OrderSide, OrderTypeSpot, SpotOrderQueryById } from './types/spot';
import { APIResponse, KlineInterval } from './types/shared';
import {
NewSpotOrder,
OrderSide,
OrderTypeSpot,
SpotOrderQueryById,
SpotSymbolInfo,
} from './types/spot';
import BaseRestClient from './util/BaseRestClient';
import { GenericAPIResponse, getRestBaseUrl, RestClientOptions } from './util/requestUtils';
import RequestWrapper from './util/requestWrapper';
import {
agentSource,
getRestBaseUrl,
RestClientOptions,
REST_CLIENT_TYPE_ENUM,
} from './util/requestUtils';
export class SpotClient extends BaseRestClient {
protected requestWrapper: RequestWrapper;
/**
* @public Creates an instance of the Spot REST API client.
*
@@ -24,40 +32,49 @@ export class SpotClient extends BaseRestClient {
restClientOptions: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {}
) {
super(key, secret, getRestBaseUrl(useLivenet, restClientOptions), restClientOptions, requestOptions);
super(
key,
secret,
getRestBaseUrl(useLivenet, restClientOptions),
restClientOptions,
requestOptions,
REST_CLIENT_TYPE_ENUM.spot
);
// this.requestWrapper = new RequestWrapper(
// key,
// secret,
// getRestBaseUrl(useLivenet, restClientOptions),
// restClientOptions,
// requestOptions
// );
return this;
}
async getServerTime(urlKeyOverride?: string): Promise<number> {
const result = await this.get('/spot/v1/time');
return result.serverTime;
fetchServerTime(): Promise<number> {
return this.getServerTime();
}
async getServerTime(): Promise<number> {
const res = await this.get('/spot/v1/time');
return res.result.serverTime;
}
/**
*
* Market Data Endpoints
*
**/
**/
getSymbols() {
getSymbols(): Promise<APIResponse<SpotSymbolInfo[]>> {
return this.get('/spot/v1/symbols');
}
getOrderBook(symbol: string, limit?: number) {
getOrderBook(symbol: string, limit?: number): Promise<APIResponse<any>> {
return this.get('/spot/quote/v1/depth', {
symbol, limit
symbol,
limit,
});
}
getMergedOrderBook(symbol: string, scale?: number, limit?: number) {
getMergedOrderBook(
symbol: string,
scale?: number,
limit?: number
): Promise<APIResponse<any>> {
return this.get('/spot/quote/v1/depth/merged', {
symbol,
scale,
@@ -65,14 +82,20 @@ export class SpotClient extends BaseRestClient {
});
}
getTrades(symbol: string, limit?: number) {
return this.get('/spot/v1/trades', {
getTrades(symbol: string, limit?: number): Promise<APIResponse<any[]>> {
return this.get('/spot/quote/v1/trades', {
symbol,
limit,
});
}
getCandles(symbol: string, interval: KlineInterval, limit?: number, startTime?: number, endTime?: number) {
getCandles(
symbol: string,
interval: KlineInterval,
limit?: number,
startTime?: number,
endTime?: number
): Promise<APIResponse<any[]>> {
return this.get('/spot/quote/v1/kline', {
symbol,
interval,
@@ -82,15 +105,15 @@ export class SpotClient extends BaseRestClient {
});
}
get24hrTicker(symbol?: string) {
get24hrTicker(symbol?: string): Promise<APIResponse<any>> {
return this.get('/spot/quote/v1/ticker/24hr', { symbol });
}
getLastTradedPrice(symbol?: string) {
getLastTradedPrice(symbol?: string): Promise<APIResponse<any>> {
return this.get('/spot/quote/v1/ticker/price', { symbol });
}
getBestBidAskPrice(symbol?: string) {
getBestBidAskPrice(symbol?: string): Promise<APIResponse<any>> {
return this.get('/spot/quote/v1/ticker/book_ticker', { symbol });
}
@@ -98,31 +121,40 @@ export class SpotClient extends BaseRestClient {
* Account Data Endpoints
*/
submitOrder(params: NewSpotOrder) {
return this.postPrivate('/spot/v1/order', params);
submitOrder(params: NewSpotOrder): Promise<APIResponse<any>> {
return this.postPrivate('/spot/v1/order', {
...params,
agentSource,
});
}
getOrder(params: SpotOrderQueryById) {
getOrder(params: SpotOrderQueryById): Promise<APIResponse<any>> {
return this.getPrivate('/spot/v1/order', params);
}
cancelOrder(params: SpotOrderQueryById) {
cancelOrder(params: SpotOrderQueryById): Promise<APIResponse<any>> {
return this.deletePrivate('/spot/v1/order', params);
}
cancelOrderBatch(params: {
symbol: string;
side?: OrderSide;
orderTypes: OrderTypeSpot[]
}) {
const orderTypes = params.orderTypes ? params.orderTypes.join(',') : undefined;
orderTypes: OrderTypeSpot[];
}): Promise<APIResponse<any>> {
const orderTypes = params.orderTypes
? params.orderTypes.join(',')
: undefined;
return this.deletePrivate('/spot/order/batch-cancel', {
...params,
orderTypes,
});
}
getOpenOrders(symbol?: string, orderId?: string, limit?: number) {
getOpenOrders(
symbol?: string,
orderId?: string,
limit?: number
): Promise<APIResponse<any>> {
return this.getPrivate('/spot/v1/open-orders', {
symbol,
orderId,
@@ -130,7 +162,11 @@ export class SpotClient extends BaseRestClient {
});
}
getPastOrders(symbol?: string, orderId?: string, limit?: number) {
getPastOrders(
symbol?: string,
orderId?: string,
limit?: number
): Promise<APIResponse<any>> {
return this.getPrivate('/spot/v1/history-orders', {
symbol,
orderId,
@@ -138,7 +174,12 @@ export class SpotClient extends BaseRestClient {
});
}
getMyTrades(symbol?: string, limit?: number, fromId?: number, toId?: number) {
getMyTrades(
symbol?: string,
limit?: number,
fromId?: number,
toId?: number
): Promise<APIResponse<any>> {
return this.getPrivate('/spot/v1/myTrades', {
symbol,
limit,
@@ -151,7 +192,7 @@ export class SpotClient extends BaseRestClient {
* Wallet Data Endpoints
*/
getBalances() {
getBalances(): Promise<APIResponse<any>> {
return this.getPrivate('/spot/v1/account');
}
}

View File

@@ -1,4 +1,5 @@
export type KlineInterval = '1m'
export type KlineInterval =
| '1m'
| '3m'
| '5m'
| '15m'
@@ -16,12 +17,17 @@ export type numberInString = string;
export interface APIResponse<T> {
ret_code: number;
ret_msg: "OK" | string;
ext_code: string;
ext_info: string;
ret_msg: 'OK' | string;
ext_code: string | null;
ext_info: string | null;
result: T;
}
export interface APIResponseWithTime<T> extends APIResponse<T> {
/** UTC timestamp */
time_now: numberInString;
}
/**
* Request Parameter Types
*/

View File

@@ -1,3 +1,5 @@
import { numberInString } from './shared';
export type OrderSide = 'Buy' | 'Sell';
export type OrderTypeSpot = 'LIMIT' | 'MARKET' | 'LIMIT_MAKER';
export type OrderTimeInForce = 'GTC' | 'FOK' | 'IOC';
@@ -16,3 +18,18 @@ export interface SpotOrderQueryById {
orderId?: string;
orderLinkId?: string;
}
export interface SpotSymbolInfo {
name: string;
alias: string;
baseCurrency: string;
quoteCurrency: string;
basePrecision: numberInString;
quotePrecision: numberInString;
minTradeQuantity: numberInString;
minTradeAmount: numberInString;
minPricePrecision: numberInString;
maxTradeQuantity: numberInString;
maxTradeAmount: numberInString;
category: numberInString;
}

View File

@@ -1,7 +1,37 @@
import axios, { AxiosError, AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import { signMessage } from './node-support';
import { RestClientOptions, GenericAPIResponse, getRestBaseUrl, serializeParams, isPublicEndpoint } from './requestUtils';
import {
RestClientOptions,
serializeParams,
RestClientType,
REST_CLIENT_TYPE_ENUM,
agentSource,
} from './requestUtils';
// axios.interceptors.request.use((request) => {
// console.log(new Date(), 'Starting Request', JSON.stringify(request, null, 2));
// return request;
// });
// axios.interceptors.response.use((response) => {
// console.log(new Date(), 'Response:', JSON.stringify(response, null, 2));
// return response;
// });
interface SignedRequestContext {
timestamp: number;
api_key?: string;
recv_window?: number;
// spot is diff from the rest...
recvWindow?: number;
}
interface SignedRequest<T> {
originalParams: T & SignedRequestContext;
paramsWithSign?: T & SignedRequestContext & { sign: string };
sign: string;
}
export default abstract class BaseRestClient {
private timeOffset: number | null;
@@ -11,24 +41,31 @@ export default abstract class BaseRestClient {
private globalRequestOptions: AxiosRequestConfig;
private key: string | undefined;
private secret: string | undefined;
private clientType: RestClientType;
/** Function that calls exchange API to query & resolve server time, used by time sync */
abstract fetchServerTime(): Promise<number>;
constructor(
key: string | undefined,
secret: string | undefined,
baseUrl: string,
options: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {}
requestOptions: AxiosRequestConfig = {},
clientType: RestClientType
) {
this.timeOffset = null;
this.syncTimePromise = null;
this.clientType = clientType;
this.options = {
recv_window: 5000,
// how often to sync time drift with bybit servers
sync_interval_ms: 3600000,
// if true, we'll throw errors if any params are undefined
strict_param_validation: false,
...options
...options,
};
this.globalRequestOptions = {
@@ -37,14 +74,16 @@ export default abstract class BaseRestClient {
// custom request options based on axios specs - see: https://github.com/axios/axios#request-config
...requestOptions,
headers: {
'x-referer': 'bybitapinode'
'x-referer': 'bybitapinode',
},
};
this.baseUrl = baseUrl;
if (key && !secret) {
throw new Error('API Key & Secret are both required for private enpoints')
throw new Error(
'API Key & Secret are both required for private enpoints'
);
}
if (this.options.disable_time_sync !== true) {
@@ -56,62 +95,93 @@ export default abstract class BaseRestClient {
this.secret = secret;
}
get(endpoint: string, params?: any): GenericAPIResponse {
private isSpotClient() {
return this.clientType === REST_CLIENT_TYPE_ENUM.spot;
}
get(endpoint: string, params?: any) {
return this._call('GET', endpoint, params, true);
}
post(endpoint: string, params?: any): GenericAPIResponse {
post(endpoint: string, params?: any) {
return this._call('POST', endpoint, params, true);
}
getPrivate(endpoint: string, params?: any): GenericAPIResponse {
getPrivate(endpoint: string, params?: any) {
return this._call('GET', endpoint, params, false);
}
postPrivate(endpoint: string, params?: any): GenericAPIResponse {
postPrivate(endpoint: string, params?: any) {
return this._call('POST', endpoint, params, false);
}
deletePrivate(endpoint: string, params?: any): GenericAPIResponse {
deletePrivate(endpoint: string, params?: any) {
return this._call('DELETE', endpoint, params, false);
}
private async prepareSignParams(params?: any, isPublicApi?: boolean) {
if (isPublicApi) {
return {
originalParams: params,
paramsWithSign: params,
};
}
if (!this.key || !this.secret) {
throw new Error('Private endpoints require api and private keys set');
}
if (this.timeOffset === null) {
await this.syncTime();
}
return this.signRequest(params);
}
/**
* @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed.
*/
private async _call(method: Method, endpoint: string, params?: any, isPublicApi?: boolean): GenericAPIResponse {
if (!isPublicApi) {
if (!this.key || !this.secret) {
throw new Error('Private endpoints require api and private keys set');
}
if (this.timeOffset === null) {
await this.syncTime();
}
params = await this.signRequest(params);
}
private async _call(
method: Method,
endpoint: string,
params?: any,
isPublicApi?: boolean
): Promise<any> {
const options = {
...this.globalRequestOptions,
url: [this.baseUrl, endpoint].join(endpoint.startsWith('/') ? '' : '/'),
method: method,
json: true
json: true,
};
if (method === 'GET') {
options.params = params;
} else {
options.data = params;
for (const key in params) {
if (typeof params[key] === 'undefined') {
delete params[key];
}
}
return axios(options).then(response => {
if (response.status == 200) {
return response.data;
}
const signResult = await this.prepareSignParams(params, isPublicApi);
throw response;
}).catch(e => this.parseException(e));
if (method === 'GET' || this.isSpotClient()) {
options.params = signResult.paramsWithSign;
if (options.params?.agentSource) {
options.data = {
agentSource: agentSource,
};
}
} else {
options.data = signResult.paramsWithSign;
}
return axios(options)
.then((response) => {
if (response.status == 200) {
return response.data;
}
throw response;
})
.catch((e) => this.parseException(e));
}
/**
@@ -140,37 +210,53 @@ export default abstract class BaseRestClient {
message: response.statusText,
body: response.data,
headers: response.headers,
requestOptions: this.options
requestOptions: this.options,
};
}
/**
* @private sign request and set recv window
*/
async signRequest(data: any): Promise<any> {
const params = {
...data,
api_key: this.key,
timestamp: Date.now() + (this.timeOffset || 0)
private async signRequest<T extends Object>(
data: T & SignedRequestContext
): Promise<SignedRequest<T>> {
const res: SignedRequest<T> = {
originalParams: {
...data,
api_key: this.key,
timestamp: Date.now() + (this.timeOffset || 0),
},
sign: '',
};
// Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen.
if (this.options.recv_window && !params.recv_window) {
params.recv_window = this.options.recv_window;
if (this.options.recv_window && !res.originalParams.recv_window) {
if (this.isSpotClient()) {
res.originalParams.recvWindow = this.options.recv_window;
} else {
res.originalParams.recv_window = this.options.recv_window;
}
}
if (this.key && this.secret) {
const serializedParams = serializeParams(params, this.options.strict_param_validation);
params.sign = await signMessage(serializedParams, this.secret);
const serializedParams = serializeParams(
res.originalParams,
this.options.strict_param_validation
);
res.sign = await signMessage(serializedParams, this.secret);
res.paramsWithSign = {
...res.originalParams,
sign: res.sign,
};
}
return params;
return res;
}
/**
* Trigger time sync and store promise
*/
private syncTime(): GenericAPIResponse {
private syncTime(): Promise<any> {
if (this.options.disable_time_sync === true) {
return Promise.resolve(false);
}
@@ -179,7 +265,7 @@ export default abstract class BaseRestClient {
return this.syncTimePromise;
}
this.syncTimePromise = this.fetchTimeOffset().then(offset => {
this.syncTimePromise = this.fetchTimeOffset().then((offset) => {
this.timeOffset = offset;
this.syncTimePromise = null;
});
@@ -187,22 +273,27 @@ export default abstract class BaseRestClient {
return this.syncTimePromise;
}
abstract getServerTime(baseUrlKeyOverride?: string): Promise<number>;
/**
* Estimate drift based on client<->server latency
*/
async fetchTimeOffset(): Promise<number> {
try {
const start = Date.now();
const serverTime = await this.getServerTime();
const serverTime = await this.fetchServerTime();
if (!serverTime || isNaN(serverTime)) {
throw new Error(
`fetchServerTime() returned non-number: "${serverTime}" typeof(${typeof serverTime})`
);
}
const end = Date.now();
const avgDrift = ((end - start) / 2);
const avgDrift = (end - start) / 2;
return Math.ceil(serverTime - end + avgDrift);
} catch (e) {
console.error('Failed to fetch get time offset: ', e);
return 0;
}
}
};
}

View File

@@ -9,14 +9,13 @@ type WsTopicList = Set<WsTopic>;
interface WsStoredState {
ws?: WebSocket;
connectionState?: WsConnectionState;
activePingTimer?: NodeJS.Timeout | undefined;
activePongTimer?: NodeJS.Timeout | undefined;
activePingTimer?: ReturnType<typeof setTimeout> | undefined;
activePongTimer?: ReturnType<typeof setTimeout> | undefined;
subscribedTopics: WsTopicList;
};
}
export default class WsStore {
private wsState: Record<string, WsStoredState>
private wsState: Record<string, WsStoredState>;
private logger: typeof DefaultLogger;
constructor(logger: typeof DefaultLogger) {
@@ -40,11 +39,14 @@ export default class WsStore {
create(key: string): WsStoredState | undefined {
if (this.hasExistingActiveConnection(key)) {
this.logger.warning('WsStore setConnection() overwriting existing open connection: ', this.getWs(key));
this.logger.warning(
'WsStore setConnection() overwriting existing open connection: ',
this.getWs(key)
);
}
this.wsState[key] = {
subscribedTopics: new Set(),
connectionState: WsConnectionState.READY_STATE_INITIAL
connectionState: WsConnectionState.READY_STATE_INITIAL,
};
return this.get(key);
}
@@ -52,7 +54,10 @@ export default class WsStore {
delete(key: string) {
if (this.hasExistingActiveConnection(key)) {
const ws = this.getWs(key);
this.logger.warning('WsStore deleting state for connection still open: ', ws);
this.logger.warning(
'WsStore deleting state for connection still open: ',
ws
);
ws?.close();
}
delete this.wsState[key];
@@ -70,7 +75,10 @@ export default class WsStore {
setWs(key: string, wsConnection: WebSocket): WebSocket {
if (this.isWsOpen(key)) {
this.logger.warning('WsStore setConnection() overwriting existing open connection: ', this.getWs(key));
this.logger.warning(
'WsStore setConnection() overwriting existing open connection: ',
this.getWs(key)
);
}
this.get(key, true)!.ws = wsConnection;
return wsConnection;
@@ -80,7 +88,10 @@ export default class WsStore {
isWsOpen(key: string): boolean {
const existingConnection = this.getWs(key);
return !!existingConnection && existingConnection.readyState === existingConnection.OPEN;
return (
!!existingConnection &&
existingConnection.readyState === existingConnection.OPEN
);
}
getConnectionState(key: string): WsConnectionState {
@@ -102,7 +113,7 @@ export default class WsStore {
}
getTopicsByKey(): Record<string, WsTopicList> {
const result = {};
const result = {};
for (const refKey in this.wsState) {
result[refKey] = this.getTopics(refKey);
}

View File

@@ -1,18 +1,25 @@
export async function signMessage(message: string, secret: string): Promise<string> {
export async function signMessage(
message: string,
secret: string
): Promise<string> {
const encoder = new TextEncoder();
const key = await window.crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{name: 'HMAC', hash: {name: 'SHA-256'}},
{ name: 'HMAC', hash: { name: 'SHA-256' } },
false,
['sign']
);
const signature = await window.crypto.subtle.sign('HMAC', key, encoder.encode(message));
const signature = await window.crypto.subtle.sign(
'HMAC',
key,
encoder.encode(message)
);
return Array.prototype.map.call(
new Uint8Array(signature),
(x: any) => ('00'+x.toString(16)).slice(-2)
).join('');
};
return Array.prototype.map
.call(new Uint8Array(signature), (x: any) =>
('00' + x.toString(16)).slice(-2)
)
.join('');
}

View File

@@ -1,7 +1,8 @@
import { createHmac } from 'crypto';
export async function signMessage(message: string, secret: string): Promise<string> {
return createHmac('sha256', secret)
.update(message)
.digest('hex');
};
export async function signMessage(
message: string,
secret: string
): Promise<string> {
return createHmac('sha256', secret).update(message).digest('hex');
}

View File

@@ -19,25 +19,31 @@ export interface RestClientOptions {
parse_exceptions?: boolean;
}
export type GenericAPIResponse = Promise<any>;
export function serializeParams(params: object = {}, strict_validation = false): string {
export function serializeParams(
params: object = {},
strict_validation = false
): string {
return Object.keys(params)
.sort()
.map(key => {
.map((key) => {
const value = params[key];
if (strict_validation === true && typeof value === 'undefined') {
throw new Error('Failed to sign API request due to undefined parameter');
throw new Error(
'Failed to sign API request due to undefined parameter'
);
}
return `${key}=${value}`;
})
.join('&');
};
}
export function getRestBaseUrl(useLivenet: boolean, restInverseOptions: RestClientOptions) {
export function getRestBaseUrl(
useLivenet: boolean,
restInverseOptions: RestClientOptions
) {
const baseUrlsInverse = {
livenet: 'https://api.bybit.com',
testnet: 'https://api-testnet.bybit.com'
testnet: 'https://api-testnet.bybit.com',
};
if (restInverseOptions.baseUrl) {
@@ -50,18 +56,25 @@ export function getRestBaseUrl(useLivenet: boolean, restInverseOptions: RestClie
return baseUrlsInverse.testnet;
}
export function isPublicEndpoint (endpoint: string): boolean {
if (endpoint.startsWith('v2/public')) {
return true;
}
if (endpoint.startsWith('public/linear')) {
return true;
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) {
if (response.pong || response.ping) {
return true;
}
return (
@@ -71,3 +84,15 @@ export function isWsPong(response: any) {
response.success === true
);
}
export const agentSource = 'bybitapinode';
export const REST_CLIENT_TYPE_ENUM = {
inverse: 'inverse',
inverseFutures: 'inverseFutures',
linear: 'linear',
spot: 'spot',
} as const;
export type RestClientType =
typeof REST_CLIENT_TYPE_ENUM[keyof typeof REST_CLIENT_TYPE_ENUM];

View File

@@ -1,191 +0,0 @@
import axios, { AxiosRequestConfig, AxiosResponse, Method } from 'axios';
import { signMessage } from './node-support';
import { serializeParams, RestClientOptions, GenericAPIResponse, isPublicEndpoint } from './requestUtils';
export default class RequestUtil {
private timeOffset: number | null;
private syncTimePromise: null | Promise<any>;
private options: RestClientOptions;
private baseUrl: string;
private globalRequestOptions: AxiosRequestConfig;
private key: string | undefined;
private secret: string | undefined;
constructor(
key: string | undefined,
secret: string | undefined,
baseUrl: string,
options: RestClientOptions = {},
requestOptions: AxiosRequestConfig = {}
) {
this.timeOffset = null;
this.syncTimePromise = null;
this.options = {
recv_window: 5000,
// how often to sync time drift with bybit servers
sync_interval_ms: 3600000,
// if true, we'll throw errors if any params are undefined
strict_param_validation: false,
...options
};
this.globalRequestOptions = {
// in ms == 5 minutes by default
timeout: 1000 * 60 * 5,
// custom request options based on axios specs - see: https://github.com/axios/axios#request-config
...requestOptions,
headers: {
'x-referer': 'bybitapinode'
},
};
this.baseUrl = baseUrl;
if (key && !secret) {
throw new Error('API Key & Secret are both required for private enpoints')
}
if (this.options.disable_time_sync !== true) {
this.syncTime();
setInterval(this.syncTime.bind(this), +this.options.sync_interval_ms!);
}
this.key = key;
this.secret = secret;
}
get<T>(endpoint: string, params?: any): Promise<T> {
return this._call('GET', endpoint, params);
}
post<T>(endpoint: string, params?: any): Promise<T> {
return this._call('POST', endpoint, params);
}
/**
* @private Make a HTTP request to a specific endpoint. Private endpoints are automatically signed.
*/
async _call<T>(method: Method, endpoint: string, params?: any): Promise<T> {
if (!isPublicEndpoint(endpoint)) {
if (!this.key || !this.secret) {
throw new Error('Private endpoints require api and private keys set');
}
if (this.timeOffset === null) {
await this.syncTime();
}
params = await this.signRequest(params);
}
const options = {
...this.globalRequestOptions,
url: [this.baseUrl, endpoint].join('/'),
method: method,
json: true
};
if (method === 'GET') {
options.params = params;
} else {
options.data = params;
}
return axios(options).then(response => {
if (response.status == 200) {
return response.data;
}
throw response;
}).catch(e => this.parseException(e));
}
/**
* @private generic handler to parse request exceptions
*/
parseException(e: any): unknown {
if (this.options.parse_exceptions === false) {
throw e;
}
// Something happened in setting up the request that triggered an Error
if (!e.response) {
if (!e.request) {
throw e.message;
}
// request made but no response received
throw e;
}
// The request was made and the server responded with a status code
// that falls out of the range of 2xx
const response: AxiosResponse = e.response;
throw {
code: response.status,
message: response.statusText,
body: response.data,
headers: response.headers,
requestOptions: this.options
};
}
/**
* @private sign request and set recv window
*/
async signRequest(data: any): Promise<any> {
const params = {
...data,
api_key: this.key,
timestamp: Date.now() + (this.timeOffset || 0)
};
// Optional, set to 5000 by default. Increase if timestamp/recv_window errors are seen.
if (this.options.recv_window && !params.recv_window) {
params.recv_window = this.options.recv_window;
}
if (this.key && this.secret) {
const serializedParams = serializeParams(params, this.options.strict_param_validation);
params.sign = await signMessage(serializedParams, this.secret);
}
return params;
}
/**
* @private trigger time sync and store promise
*/
syncTime(): GenericAPIResponse {
if (this.options.disable_time_sync === true) {
return Promise.resolve(false);
}
if (this.syncTimePromise !== null) {
return this.syncTimePromise;
}
this.syncTimePromise = this.getTimeOffset().then(offset => {
this.timeOffset = offset;
this.syncTimePromise = null;
});
return this.syncTimePromise;
}
/**
* @deprecated move this somewhere else, because v2/public/time shouldn't be hardcoded here
*
* @returns {Promise<number>}
* @memberof RequestUtil
*/
async getTimeOffset(): Promise<number> {
const start = Date.now();
const result = await this.get<any>('v2/public/time');
const end = Date.now();
return Math.ceil((result.time_now * 1000) - end + ((end - start) / 2));
}
};

View File

@@ -12,20 +12,20 @@ import WsStore from './util/WsStore';
const inverseEndpoints = {
livenet: 'wss://stream.bybit.com/realtime',
testnet: 'wss://stream-testnet.bybit.com/realtime'
testnet: 'wss://stream-testnet.bybit.com/realtime',
};
const linearEndpoints = {
private: {
livenet: 'wss://stream.bybit.com/realtime_private',
livenet2: 'wss://stream.bytick.com/realtime_private',
testnet: 'wss://stream-testnet.bybit.com/realtime_private'
testnet: 'wss://stream-testnet.bybit.com/realtime_private',
},
public: {
livenet: 'wss://stream.bybit.com/realtime_public',
livenet2: 'wss://stream.bytick.com/realtime_public',
testnet: 'wss://stream-testnet.bybit.com/realtime_public'
}
testnet: 'wss://stream-testnet.bybit.com/realtime_public',
},
};
const spotEndpoints = {
@@ -38,8 +38,8 @@ const spotEndpoints = {
livenet2: 'wss://stream.bybit.com/spot/quote/ws/v2',
testnet: 'wss://stream-testnet.bybit.com/spot/quote/ws/v1',
testnet2: 'wss://stream-testnet.bybit.com/spot/quote/ws/v2',
}
}
},
};
const loggerCategory = { category: 'bybit-ws' };
@@ -54,62 +54,71 @@ export enum WsConnectionState {
READY_STATE_CONNECTING,
READY_STATE_CONNECTED,
READY_STATE_CLOSING,
READY_STATE_RECONNECTING
};
READY_STATE_RECONNECTING,
}
export type APIMarket = 'inverse' | 'linear' | 'spot';
// Same as inverse futures
export type WsPublicInverseTopic = 'orderBookL2_25'
export type WsPublicInverseTopic =
| 'orderBookL2_25'
| 'orderBookL2_200'
| 'trade'
| 'insurance'
| 'instrument_info'
| 'klineV2';
export type WsPublicUSDTPerpTopic = 'orderBookL2_25'
export type WsPublicUSDTPerpTopic =
| 'orderBookL2_25'
| 'orderBookL2_200'
| 'trade'
| 'insurance'
| 'instrument_info'
| 'kline';
export type WsPublicSpotV1Topic = 'trade'
export type WsPublicSpotV1Topic =
| 'trade'
| 'realtimes'
| 'kline'
| 'depth'
| 'mergedDepth'
| 'diffDepth';
export type WsPublicSpotV2Topic = 'depth'
export type WsPublicSpotV2Topic =
| 'depth'
| 'kline'
| 'trade'
| 'bookTicker'
| 'realtimes';
export type WsPublicTopics = WsPublicInverseTopic
export type WsPublicTopics =
| WsPublicInverseTopic
| WsPublicUSDTPerpTopic
| WsPublicSpotV1Topic
| WsPublicSpotV2Topic
| string;
// Same as inverse futures
export type WsPrivateInverseTopic = 'position'
export type WsPrivateInverseTopic =
| 'position'
| 'execution'
| 'order'
| 'stop_order';
export type WsPrivateUSDTPerpTopic = 'position'
export type WsPrivateUSDTPerpTopic =
| 'position'
| 'execution'
| 'order'
| 'stop_order'
| 'wallet';
export type WsPrivateSpotTopic = 'outboundAccountInfo'
export type WsPrivateSpotTopic =
| 'outboundAccountInfo'
| 'executionReport'
| 'ticketInfo';
export type WsPrivateTopic = WsPrivateInverseTopic
export type WsPrivateTopic =
| WsPrivateInverseTopic
| WsPrivateUSDTPerpTopic
| WsPrivateSpotTopic
| string;
@@ -135,7 +144,7 @@ export interface WSClientConfigurableOptions {
restOptions?: any;
requestOptions?: any;
wsUrl?: string;
};
}
export interface WebsocketClientOptions extends WSClientConfigurableOptions {
livenet: boolean;
@@ -147,8 +156,7 @@ export interface WebsocketClientOptions extends WSClientConfigurableOptions {
pongTimeout: number;
pingInterval: number;
reconnectTimeout: number;
};
}
export const wsKeyInverse = 'inverse';
export const wsKeyLinearPrivate = 'linearPrivate';
@@ -157,29 +165,54 @@ export const wsKeySpotPrivate = 'spotPrivate';
export const wsKeySpotPublic = 'spotPublic';
// This is used to differentiate between each of the available websocket streams (as bybit has multiple websockets)
export type WsKey = 'inverse' | 'linearPrivate' | 'linearPublic' | 'spotPrivate' | 'spotPublic';
export type WsKey =
| 'inverse'
| 'linearPrivate'
| 'linearPublic'
| 'spotPrivate'
| 'spotPublic';
const getLinearWsKeyForTopic = (topic: string): WsKey => {
const privateLinearTopics = ['position', 'execution', 'order', 'stop_order', 'wallet'];
const privateLinearTopics = [
'position',
'execution',
'order',
'stop_order',
'wallet',
];
if (privateLinearTopics.includes(topic)) {
return wsKeyLinearPrivate;
}
return wsKeyLinearPublic;
}
};
const getSpotWsKeyForTopic = (topic: string): WsKey => {
const privateLinearTopics = ['position', 'execution', 'order', 'stop_order', 'outboundAccountInfo', 'executionReport', 'ticketInfo'];
const privateLinearTopics = [
'position',
'execution',
'order',
'stop_order',
'outboundAccountInfo',
'executionReport',
'ticketInfo',
];
if (privateLinearTopics.includes(topic)) {
return wsKeySpotPrivate;
}
return wsKeySpotPublic;
}
};
export declare interface WebsocketClient {
on(event: 'open' | 'reconnected', listener: ({ wsKey: WsKey, event: any }) => void): this;
on(event: 'response' | 'update' | 'error', listener: (response: any) => void): this;
on(
event: 'open' | 'reconnected',
listener: ({ wsKey: WsKey, event: any }) => void
): this;
on(
event: 'response' | 'update' | 'error',
listener: (response: any) => void
): this;
on(event: 'reconnect' | 'close', listener: ({ wsKey: WsKey }) => void): this;
}
@@ -196,7 +229,10 @@ export class WebsocketClient extends EventEmitter {
private options: WebsocketClientOptions;
private wsStore: WsStore;
constructor(options: WSClientConfigurableOptions, logger?: typeof DefaultLogger) {
constructor(
options: WSClientConfigurableOptions,
logger?: typeof DefaultLogger
) {
super();
this.logger = logger || DefaultLogger;
@@ -207,7 +243,7 @@ export class WebsocketClient extends EventEmitter {
pongTimeout: 1000,
pingInterval: 10000,
reconnectTimeout: 500,
...options
...options,
};
if (!this.options.market) {
@@ -215,13 +251,31 @@ export class WebsocketClient extends EventEmitter {
}
if (this.isLinear()) {
this.restClient = new LinearClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions);
this.restClient = new LinearClient(
undefined,
undefined,
this.isLivenet(),
this.options.restOptions,
this.options.requestOptions
);
} else if (this.isSpot()) {
// TODO: spot client
this.restClient = new LinearClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions);
this.restClient = new LinearClient(
undefined,
undefined,
this.isLivenet(),
this.options.restOptions,
this.options.requestOptions
);
this.connectPublic();
} else {
this.restClient = new InverseClient(undefined, undefined, this.isLivenet(), this.options.restOptions, this.options.requestOptions);
this.restClient = new InverseClient(
undefined,
undefined,
this.isLivenet(),
this.options.restOptions,
this.options.requestOptions
);
}
}
@@ -246,10 +300,9 @@ export class WebsocketClient extends EventEmitter {
*/
public subscribe(wsTopics: WsTopic[] | WsTopic) {
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach(topic => this.wsStore.addTopic(
this.getWsKeyForTopic(topic),
topic
));
topics.forEach((topic) =>
this.wsStore.addTopic(this.getWsKeyForTopic(topic), topic)
);
// attempt to send subscription topic per websocket
this.wsStore.getKeys().forEach((wsKey: WsKey) => {
@@ -273,10 +326,9 @@ export class WebsocketClient extends EventEmitter {
*/
public unsubscribe(wsTopics: WsTopic[] | WsTopic) {
const topics = Array.isArray(wsTopics) ? wsTopics : [wsTopics];
topics.forEach(topic => this.wsStore.deleteTopic(
this.getWsKeyForTopic(topic),
topic
));
topics.forEach((topic) =>
this.wsStore.deleteTopic(this.getWsKeyForTopic(topic), topic)
);
this.wsStore.getKeys().forEach((wsKey: WsKey) => {
// unsubscribe request only necessary if active connection exists
@@ -303,7 +355,10 @@ export class WebsocketClient extends EventEmitter {
}
if (this.isLinear()) {
return [this.connect(wsKeyLinearPublic), this.connect(wsKeyLinearPrivate)];
return [
this.connect(wsKeyLinearPublic),
this.connect(wsKeyLinearPrivate),
];
}
if (this.isSpot()) {
@@ -342,12 +397,18 @@ export class WebsocketClient extends EventEmitter {
private async connect(wsKey: WsKey): Promise<WebSocket | undefined> {
try {
if (this.wsStore.isWsOpen(wsKey)) {
this.logger.error('Refused to connect to ws with existing active connection', { ...loggerCategory, wsKey })
this.logger.error(
'Refused to connect to ws with existing active connection',
{ ...loggerCategory, wsKey }
);
return this.wsStore.getWs(wsKey);
}
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) {
this.logger.error('Refused to connect to ws, connection attempt already active', { ...loggerCategory, wsKey })
this.logger.error(
'Refused to connect to ws, connection attempt already active',
{ ...loggerCategory, wsKey }
);
return;
}
@@ -377,11 +438,17 @@ export class WebsocketClient extends EventEmitter {
switch (error.message) {
case 'Unexpected server response: 401':
this.logger.error(`${context} due to 401 authorization failure.`, { ...loggerCategory, wsKey });
this.logger.error(`${context} due to 401 authorization failure.`, {
...loggerCategory,
wsKey,
});
break;
default:
this.logger.error(`{context} due to unexpected response error: ${error.msg}`, { ...loggerCategory, wsKey });
this.logger.error(
`{context} due to unexpected response error: ${error.msg}`,
{ ...loggerCategory, wsKey }
);
break;
}
}
@@ -392,23 +459,39 @@ export class WebsocketClient extends EventEmitter {
private async getAuthParams(wsKey: WsKey): Promise<string> {
const { key, secret } = this.options;
if (key && secret && wsKey !== wsKeyLinearPublic && wsKey !== wsKeySpotPublic) {
this.logger.debug('Getting auth\'d request params', { ...loggerCategory, wsKey });
if (
key &&
secret &&
wsKey !== wsKeyLinearPublic &&
wsKey !== wsKeySpotPublic
) {
this.logger.debug("Getting auth'd request params", {
...loggerCategory,
wsKey,
});
const timeOffset = await this.restClient.getTimeOffset();
const timeOffset = await this.restClient.fetchTimeOffset();
const params: any = {
api_key: this.options.key,
expires: (Date.now() + timeOffset + 5000)
expires: Date.now() + timeOffset + 5000,
};
params.signature = await signMessage('GET/realtime' + params.expires, secret);
params.signature = await signMessage(
'GET/realtime' + params.expires,
secret
);
return '?' + serializeParams(params);
} else if (!key || !secret) {
this.logger.warning('Connot authenticate websocket, either api or private keys missing.', { ...loggerCategory, wsKey });
this.logger.warning(
'Connot authenticate websocket, either api or private keys missing.',
{ ...loggerCategory, wsKey }
);
} else {
this.logger.debug('Starting public only websocket client.', { ...loggerCategory, wsKey });
this.logger.debug('Starting public only websocket client.', {
...loggerCategory,
wsKey,
});
}
return '';
@@ -421,7 +504,10 @@ export class WebsocketClient extends EventEmitter {
}
setTimeout(() => {
this.logger.info('Reconnecting to websocket', { ...loggerCategory, wsKey });
this.logger.info('Reconnecting to websocket', {
...loggerCategory,
wsKey,
});
this.connect(wsKey);
}, connectionDelayMs);
}
@@ -433,7 +519,10 @@ export class WebsocketClient extends EventEmitter {
this.tryWsSend(wsKey, JSON.stringify({ op: 'ping' }));
this.wsStore.get(wsKey, true)!.activePongTimer = setTimeout(() => {
this.logger.info('Pong timeout - closing socket to reconnect', { ...loggerCategory, wsKey });
this.logger.info('Pong timeout - closing socket to reconnect', {
...loggerCategory,
wsKey,
});
this.getWs(wsKey)?.close();
}, this.options.pongTimeout);
}
@@ -470,7 +559,7 @@ export class WebsocketClient extends EventEmitter {
}
const wsMessage = JSON.stringify({
op: 'subscribe',
args: topics
args: topics,
});
this.tryWsSend(wsKey, wsMessage);
@@ -485,7 +574,7 @@ export class WebsocketClient extends EventEmitter {
}
const wsMessage = JSON.stringify({
op: 'unsubscribe',
args: topics
args: topics,
});
this.tryWsSend(wsKey, wsMessage);
@@ -493,38 +582,62 @@ export class WebsocketClient extends EventEmitter {
private tryWsSend(wsKey: WsKey, wsMessage: string) {
try {
this.logger.silly(`Sending upstream ws message: `, { ...loggerCategory, wsMessage, wsKey });
this.logger.silly(`Sending upstream ws message: `, {
...loggerCategory,
wsMessage,
wsKey,
});
if (!wsKey) {
throw new Error('Cannot send message due to no known websocket for this wsKey');
throw new Error(
'Cannot send message due to no known websocket for this wsKey'
);
}
const ws = this.getWs(wsKey);
if (!ws) {
throw new Error(`${wsKey} socket not connected yet, call "connect(${wsKey}) first then try again when the "open" event arrives`);
throw new Error(
`${wsKey} socket not connected yet, call "connect(${wsKey}) first then try again when the "open" event arrives`
);
}
ws.send(wsMessage);
} catch (e) {
this.logger.error(`Failed to send WS message`, { ...loggerCategory, wsMessage, wsKey, exception: e });
this.logger.error(`Failed to send WS message`, {
...loggerCategory,
wsMessage,
wsKey,
exception: e,
});
}
}
private connectToWsUrl(url: string, wsKey: WsKey): WebSocket {
this.logger.silly(`Opening WS connection to URL: ${url}`, { ...loggerCategory, wsKey })
this.logger.silly(`Opening WS connection to URL: ${url}`, {
...loggerCategory,
wsKey,
});
const agent = this.options.requestOptions?.agent;
const ws = new WebSocket(url, undefined, agent ? { agent } : undefined);
ws.onopen = event => this.onWsOpen(event, wsKey);
ws.onmessage = event => this.onWsMessage(event, wsKey);
ws.onerror = event => this.onWsError(event, wsKey);
ws.onclose = event => this.onWsClose(event, wsKey);
ws.onopen = (event) => this.onWsOpen(event, wsKey);
ws.onmessage = (event) => this.onWsMessage(event, wsKey);
ws.onerror = (event) => this.onWsError(event, wsKey);
ws.onclose = (event) => this.onWsClose(event, wsKey);
return ws;
}
private onWsOpen(event, wsKey: WsKey) {
if (this.wsStore.isConnectionState(wsKey, READY_STATE_CONNECTING)) {
this.logger.info('Websocket connected', { ...loggerCategory, wsKey, livenet: this.isLivenet(), linear: this.isLinear(), spot: this.isSpot() });
this.logger.info('Websocket connected', {
...loggerCategory,
wsKey,
livenet: this.isLivenet(),
linear: this.isLinear(),
spot: this.isSpot(),
});
this.emit('open', { wsKey, event });
} else if (this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)) {
} else if (
this.wsStore.isConnectionState(wsKey, READY_STATE_RECONNECTING)
) {
this.logger.info('Websocket reconnected', { ...loggerCategory, wsKey });
this.emit('reconnected', { wsKey, event });
}
@@ -547,16 +660,26 @@ export class WebsocketClient extends EventEmitter {
// any message can clear the pong timer - wouldn't get a message if the ws dropped
this.clearPongTimer(wsKey);
const msg = JSON.parse(event && event.data || event);
const msg = JSON.parse((event && event.data) || event);
if ('success' in msg || msg?.pong) {
this.onWsMessageResponse(msg, wsKey);
} else if (msg.topic) {
this.onWsMessageUpdate(msg);
} else {
this.logger.warning('Got unhandled ws message', { ...loggerCategory, message: msg, event, wsKey});
this.logger.warning('Got unhandled ws message', {
...loggerCategory,
message: msg,
event,
wsKey,
});
}
} catch (e) {
this.logger.error('Failed to parse ws event message', { ...loggerCategory, error: e, event, wsKey})
this.logger.error('Failed to parse ws event message', {
...loggerCategory,
error: e,
event,
wsKey,
});
}
}
@@ -568,7 +691,10 @@ export class WebsocketClient extends EventEmitter {
}
private onWsClose(event, wsKey: WsKey) {
this.logger.info('Websocket connection closed', { ...loggerCategory, wsKey});
this.logger.info('Websocket connection closed', {
...loggerCategory,
wsKey,
});
if (this.wsStore.getConnectionState(wsKey) !== READY_STATE_CLOSING) {
this.reconnectWithDelay(wsKey, this.options.reconnectTimeout!);
@@ -606,7 +732,7 @@ export class WebsocketClient extends EventEmitter {
const networkKey = this.isLivenet() ? 'livenet' : 'testnet';
// TODO: reptitive
if (this.isLinear() || wsKey.startsWith('linear')){
if (this.isLinear() || wsKey.startsWith('linear')) {
if (wsKey === wsKeyLinearPublic) {
return linearEndpoints.public[networkKey];
}
@@ -615,11 +741,14 @@ export class WebsocketClient extends EventEmitter {
return linearEndpoints.private[networkKey];
}
this.logger.error('Unhandled linear wsKey: ', { ...loggerCategory, wsKey });
this.logger.error('Unhandled linear wsKey: ', {
...loggerCategory,
wsKey,
});
return linearEndpoints[networkKey];
}
if (this.isSpot() || wsKey.startsWith('spot')){
if (this.isSpot() || wsKey.startsWith('spot')) {
if (wsKey === wsKeySpotPublic) {
return spotEndpoints.public[networkKey];
}
@@ -641,13 +770,15 @@ export class WebsocketClient extends EventEmitter {
return wsKeyInverse;
}
if (this.isLinear()) {
return getLinearWsKeyForTopic(topic)
return getLinearWsKeyForTopic(topic);
}
return getSpotWsKeyForTopic(topic);
}
private wrongMarketError(market: APIMarket) {
return new Error(`This WS client was instanced for the ${this.options.market} market. Make another WebsocketClient instance with "market: '${market}' to listen to spot topics`);
return new Error(
`This WS client was instanced for the ${this.options.market} market. Make another WebsocketClient instance with "market: '${market}' to listen to spot topics`
);
}
// TODO: persistance for subbed topics. Look at ftx-api implementation.
@@ -656,14 +787,17 @@ export class WebsocketClient extends EventEmitter {
throw this.wrongMarketError('spot');
}
return this.tryWsSend(wsKeySpotPublic, JSON.stringify({
topic: 'trade',
event: 'sub',
symbol,
params: {
binary: !!binary,
}
}));
return this.tryWsSend(
wsKeySpotPublic,
JSON.stringify({
topic: 'trade',
event: 'sub',
symbol,
params: {
binary: !!binary,
},
})
);
}
public subscribePublicSpotTradingPair(symbol: string, binary?: boolean) {
@@ -671,35 +805,50 @@ export class WebsocketClient extends EventEmitter {
throw this.wrongMarketError('spot');
}
return this.tryWsSend(wsKeySpotPublic, JSON.stringify({
symbol,
topic: 'realtimes',
event: 'sub',
params: {
binary: !!binary,
},
}));
return this.tryWsSend(
wsKeySpotPublic,
JSON.stringify({
symbol,
topic: 'realtimes',
event: 'sub',
params: {
binary: !!binary,
},
})
);
}
public subscribePublicSpotV1Kline(symbol: string, candleSize: KlineInterval, binary?: boolean) {
public subscribePublicSpotV1Kline(
symbol: string,
candleSize: KlineInterval,
binary?: boolean
) {
if (!this.isSpot()) {
throw this.wrongMarketError('spot');
}
return this.tryWsSend(wsKeySpotPublic, JSON.stringify({
symbol,
topic: 'kline_' + candleSize,
event: 'sub',
params: {
binary: !!binary,
},
}));
return this.tryWsSend(
wsKeySpotPublic,
JSON.stringify({
symbol,
topic: 'kline_' + candleSize,
event: 'sub',
params: {
binary: !!binary,
},
})
);
}
//ws.send('{"symbol":"BTCUSDT","topic":"depth","event":"sub","params":{"binary":false}}');
//ws.send('{"symbol":"BTCUSDT","topic":"mergedDepth","event":"sub","params":{"binary":false,"dumpScale":1}}');
//ws.send('{"symbol":"BTCUSDT","topic":"diffDepth","event":"sub","params":{"binary":false}}');
public subscribePublicSpotOrderbook(symbol: string, depth: 'full' | 'merge' | 'delta', dumpScale?: number, binary?: boolean) {
public subscribePublicSpotOrderbook(
symbol: string,
depth: 'full' | 'merge' | 'delta',
dumpScale?: number,
binary?: boolean
) {
if (!this.isSpot()) {
throw this.wrongMarketError('spot');
}
@@ -709,7 +858,7 @@ export class WebsocketClient extends EventEmitter {
case 'full': {
topic = 'depth';
break;
};
}
case 'merge': {
topic = 'mergedDepth';
if (!dumpScale) {
@@ -736,5 +885,4 @@ export class WebsocketClient extends EventEmitter {
}
return this.tryWsSend(wsKeySpotPublic, JSON.stringify(msg));
}
};
}

View File

@@ -0,0 +1,105 @@
import { InverseFuturesClient } from '../../src/inverse-futures-client';
import { successResponseList, successResponseObject } from '../response.util';
describe('Public Inverse-Futures REST API GET Endpoints', () => {
const useLivenet = true;
const API_KEY = process.env.API_KEY_COM;
const API_SECRET = process.env.API_SECRET_COM;
const api = new InverseFuturesClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
// Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol!
const symbol = 'BTCUSDU22';
it('getApiKeyInfo()', async () => {
expect(await api.getApiKeyInfo()).toMatchObject(successResponseObject());
});
it('getWalletBalance()', async () => {
expect(await api.getWalletBalance()).toMatchObject(successResponseObject());
});
it('getWalletFundRecords()', async () => {
expect(await api.getWalletFundRecords()).toMatchObject(
successResponseObject()
);
});
it('getWithdrawRecords()', async () => {
expect(await api.getWithdrawRecords()).toMatchObject(
successResponseObject()
);
});
it('getAssetExchangeRecords()', async () => {
expect(await api.getAssetExchangeRecords()).toMatchObject(
successResponseList()
);
});
it('getActiveOrderList()', async () => {
expect(await api.getActiveOrderList({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryActiveOrder()', async () => {
expect(await api.queryActiveOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getConditionalOrder()', async () => {
expect(await api.getConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryConditionalOrder()', async () => {
expect(await api.queryConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getPosition()', async () => {
expect(await api.getPosition()).toMatchObject(successResponseObject());
});
it('getTradeRecords()', async () => {
expect(await api.getTradeRecords({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getClosedPnl()', async () => {
expect(await api.getClosedPnl({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getRiskLimitList()', async () => {
expect(await api.getRiskLimitList()).toMatchObject(
successResponseList('ok')
);
});
it('getMyLastFundingFee()', async () => {
expect(await api.getMyLastFundingFee({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getPredictedFunding()', async () => {
expect(await api.getPredictedFunding({ symbol: 'BTCUSD' })).toMatchObject(
successResponseObject()
);
});
it('getLcpInfo()', async () => {
expect(await api.getLcpInfo({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
});

View File

@@ -0,0 +1,198 @@
import { API_ERROR_CODE, InverseFuturesClient } from '../../src';
import { successResponseObject } from '../response.util';
describe('Private Inverse-Futures REST API POST 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 InverseFuturesClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
// Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol!
const symbol = 'BTCUSDU22';
// These tests are primarily check auth is working by expecting balance or order not found style errors
it('placeActiveOrder()', async () => {
expect(
await api.placeActiveOrder({
side: 'Buy',
symbol,
order_type: 'Limit',
price: 30000,
qty: 1,
time_in_force: 'GoodTillCancel',
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_IDX_NOT_MATCH_POSITION_MODE,
ret_msg: 'position idx not match position mode',
});
});
it('cancelActiveOrder()', async () => {
expect(
await api.cancelActiveOrder({
symbol,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllActiveOrders()', async () => {
expect(
await api.cancelAllActiveOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceActiveOrder()', async () => {
expect(
await api.replaceActiveOrder({
symbol,
order_id: '123123123',
p_r_qty: '1',
p_r_price: '30000',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to replace',
});
});
it('placeConditionalOrder()', async () => {
expect(
await api.placeConditionalOrder({
order_type: 'Limit',
side: 'Buy',
symbol,
qty: '1',
price: '8100',
base_price: '8300',
stop_px: '8150',
time_in_force: 'GoodTillCancel',
order_link_id: 'cus_order_id_1',
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_IDX_NOT_MATCH_POSITION_MODE,
ret_msg: 'position idx not match position mode',
});
});
it('cancelConditionalOrder()', async () => {
expect(
await api.cancelConditionalOrder({
symbol,
order_link_id: 'lkasmdflasd',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllConditionalOrders()', async () => {
expect(
await api.cancelAllConditionalOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceConditionalOrder()', async () => {
expect(
await api.replaceConditionalOrder({
symbol,
p_r_price: '50000',
p_r_qty: 1,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to replace',
});
});
it('changePositionMargin()', async () => {
expect(
await api.changePositionMargin({
symbol,
margin: '10',
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_IDX_NOT_MATCH_POSITION_MODE,
ret_msg: 'position idx not match position mode',
});
});
it('setTradingStop()', async () => {
expect(
await api.setTradingStop({
symbol,
take_profit: 50000,
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_STATUS_NOT_NORMAL,
ret_msg: 'position status is not normal',
});
});
it('setUserLeverage()', async () => {
expect(
await api.setUserLeverage({
symbol,
buy_leverage: 5,
sell_leverage: 5,
})
).toMatchObject({
ret_code: API_ERROR_CODE.LEVERAGE_NOT_MODIFIED,
ret_msg: 'leverage not modified',
});
});
it('setPositionMode()', async () => {
expect(
await api.setPositionMode({
symbol,
mode: 3,
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_MODE_NOT_MODIFIED,
ret_msg: 'position mode not modified',
});
});
it('setMarginType()', async () => {
expect(
await api.setMarginType({
symbol,
is_isolated: false,
buy_leverage: 5,
sell_leverage: 5,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ISOLATED_NOT_MODIFIED,
ret_msg: 'Isolated not modified',
});
});
it('setRiskLimit()', async () => {
expect(
await api.setRiskLimit({
symbol,
risk_id: 'myriskid',
})
).toMatchObject({
ret_code: -1,
ret_msg: `Currently not support symbol[${symbol}]`,
});
});
});

View File

@@ -1,66 +1,93 @@
import { InverseFuturesClient } from "../../src/inverse-futures-client";
import { notAuthenticatedError, successResponseList, successResponseObject } from "../response.util";
import { InverseFuturesClient } from '../../src/inverse-futures-client';
import {
notAuthenticatedError,
successResponseList,
successResponseObject,
} from '../response.util';
describe('Public Inverse Futures REST API Endpoints', () => {
const useLivenet = true;
const api = new InverseFuturesClient(undefined, undefined, useLivenet, { disable_time_sync: true });
const api = new InverseFuturesClient(undefined, undefined, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSD';
const interval = '15';
const timestampOneHourAgo = (new Date().getTime() / 1000) - (1000 * 60 * 60);
const timestampOneHourAgo = new Date().getTime() / 1000 - 1000 * 60 * 60;
const from = Number(timestampOneHourAgo.toFixed(0));
describe('Inverse-Futures only endpoints', () => {
it('should throw for unauthenticated private calls', async () => {
expect(() => api.getPosition()).rejects.toMatchObject(notAuthenticatedError());
});
it('getKline()', async () => {
expect(
await api.getKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(await api.getIndexPriceKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(await api.getPremiumIndexKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(successResponseObject());
});
});
describe('Shared endpoints', () => {
it('should throw for unauthenticated private calls', async () => {
expect(() => api.getApiKeyInfo()).rejects.toMatchObject(notAuthenticatedError());
expect(() => api.getPosition()).rejects.toMatchObject(
notAuthenticatedError()
);
expect(() => api.getApiKeyInfo()).rejects.toMatchObject(
notAuthenticatedError()
);
});
it('getOrderBook()', async () => {
expect(await api.getOrderBook({ symbol })).toMatchObject(successResponseList());
expect(await api.getOrderBook({ symbol })).toMatchObject(
successResponseList()
);
});
it('getKline()', async () => {
expect(await api.getKline({ symbol, interval, from })).toMatchObject(
successResponseList()
);
});
it('getTickers()', async () => {
expect(await api.getTickers()).toMatchObject(successResponseList());
});
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(
successResponseList()
);
});
it('getSymbols()', async () => {
expect(await api.getSymbols()).toMatchObject(successResponseList());
});
it('getMarkPriceKline()', async () => {
expect(
await api.getMarkPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(
await api.getIndexPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(
await api.getPremiumIndexKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(
successResponseObject()
);
});
it('getServerTime()', async () => {
expect(await api.getServerTime()).toMatchObject(successResponseObject());
});
it('fetchServertime() returns number', async () => {
expect(await api.fetchServerTime()).toStrictEqual(expect.any(Number));
});
it('getApiAnnouncements()', async () => {
expect(await api.getApiAnnouncements()).toMatchObject(successResponseList());
expect(await api.getApiAnnouncements()).toMatchObject(
successResponseList()
);
});
});
});

View File

@@ -0,0 +1,103 @@
import { InverseClient } from '../../src/inverse-client';
import { successResponseList, successResponseObject } from '../response.util';
describe('Private Inverse 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 InverseClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSD';
it('getApiKeyInfo()', async () => {
expect(await api.getApiKeyInfo()).toMatchObject(successResponseObject());
});
it('getWalletBalance()', async () => {
expect(await api.getWalletBalance()).toMatchObject(successResponseObject());
});
it('getWalletFundRecords()', async () => {
expect(await api.getWalletFundRecords()).toMatchObject(
successResponseObject()
);
});
it('getWithdrawRecords()', async () => {
expect(await api.getWithdrawRecords()).toMatchObject(
successResponseObject()
);
});
it('getAssetExchangeRecords()', async () => {
expect(await api.getAssetExchangeRecords()).toMatchObject(
successResponseList()
);
});
it('getActiveOrderList()', async () => {
expect(await api.getActiveOrderList({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryActiveOrder()', async () => {
expect(await api.queryActiveOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getConditionalOrder()', async () => {
expect(await api.getConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryConditionalOrder()', async () => {
expect(await api.queryConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getPosition()', async () => {
expect(await api.getPosition()).toMatchObject(successResponseObject());
});
it('getTradeRecords()', async () => {
expect(await api.getTradeRecords({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getRiskLimitList()', async () => {
expect(await api.getRiskLimitList()).toMatchObject(
successResponseList('ok')
);
});
it('getClosedPnl()', async () => {
expect(await api.getClosedPnl({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getMyLastFundingFee()', async () => {
expect(await api.getMyLastFundingFee({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getLcpInfo()', async () => {
expect(await api.getLcpInfo({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
});

View File

@@ -0,0 +1,193 @@
import { API_ERROR_CODE } from '../../src';
import { InverseClient } from '../../src/inverse-client';
import { successResponseObject } from '../response.util';
describe('Private Inverse 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 InverseClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSD';
// These tests are primarily check auth is working by expecting balance or order not found style errors
it('placeActiveOrder()', async () => {
expect(
await api.placeActiveOrder({
side: 'Buy',
symbol,
order_type: 'Limit',
price: 30000,
qty: 1,
time_in_force: 'GoodTillCancel',
})
).toMatchObject({
ret_code: API_ERROR_CODE.INSUFFICIENT_BALANCE_FOR_ORDER_COST,
});
});
it('cancelActiveOrder()', async () => {
expect(
await api.cancelActiveOrder({
symbol,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllActiveOrders()', async () => {
expect(
await api.cancelAllActiveOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceActiveOrder()', async () => {
expect(
await api.replaceActiveOrder({
symbol,
order_id: '123123123',
p_r_qty: 1,
p_r_price: '30000',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to replace',
});
});
it('placeConditionalOrder()', async () => {
expect(
await api.placeConditionalOrder({
order_type: 'Limit',
side: 'Buy',
symbol,
qty: '1',
price: '8100',
base_price: '8300',
stop_px: '8150',
time_in_force: 'GoodTillCancel',
order_link_id: 'cus_order_id_1',
})
).toMatchObject({
ret_code: API_ERROR_CODE.INSUFFICIENT_BALANCE,
ret_msg: 'Insufficient wallet balance',
});
});
it('cancelConditionalOrder()', async () => {
expect(
await api.cancelConditionalOrder({
symbol,
order_link_id: 'lkasmdflasd',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllConditionalOrders()', async () => {
expect(
await api.cancelAllConditionalOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceConditionalOrder()', async () => {
expect(
await api.replaceConditionalOrder({
symbol,
p_r_price: '50000',
p_r_qty: 1,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to replace',
});
});
it('changePositionMargin()', async () => {
expect(
await api.changePositionMargin({
symbol,
margin: '10',
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_IS_CROSS_MARGIN,
ret_msg: 'position is in crossMargin',
});
});
it('setTradingStop()', async () => {
expect(
await api.setTradingStop({
symbol,
take_profit: 5555,
})
).toMatchObject({
ret_code: API_ERROR_CODE.CANNOT_SET_TRADING_STOP_FOR_ZERO_POS,
ret_msg: 'can not set tp/sl/ts for zero position',
});
});
it('setUserLeverage()', async () => {
expect(
await api.setUserLeverage({
symbol,
leverage: 5,
})
).toMatchObject({
result: 5,
ret_code: 0,
});
});
it('setSlTpPositionMode()', async () => {
expect(
await api.setSlTpPositionMode({
symbol,
tp_sl_mode: 'Full',
})
).toMatchObject({
ret_code: API_ERROR_CODE.SAME_SLTP_MODE,
ret_msg: 'same tp sl mode2',
});
});
it('setMarginType()', async () => {
expect(
await api.setMarginType({
symbol,
is_isolated: false,
buy_leverage: 5,
sell_leverage: 5,
})
).toMatchObject(successResponseObject());
});
it('setRiskLimit()', async () => {
expect(
await api.setRiskLimit({
symbol,
risk_id: 'myriskid',
})
).toMatchObject({
ret_code: API_ERROR_CODE.RISK_LIMIT_NOT_EXISTS,
ret_msg: 'risk limit not exists',
});
});
});

View File

@@ -1,66 +1,93 @@
import { InverseClient } from "../../src/inverse-client";
import { notAuthenticatedError, successResponseList, successResponseObject } from "../response.util";
import { InverseClient } from '../../src/inverse-client';
import {
notAuthenticatedError,
successResponseList,
successResponseObject,
} from '../response.util';
describe('Public Inverse REST API Endpoints', () => {
const useLivenet = true;
const api = new InverseClient(undefined, undefined, useLivenet, { disable_time_sync: true });
const api = new InverseClient(undefined, undefined, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSD';
const interval = '15';
const timestampOneHourAgo = (new Date().getTime() / 1000) - (1000 * 60 * 60);
const timestampOneHourAgo = new Date().getTime() / 1000 - 1000 * 60 * 60;
const from = Number(timestampOneHourAgo.toFixed(0));
describe('Inverse only endpoints', () => {
it('should throw for unauthenticated private calls', async () => {
expect(() => api.getPosition()).rejects.toMatchObject(notAuthenticatedError());
});
it('getKline()', async () => {
expect(
await api.getKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(await api.getIndexPriceKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(await api.getPremiumIndexKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(successResponseObject());
});
});
describe('Shared endpoints', () => {
it('should throw for unauthenticated private calls', async () => {
expect(() => api.getApiKeyInfo()).rejects.toMatchObject(notAuthenticatedError());
expect(() => api.getPosition()).rejects.toMatchObject(
notAuthenticatedError()
);
expect(() => api.getApiKeyInfo()).rejects.toMatchObject(
notAuthenticatedError()
);
});
it('getOrderBook()', async () => {
expect(await api.getOrderBook({ symbol })).toMatchObject(successResponseList());
expect(await api.getOrderBook({ symbol })).toMatchObject(
successResponseList()
);
});
it('getKline()', async () => {
expect(await api.getKline({ symbol, interval, from })).toMatchObject(
successResponseList()
);
});
it('getTickers()', async () => {
expect(await api.getTickers()).toMatchObject(successResponseList());
});
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(
successResponseList()
);
});
it('getSymbols()', async () => {
expect(await api.getSymbols()).toMatchObject(successResponseList());
});
it('getMarkPriceKline()', async () => {
expect(
await api.getMarkPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(
await api.getIndexPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(
await api.getPremiumIndexKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(
successResponseObject()
);
});
it('getServerTime()', async () => {
expect(await api.getServerTime()).toMatchObject(successResponseObject());
});
it('fetchServertime() returns number', async () => {
expect(await api.fetchServerTime()).toStrictEqual(expect.any(Number));
});
it('getApiAnnouncements()', async () => {
expect(await api.getApiAnnouncements()).toMatchObject(successResponseList());
expect(await api.getApiAnnouncements()).toMatchObject(
successResponseList()
);
});
});
});

View File

@@ -0,0 +1,103 @@
import { LinearClient } from '../../src/linear-client';
import { successResponseList, successResponseObject } from '../response.util';
describe('Public Linear REST API GET 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 LinearClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSDT';
it('getApiKeyInfo()', async () => {
expect(await api.getApiKeyInfo()).toMatchObject(successResponseObject());
});
it('getWalletBalance()', async () => {
expect(await api.getWalletBalance()).toMatchObject(successResponseObject());
});
it('getWalletFundRecords()', async () => {
expect(await api.getWalletFundRecords()).toMatchObject(
successResponseObject()
);
});
it('getWithdrawRecords()', async () => {
expect(await api.getWithdrawRecords()).toMatchObject(
successResponseObject()
);
});
it('getAssetExchangeRecords()', async () => {
expect(await api.getAssetExchangeRecords()).toMatchObject(
successResponseList()
);
});
it('getActiveOrderList()', async () => {
expect(await api.getActiveOrderList({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryActiveOrder()', async () => {
expect(await api.queryActiveOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getConditionalOrder()', async () => {
expect(await api.getConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('queryConditionalOrder()', async () => {
expect(await api.queryConditionalOrder({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getPosition()', async () => {
expect(await api.getPosition()).toMatchObject(successResponseObject());
});
it('getTradeRecords()', async () => {
expect(await api.getTradeRecords({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getClosedPnl()', async () => {
expect(await api.getClosedPnl({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getRiskLimitList()', async () => {
expect(await api.getRiskLimitList({ symbol: symbol })).toMatchObject(
successResponseList()
);
});
it('getPredictedFundingFee()', async () => {
expect(await api.getPredictedFundingFee({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
it('getLastFundingFee()', async () => {
expect(await api.getLastFundingFee({ symbol: symbol })).toMatchObject(
successResponseObject()
);
});
});

View File

@@ -0,0 +1,230 @@
import { API_ERROR_CODE, LinearClient } from '../../src';
import { successResponseObject } from '../response.util';
describe('Private Inverse-Futures REST API POST 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 LinearClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
// Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol!
const symbol = 'BTCUSDT';
// These tests are primarily check auth is working by expecting balance or order not found style errors
it('placeActiveOrder()', async () => {
expect(
await api.placeActiveOrder({
side: 'Buy',
symbol,
order_type: 'Limit',
price: 20000,
qty: 1,
time_in_force: 'GoodTillCancel',
reduce_only: false,
close_on_trigger: false,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_COST_NOT_AVAILABLE,
});
});
it('cancelActiveOrder()', async () => {
expect(
await api.cancelActiveOrder({
symbol,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllActiveOrders()', async () => {
expect(
await api.cancelAllActiveOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceActiveOrder()', async () => {
expect(
await api.replaceActiveOrder({
symbol,
order_id: '123123123',
p_r_qty: 1,
p_r_price: 30000,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE,
ret_msg: 'order not exists or too late to replace',
});
});
it('placeConditionalOrder()', async () => {
expect(
await api.placeConditionalOrder({
order_type: 'Limit',
side: 'Buy',
symbol,
qty: 1,
price: 8100,
base_price: 8300,
stop_px: 8150,
time_in_force: 'GoodTillCancel',
order_link_id: 'cus_order_id_1',
reduce_only: false,
trigger_by: 'LastPrice',
})
).toMatchObject({
ret_code: API_ERROR_CODE.INSUFFICIENT_BALANCE_FOR_ORDER_COST_LINEAR,
ret_msg: 'Insufficient wallet balance',
});
});
it('cancelConditionalOrder()', async () => {
expect(
await api.cancelConditionalOrder({
symbol,
order_link_id: 'lkasmdflasd',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE_LINEAR,
ret_msg: 'order not exists or too late to cancel',
});
});
it('cancelAllConditionalOrders()', async () => {
expect(
await api.cancelAllConditionalOrders({
symbol,
})
).toMatchObject(successResponseObject());
});
it('replaceConditionalOrder()', async () => {
expect(
await api.replaceConditionalOrder({
symbol,
p_r_price: 50000,
p_r_qty: 1,
order_link_id: 'someorderid',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE_LINEAR,
ret_msg: 'order not exists or too late to replace',
});
});
it('setAutoAddMargin()', async () => {
expect(
await api.setAutoAddMargin({
symbol,
side: 'Buy',
auto_add_margin: true,
})
).toMatchObject({
ret_code: API_ERROR_CODE.AUTO_ADD_MARGIN_NOT_MODIFIED,
ret_msg: 'autoAddMargin not modified',
});
});
it('setMarginSwitch()', async () => {
expect(
await api.setMarginSwitch({
symbol,
is_isolated: true,
buy_leverage: 5,
sell_leverage: 5,
})
).toMatchObject({
ret_code: API_ERROR_CODE.ISOLATED_NOT_MODIFIED_LINEAR,
ret_msg: 'Isolated not modified',
});
});
it('setPositionMode()', async () => {
expect(
await api.setPositionMode({
symbol,
mode: 'BothSide',
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_MODE_NOT_MODIFIED,
ret_msg: 'position mode not modified',
});
});
it('setPositionTpSlMode()', async () => {
expect(
await api.setPositionTpSlMode({
symbol,
tp_sl_mode: 'Full',
})
).toMatchObject({
ret_code: API_ERROR_CODE.SAME_SLTP_MODE_LINEAR,
ret_msg: 'same tp sl mode2',
});
});
it('setAddReduceMargin()', async () => {
expect(
await api.setAddReduceMargin({
symbol,
side: 'Buy',
margin: 5,
})
).toMatchObject({
ret_code: API_ERROR_CODE.POSITION_SIZE_IS_ZERO,
ret_msg: 'position size is zero',
});
});
it('setUserLeverage()', async () => {
expect(
await api.setUserLeverage({
symbol,
buy_leverage: 5,
sell_leverage: 5,
})
).toMatchObject({
ret_code: API_ERROR_CODE.LEVERAGE_NOT_MODIFIED,
ret_msg: 'leverage not modified',
});
});
it('setTradingStop()', async () => {
expect(
await api.setTradingStop({
symbol,
side: 'Buy',
take_profit: 555,
})
).toMatchObject({
ret_code: API_ERROR_CODE.CANNOT_SET_LINEAR_TRADING_STOP_FOR_ZERO_POS,
ret_msg: 'can not set tp/sl/ts for zero position',
});
});
it('setRiskLimit()', async () => {
expect(
await api.setRiskLimit({
symbol,
side: 'Buy',
risk_id: 2,
})
).toMatchObject({
ret_code: API_ERROR_CODE.RISK_ID_NOT_MODIFIED,
ret_msg: 'risk id not modified',
});
});
});

View File

@@ -1,66 +1,92 @@
import { LinearClient } from "../../src/linear-client";
import { notAuthenticatedError, successResponseList, successResponseObject } from "../response.util";
import { LinearClient } from '../../src/linear-client';
import {
notAuthenticatedError,
successResponseList,
successResponseObject,
} from '../response.util';
describe('Public Linear REST API Endpoints', () => {
const useLivenet = true;
const api = new LinearClient(undefined, undefined, useLivenet, { disable_time_sync: true });
const api = new LinearClient(undefined, undefined, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSDT';
const interval = '15';
const timestampOneHourAgo = (new Date().getTime() / 1000) - (1000 * 60 * 60);
const timestampOneHourAgo = new Date().getTime() / 1000 - 1000 * 60 * 60;
const from = Number(timestampOneHourAgo.toFixed(0));
describe('Linear only endpoints', () => {
it('should throw for unauthenticated private calls', async () => {
expect(() => api.getPosition()).rejects.toMatchObject(notAuthenticatedError());
});
it('getKline()', async () => {
expect(
await api.getKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(await api.getIndexPriceKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(await api.getPremiumIndexKline({ symbol, interval, from })).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(successResponseObject());
});
});
describe('Shared endpoints', () => {
it('should throw for unauthenticated private calls', async () => {
expect(() => api.getApiKeyInfo()).rejects.toMatchObject(notAuthenticatedError());
expect(() => api.getPosition()).rejects.toMatchObject(
notAuthenticatedError()
);
expect(() => api.getApiKeyInfo()).rejects.toMatchObject(
notAuthenticatedError()
);
});
it('getOrderBook()', async () => {
expect(await api.getOrderBook({ symbol })).toMatchObject(successResponseList());
expect(await api.getOrderBook({ symbol })).toMatchObject(
successResponseList()
);
});
it('getKline()', async () => {
expect(await api.getKline({ symbol, interval, from })).toMatchObject(
successResponseList()
);
});
it('getTickers()', async () => {
expect(await api.getTickers()).toMatchObject(successResponseList());
});
it('getTrades()', async () => {
expect(await api.getTrades({ symbol })).toMatchObject(
successResponseList()
);
});
it('getSymbols()', async () => {
expect(await api.getSymbols()).toMatchObject(successResponseList());
});
it('getMarkPriceKline()', async () => {
expect(
await api.getMarkPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getIndexPriceKline()', async () => {
expect(
await api.getIndexPriceKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getPremiumIndexKline()', async () => {
expect(
await api.getPremiumIndexKline({ symbol, interval, from })
).toMatchObject(successResponseList());
});
it('getLastFundingRate()', async () => {
expect(await api.getLastFundingRate({ symbol })).toMatchObject(
successResponseObject()
);
});
it('getServerTime()', async () => {
expect(await api.getServerTime()).toMatchObject(successResponseObject());
});
it('fetchServertime() returns number', async () => {
expect(await api.fetchServerTime()).toStrictEqual(expect.any(Number));
});
it('getApiAnnouncements()', async () => {
expect(await api.getApiAnnouncements()).toMatchObject(successResponseList());
expect(await api.getApiAnnouncements()).toMatchObject(
successResponseList()
);
});
});
});

View File

@@ -1,26 +1,31 @@
export function successResponseList() {
export function successResponseList(successMsg: string | null = 'OK') {
return {
"ext_code": "",
"ext_info": "",
"result": expect.any(Array),
"ret_code": 0,
"ret_msg": "OK",
"time_now": expect.any(String),
result: expect.any(Array),
ret_code: 0,
ret_msg: successMsg,
};
};
}
export function successResponseObject() {
export function successResponseObject(successMsg: string | null = 'OK') {
return {
"ext_code": "",
"ext_info": "",
"result": expect.any(Object),
"ret_code": 0,
"ret_msg": "OK",
"time_now": expect.any(String),
result: expect.any(Object),
ret_code: 0,
ret_msg: successMsg,
};
};
}
export function errorResponseObject(
result: null | any = null,
ret_code: number,
ret_msg: string
) {
return {
result,
ret_code,
ret_msg,
};
}
export function notAuthenticatedError() {
return new Error('Private endpoints require api and private keys set');
};
}

View File

@@ -0,0 +1,54 @@
import { SpotClient } from '../../src';
import {
errorResponseObject,
notAuthenticatedError,
successResponseList,
successResponseObject,
} from '../response.util';
describe('Private Spot 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 SpotClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSDT';
const interval = '15m';
it('getOrder()', async () => {
// No auth error == test pass
expect(await api.getOrder({ orderId: '123123' })).toMatchObject(
errorResponseObject(null, -2013, 'Order does not exist.')
);
});
it('getOpenOrders()', async () => {
expect(await api.getOpenOrders()).toMatchObject(successResponseList(''));
});
it('getPastOrders()', async () => {
expect(await api.getPastOrders()).toMatchObject(successResponseList(''));
});
it('getMyTrades()', async () => {
expect(await api.getMyTrades()).toMatchObject(successResponseList(''));
});
it('getBalances()', async () => {
expect(await api.getBalances()).toMatchObject({
result: {
balances: expect.any(Array),
},
ret_code: 0,
ret_msg: '',
});
});
});

View File

@@ -0,0 +1,56 @@
import { API_ERROR_CODE, SpotClient } from '../../src';
import { successResponseObject } from '../response.util';
describe('Private Inverse-Futures REST API POST 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 SpotClient(API_KEY, API_SECRET, useLivenet, {
disable_time_sync: true,
});
// Warning: if some of these start to fail with 10001 params error, it's probably that this future expired and a newer one exists with a different symbol!
const symbol = 'BTCUSDT';
// These tests are primarily check auth is working by expecting balance or order not found style errors
it('submitOrder()', async () => {
expect(
await api.submitOrder({
side: 'Buy',
symbol,
qty: 10000,
type: 'MARKET',
})
).toMatchObject({
ret_code: API_ERROR_CODE.BALANCE_INSUFFICIENT_SPOT,
ret_msg: 'Balance insufficient ',
});
});
it('cancelOrder()', async () => {
expect(
await api.cancelOrder({
orderId: '1231231',
})
).toMatchObject({
ret_code: API_ERROR_CODE.ORDER_NOT_FOUND_OR_TOO_LATE_SPOT,
ret_msg: 'Order does not exist.',
});
});
it('cancelOrderBatch()', async () => {
expect(
await api.cancelOrderBatch({
symbol,
orderTypes: ['LIMIT', 'LIMIT_MAKER'],
})
).toMatchObject(successResponseObject(''));
});
});

81
test/spot/public.test.ts Normal file
View File

@@ -0,0 +1,81 @@
import { SpotClient } from '../../src';
import {
notAuthenticatedError,
successResponseList,
successResponseObject,
} from '../response.util';
describe('Public Spot REST API Endpoints', () => {
const useLivenet = true;
const api = new SpotClient(undefined, undefined, useLivenet, {
disable_time_sync: true,
});
const symbol = 'BTCUSDT';
const interval = '15m';
const timestampOneHourAgo = new Date().getTime() / 1000 - 1000 * 60 * 60;
const from = Number(timestampOneHourAgo.toFixed(0));
it('should throw for unauthenticated private calls', async () => {
expect(() => api.getOpenOrders()).rejects.toMatchObject(
notAuthenticatedError()
);
expect(() => api.getBalances()).rejects.toMatchObject(
notAuthenticatedError()
);
});
it('getSymbols()', async () => {
expect(await api.getSymbols()).toMatchObject(successResponseList(''));
});
it('getOrderBook()', async () => {
expect(await api.getOrderBook(symbol)).toMatchObject(
successResponseObject(null)
);
});
it('getMergedOrderBook()', async () => {
expect(await api.getMergedOrderBook(symbol)).toMatchObject(
successResponseObject(null)
);
});
it('getTrades()', async () => {
expect(await api.getTrades(symbol)).toMatchObject(
successResponseObject(null)
);
});
it('getCandles()', async () => {
expect(await api.getCandles(symbol, interval)).toMatchObject(
successResponseObject(null)
);
});
it('get24hrTicker()', async () => {
expect(await api.get24hrTicker()).toMatchObject(
successResponseObject(null)
);
});
it('getLastTradedPrice()', async () => {
expect(await api.getLastTradedPrice()).toMatchObject(
successResponseObject(null)
);
});
it('getBestBidAskPrice()', async () => {
expect(await api.getBestBidAskPrice()).toMatchObject(
successResponseObject(null)
);
});
it('getServerTime()', async () => {
expect(await api.getServerTime()).toStrictEqual(expect.any(Number));
});
it('fetchServertime() returns number', async () => {
expect(await api.fetchServerTime()).toStrictEqual(expect.any(Number));
});
});