From 99a24e69823528a61a4b17c9e82e6b639686680c Mon Sep 17 00:00:00 2001 From: Tiago Siebler Date: Tue, 22 Nov 2022 12:32:26 +0000 Subject: [PATCH] Expand types, add type guards, flesh out futures position example --- examples/rest-trade-futures.ts | 81 ++++++++++++++++-- src/futures-client.ts | 11 ++- src/types/request/futures.ts | 7 +- src/types/response/futures.ts | 27 ++++++ .../{websockets.ts => websockets/client.ts} | 2 +- src/types/websockets/events.ts | 82 +++++++++++++++++++ src/types/websockets/index.ts | 2 + src/util/index.ts | 1 + src/util/type-guards.ts | 73 +++++++++++++++++ 9 files changed, 276 insertions(+), 10 deletions(-) rename src/types/{websockets.ts => websockets/client.ts} (97%) create mode 100644 src/types/websockets/events.ts create mode 100644 src/types/websockets/index.ts create mode 100644 src/util/type-guards.ts diff --git a/examples/rest-trade-futures.ts b/examples/rest-trade-futures.ts index ba156f4..167a27b 100644 --- a/examples/rest-trade-futures.ts +++ b/examples/rest-trade-futures.ts @@ -1,7 +1,19 @@ -import { FuturesClient, WebsocketClient } from '../src/index'; +import { + FuturesClient, + isWsFuturesAccountSnapshotEvent, + isWsFuturesPositionsSnapshotEvent, + NewFuturesOrder, + WebsocketClient, +} from '../src'; // or -// import { SpotClient } from 'bitget-api'; +// import { +// FuturesClient, +// isWsFuturesAccountSnapshotEvent, +// isWsFuturesPositionsSnapshotEvent, +// NewFuturesOrder, +// WebsocketClient, +// } from 'bitget-api'; // read from environmental variables const API_KEY = process.env.API_KEY_COM; @@ -39,11 +51,39 @@ function roundDown(value, decimals) { ); } -/** This is a simple script wrapped in a immediately invoked function expression, designed to check for any available BTC balance and immediately sell the full amount for USDT */ +/** WS event handler that uses type guards to narrow down event type */ +async function handleWsUpdate(event) { + if (isWsFuturesAccountSnapshotEvent(event)) { + console.log(new Date(), 'ws update (account balance):', event); + return; + } + + if (isWsFuturesPositionsSnapshotEvent(event)) { + console.log(new Date(), 'ws update (positions):', event); + return; + } + + logWSEvent('update (unhandled)', event); +} + +/** + * This is a simple script wrapped in a immediately invoked function expression (to execute the below workflow immediately). + * + * It is designed to: + * - open a private websocket channel to log account events + * - check for any available USDT balance in the futures account + * - immediately open a minimum sized long position on BTCUSDT + * - check active positions + * - immediately send closing orders for any active futures positions + * - check positions again + * + * The corresponding UI for this is at https://www.bitget.com/en/mix/usdt/BTCUSDT_UMCBL + */ (async () => { try { // Add event listeners to log websocket events on account - wsClient.on('update', (data) => logWSEvent('update', data)); + wsClient.on('update', (data) => handleWsUpdate(data)); + wsClient.on('open', (data) => logWSEvent('open', data)); wsClient.on('response', (data) => logWSEvent('response', data)); wsClient.on('reconnect', (data) => logWSEvent('reconnect', data)); @@ -79,13 +119,14 @@ function roundDown(value, decimals) { const bitcoinUSDFuturesRule = symbolRulesResult.data.find( (row) => row.symbol === symbol ); + console.log('symbol rules: ', bitcoinUSDFuturesRule); if (!bitcoinUSDFuturesRule) { console.error('Failed to get trading rules for ' + symbol); return; } - const order = { + const order: NewFuturesOrder = { marginCoin, orderType: 'market', side: 'open_long', @@ -98,6 +139,36 @@ function roundDown(value, decimals) { const result = await client.submitOrder(order); console.log('order result: ', result); + + const positionsResult = await client.getPositions('umcbl'); + const positionsToClose = positionsResult.data.filter( + (pos) => pos.total !== '0' + ); + + console.log('open positions to close: ', positionsToClose); + + // Loop through any active positions and send a closing market order on each position + for (const position of positionsToClose) { + const closingSide = + position.holdSide === 'long' ? 'close_long' : 'close_short'; + const closingOrder: NewFuturesOrder = { + marginCoin: position.marginCoin, + orderType: 'market', + side: closingSide, + size: position.available, + symbol: position.symbol, + }; + + console.log('closing position with market order: ', closingOrder); + + const result = await client.submitOrder(closingOrder); + console.log('position closing order result: ', result); + } + + console.log( + 'positions after closing all: ', + await client.getPositions('umcbl') + ); } catch (e) { console.error('request failed: ', e); } diff --git a/src/futures-client.ts b/src/futures-client.ts index 333f816..0560a4a 100644 --- a/src/futures-client.ts +++ b/src/futures-client.ts @@ -17,6 +17,8 @@ import { NewFuturesPlanStopOrder, FuturesAccount, FuturesSymbolRule, + FuturesMarginMode, + FuturesPosition, } from './types'; import { REST_CLIENT_TYPE_ENUM } from './util'; import BaseRestClient from './util/BaseRestClient'; @@ -198,7 +200,7 @@ export class FuturesClient extends BaseRestClient { setMarginMode( symbol: string, marginCoin: string, - marginMode: 'fixed' | 'crossed' + marginMode: FuturesMarginMode ): Promise> { return this.postPrivate('/api/mix/v1/account/setMarginMode', { symbol, @@ -208,7 +210,10 @@ export class FuturesClient extends BaseRestClient { } /** Get Symbol Position */ - getPosition(symbol: string, marginCoin?: string): Promise> { + getPosition( + symbol: string, + marginCoin?: string + ): Promise> { return this.getPrivate('/api/mix/v1/position/singlePosition', { symbol, marginCoin, @@ -219,7 +224,7 @@ export class FuturesClient extends BaseRestClient { getPositions( productType: FuturesProductType, marginCoin?: string - ): Promise> { + ): Promise> { return this.getPrivate('/api/mix/v1/position/allPosition', { productType, marginCoin, diff --git a/src/types/request/futures.ts b/src/types/request/futures.ts index a0ff151..053dcb7 100644 --- a/src/types/request/futures.ts +++ b/src/types/request/futures.ts @@ -8,6 +8,12 @@ export type FuturesProductType = | 'sdmcbl' | 'scmcbl'; +export type FuturesHoldSide = 'long' | 'short'; + +export type FuturesMarginMode = 'fixed' | 'crossed'; + +export type FuturesHoldMode = 'double_hold' | 'single_hold'; + export interface FuturesAccountBillRequest { symbol: string; marginCoin: string; @@ -95,7 +101,6 @@ export interface ModifyFuturesPlanOrderTPSL { } export type FuturesPlanType = 'profit_plan' | 'loss_plan'; -export type FuturesHoldSide = 'long' | 'short'; export interface NewFuturesPlanStopOrder { symbol: string; diff --git a/src/types/response/futures.ts b/src/types/response/futures.ts index 3977553..ae4dcb7 100644 --- a/src/types/response/futures.ts +++ b/src/types/response/futures.ts @@ -1,3 +1,9 @@ +import { + FuturesHoldMode, + FuturesHoldSide, + FuturesMarginMode, +} from '../request'; + export interface FuturesAccount { marginCoin: string; locked: number; @@ -33,3 +39,24 @@ export interface FuturesSymbolRule { takerFeeRate: string; volumePlace: string; } + +export interface FuturesPosition { + marginCoin: string; + symbol: string; + holdSide: FuturesHoldSide; + openDelegateCount: string; + margin: string; + available: string; + locked: string; + total: string; + leverage: number; + achievedProfits: string; + averageOpenPrice: string; + marginMode: FuturesMarginMode; + holdMode: FuturesHoldMode; + unrealizedPL: string; + liquidationPrice: string; + keepMarginRate: string; + marketPrice: string; + cTime: string; +} diff --git a/src/types/websockets.ts b/src/types/websockets/client.ts similarity index 97% rename from src/types/websockets.ts rename to src/types/websockets/client.ts index 4d8488e..1d52d3d 100644 --- a/src/types/websockets.ts +++ b/src/types/websockets/client.ts @@ -1,4 +1,4 @@ -import { WS_KEY_MAP } from '../util'; +import { WS_KEY_MAP } from '../../util'; export type WsPublicSpotTopic = | 'ticker' diff --git a/src/types/websockets/events.ts b/src/types/websockets/events.ts new file mode 100644 index 0000000..8082e64 --- /dev/null +++ b/src/types/websockets/events.ts @@ -0,0 +1,82 @@ +export interface WsBaseEvent { + action: TAction; + arg: unknown; + data: TData[]; +} + +export interface WsSnapshotChannelEvent extends WsBaseEvent<'snapshot'> { + arg: { + instType: string; + channel: string; + instId: string; + }; +} + +export interface WsSnapshotAccountEvent extends WsBaseEvent<'snapshot'> { + arg: { + instType: string; + channel: 'account'; + instId: string; + }; +} + +export interface WsSnapshotPositionsEvent extends WsBaseEvent<'snapshot'> { + arg: { + instType: string; + channel: 'positions'; + instId: string; + }; +} + +export interface WsAccountSnapshotUMCBL extends WsBaseEvent<'snapshot'> { + arg: { + instType: 'umcbl'; + channel: 'account'; + instId: string; + }; + data: WsAccountSnapshotDataUMCBL[]; +} + +export interface WsAccountSnapshotDataUMCBL { + marginCoin: string; + locked: string; + available: string; + maxOpenPosAvailable: string; + maxTransferOut: string; + equity: string; + usdtEquity: string; +} + +export interface WSPositionSnapshotUMCBL extends WsBaseEvent<'snapshot'> { + arg: { + instType: 'umcbl'; + channel: 'positions'; + instId: string; + }; + data: WsPositionSnapshotDataUMCBL[]; +} + +export interface WsPositionSnapshotDataUMCBL { + posId: string; + instId: string; + instName: string; + marginCoin: string; + margin: string; + marginMode: string; + holdSide: string; + holdMode: string; + total: string; + available: string; + locked: string; + averageOpenPrice: string; + leverage: number; + achievedProfits: string; + upl: string; + uplRate: string; + liqPx: string; + keepMarginRate: string; + marginRate: string; + cTime: string; + uTime: string; + markPrice: string; +} diff --git a/src/types/websockets/index.ts b/src/types/websockets/index.ts new file mode 100644 index 0000000..a22218e --- /dev/null +++ b/src/types/websockets/index.ts @@ -0,0 +1,2 @@ +export * from './client'; +export * from './events'; diff --git a/src/util/index.ts b/src/util/index.ts index 000f888..fd92581 100644 --- a/src/util/index.ts +++ b/src/util/index.ts @@ -2,4 +2,5 @@ export * from './BaseRestClient'; export * from './requestUtils'; export * from './WsStore'; export * from './logger'; +export * from './type-guards'; export * from './websocket-util'; diff --git a/src/util/type-guards.ts b/src/util/type-guards.ts new file mode 100644 index 0000000..2577367 --- /dev/null +++ b/src/util/type-guards.ts @@ -0,0 +1,73 @@ +import { + WsAccountSnapshotUMCBL, + WsBaseEvent, + WSPositionSnapshotUMCBL, + WsSnapshotAccountEvent, + WsSnapshotChannelEvent, + WsSnapshotPositionsEvent, +} from '../types'; + +/** TypeGuard: event has a string "action" property */ +function isWsEvent(event: unknown): event is WsBaseEvent { + return ( + typeof event === 'object' && + event && + typeof event['action'] === 'string' && + event['data'] + ); +} + +/** TypeGuard: event has "action === snapshot" */ +function isWsSnapshotEvent(event: unknown): event is WsBaseEvent<'snapshot'> { + return isWsEvent(event) && event.action === 'snapshot'; +} + +/** TypeGuard: event has a string channel name */ +function isWsChannelEvent(event: WsBaseEvent): event is WsSnapshotChannelEvent { + if ( + typeof event['arg'] === 'object' && + event.arg && + typeof event?.arg['channel'] === 'string' + ) { + return true; + } + return false; +} + +/** TypeGuard: event is an account update (balance) */ +export function isWsAccountSnapshotEvent( + event: unknown +): event is WsSnapshotAccountEvent { + return ( + isWsSnapshotEvent(event) && + isWsChannelEvent(event) && + event.arg.channel === 'account' && + Array.isArray(event.data) + ); +} + +/** TypeGuard: event is a positions update */ +export function isWsPositionsSnapshotEvent( + event: unknown +): event is WsSnapshotPositionsEvent { + return ( + isWsSnapshotEvent(event) && + isWsChannelEvent(event) && + event.arg.channel === 'positions' && + Array.isArray(event.data) + ); +} + +/** TypeGuard: event is a UMCBL account update (balance) */ +export function isWsFuturesAccountSnapshotEvent( + event: unknown +): event is WsAccountSnapshotUMCBL { + return isWsAccountSnapshotEvent(event) && event.arg.instType === 'umcbl'; +} + +/** TypeGuard: event is a UMCBL positions update */ +export function isWsFuturesPositionsSnapshotEvent( + event: unknown +): event is WSPositionSnapshotUMCBL { + return isWsPositionsSnapshotEvent(event) && event.arg.instType === 'umcbl'; +}