diff --git a/README.md b/README.md index 6888e41..ebc6343 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,6 @@ > > This project is under construction 🚧 -> Only USD based markets are supported for now > Minimalist service designed to execute [TradingView](https://www.tradingview.com/) webhooks and process them to cryptocurrencies exchanges from [AWS lightsail](https://lightsail.aws.amazon.com/). @@ -27,13 +26,17 @@ You can use the bot with : ### 🏦 Supported exchanges -| logo | id | name | doc | -|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|-----------------------------------------------------------------------------|:------------------------------------------------------------------:| +> +> Only USD based markets are supported for now +> + +| logo | id | name | doc | +|-|-|-|:-:| | [![binance](https://user-images.githubusercontent.com/1294454/29604020-d5483cdc-87ee-11e7-94c7-d1a8d9169293.jpg)](https://www.binance.com/) | binance | [Binance (spot)](https://www.binance.com/) | [API](https://binance-docs.github.io/apidocs/spot/en) | | [![binanceusdm](https://user-images.githubusercontent.com/1294454/117738721-668c8d80-b205-11eb-8c49-3fad84c4a07f.jpg)](https://www.binance.com/) | binanceusdm | [Binance USDⓈ-M (futures)](https://www.binance.com/) | [API](https://binance-docs.github.io/apidocs/spot/en) | | [![ftx](https://user-images.githubusercontent.com/1294454/67149189-df896480-f2b0-11e9-8816-41593e17f9ec.jpg)](https://ftx.com/) | ftx | [FTX (spot & futures)](https://ftx.com/) | [API](https://github.com/ftexchange/ftx) | +| [![kraken](https://user-images.githubusercontent.com/51840849/76173629-fc67fb00-61b1-11ea-84fe-f2de582f58a3.jpg)](https://www.kraken.com) | kraken | [Kraken (spot)](https://www.kraken.com) | [API](https://www.kraken.com/features/api) | | [![kucoin](https://user-images.githubusercontent.com/51840849/87295558-132aaf80-c50e-11ea-9801-a2fb0c57c799.jpg)](https://www.kucoin.com/) | kucoin | [KuCoin (spot)](https://www.kucoin.com/) | [API](https://docs.kucoin.com) | -| [![kraken](https://user-images.githubusercontent.com/51840849/76173629-fc67fb00-61b1-11ea-84fe-f2de582f58a3.jpg)](https://www.kraken.com) | kraken | [Kraken (spot)](https://www.kraken.com) | [API](https://www.kraken.com/features/api) | ### 🚧 Features @@ -46,10 +49,13 @@ You can use the bot with : - list account balances - list exchange markets - process multiple trades at once - - +- close a position and open another in the opposite direction (Futures only) +- close a position while reducing on oversell / overbuy (Futures only) ### 💡 Contributions Feel free to submit [Github issues](https://github.com/thibaultyou/tradingview-alerts-processor/issues) if you find anything you want me to add, fix or improve. + +Best way to show your support to this tool is by hitting the star button [![Stars](https://img.shields.io/github/stars/thibaultyou/tradingview-alerts-processor?style=social)](https://github.com/thibaultyou/tradingview-alerts-processor/stargazers), you can also [!["Buy Me A Coffee"](https://img.shields.io/badge/-buy_me_a%C2%A0coffee-gray?logo=buy-me-a-coffee)](https://www.buymeacoffee.com/thibaultyou). + +You can join us on the Jackrabbit Discord server [![Discord](https://img.shields.io/discord/664206005881536512)](https://discord.gg/mNMVWXpAGd) where I'll be happy to answer questions there, you'll also find great strategies to use with this tool. \ No newline at end of file diff --git a/docs/2_Alerts.md b/docs/2_Alerts.md index b101f1f..1fa9496 100644 --- a/docs/2_Alerts.md +++ b/docs/2_Alerts.md @@ -61,7 +61,6 @@ [{ "stub": "dev", "direction": "long", "symbol": "ETH-PERP", "size": "50" }, { "stub": "dev", "direction": "long", "symbol": "BTC-PERP", "size": "50" }] ``` - - + ``` \ No newline at end of file diff --git a/docs/3_Commands.md b/docs/3_Commands.md index 343bad7..8fd6b82 100644 --- a/docs/3_Commands.md +++ b/docs/3_Commands.md @@ -82,7 +82,6 @@ curl -d '[{"stub": "test", "symbol": "ETH-PERP", "size": "11", "direction": "long" }, {"stub": "test", "symbol": "BTC-PERP", "size": "11", "direction": "long" }]' -X POST http://YOUR.STATIC.IP.ADDRESS/trades -H 'Content-Type: application/json; charset=utf-8' ``` - \ No newline at end of file + ``` \ No newline at end of file diff --git a/src/entities/trade.entities.ts b/src/entities/trade.entities.ts index c2a0d74..080af2f 100644 --- a/src/entities/trade.entities.ts +++ b/src/entities/trade.entities.ts @@ -5,7 +5,12 @@ import { Matches, ValidateIf } from 'class-validator'; -import { SIDES, Side } from '../constants/trading.constants'; +import { + SIDES, + Side, + TRADING_MODES, + TradingMode +} from '../constants/trading.constants'; export class Trade { @IsString() @@ -19,10 +24,10 @@ export class Trade { @IsOptional() max?: string; - // @IsString() - // @IsIn(TRADING_MODES) - // @IsOptional() - // mode?: TradingMode; + @IsString() + @IsIn(TRADING_MODES) + @IsOptional() + mode?: TradingMode; @IsString() @Matches(/.*(PERP|USD).*/) diff --git a/src/interfaces/exchanges/base/base.exchange.interfaces.ts b/src/interfaces/exchanges/base/base.exchange.interfaces.ts new file mode 100644 index 0000000..935c82b --- /dev/null +++ b/src/interfaces/exchanges/base/base.exchange.interfaces.ts @@ -0,0 +1,53 @@ +import { Exchange, Order, Ticker } from 'ccxt'; +import { Account } from '../../../entities/account.entities'; +import { Trade } from '../../../entities/trade.entities'; +import { IMarket } from '../../market.interfaces'; +import { IOrderOptions } from '../../trading.interfaces'; +import { IBalance, ISession } from '../common.exchange.interfaces'; + +export interface IBaseExchange { + checkCredentials(account: Account, instance: Exchange): Promise; + + refreshSession(account: Account): Promise; + + getBalances(account: Account, instance?: Exchange): Promise; + + getTicker(symbol: string): Promise; + + getMarkets(): Promise; + + getAvailableFunds(account: Account, ticker: Ticker): Promise; + + getOpenOrderOptions( + account: Account, + ticker: Ticker, + trade: Trade + ): Promise; + + openOrder(account: Account, trade: Trade): Promise; + + getCloseOrderOptions( + account: Account, + ticker: Ticker, + trade: Trade + ): Promise; + + closeOrder( + account: Account, + trade: Trade, + ticker?: Ticker // can be preloaded in openOrder + ): Promise; + + handleOrderModes( + account: Account, + ticker: Ticker, + trade: Trade + ): Promise; + + handleMaxBudget( + account: Account, + ticker: Ticker, + trade: Trade, + balance: number + ): Promise; +} diff --git a/src/interfaces/exchanges/base/futures.exchange.interfaces.ts b/src/interfaces/exchanges/base/futures.exchange.interfaces.ts new file mode 100644 index 0000000..732a38f --- /dev/null +++ b/src/interfaces/exchanges/base/futures.exchange.interfaces.ts @@ -0,0 +1,26 @@ +import { Exchange, Ticker } from 'ccxt'; +import { Account } from '../../../entities/account.entities'; +import { Trade } from '../../../entities/trade.entities'; +import { FuturesPosition } from '../../../types/exchanges.types'; + +export interface IFuturesExchange { + handleReverseOrder( + account: Account, + ticker: Ticker, + trade: Trade + ): Promise; + + handleOverflow( + account: Account, + ticker: Ticker, + trade: Trade + ): Promise; + + fetchPositions(instance: Exchange): Promise; + + getPositions(account: Account): Promise; + + getTickerPosition(account: Account, ticker: Ticker): Promise; + + getTickerPositionSize(account: Account, ticker: Ticker): Promise; +} diff --git a/src/interfaces/exchanges/base/spot.exchange.interfaces.ts b/src/interfaces/exchanges/base/spot.exchange.interfaces.ts new file mode 100644 index 0000000..928f54f --- /dev/null +++ b/src/interfaces/exchanges/base/spot.exchange.interfaces.ts @@ -0,0 +1,3 @@ +/* eslint-disable @typescript-eslint/no-empty-interface */ + +export interface ISpotExchange {} diff --git a/src/services/exchanges/base/base.exchange.service.ts b/src/services/exchanges/base/base.exchange.service.ts index 519c7dc..6891a62 100644 --- a/src/services/exchanges/base/base.exchange.service.ts +++ b/src/services/exchanges/base/base.exchange.service.ts @@ -13,6 +13,8 @@ import { EXCHANGE_INIT_SUCCESS, MARKETS_READ_ERROR, MARKETS_READ_SUCCESS, + TICKER_BALANCE_READ_ERROR, + TICKER_BALANCE_READ_SUCCESS, TICKER_READ_ERROR, TICKER_READ_SUCCESS } from '../../../messages/exchanges.messages'; @@ -50,17 +52,18 @@ import { getExchangeOptions, isSpotExchange } from '../../../utils/exchanges/common.utils'; -import { getSpotQuote } from '../../../utils/trading/symbol.utils'; +import { getQuote, getSymbol } from '../../../utils/trading/symbol.utils'; import { getSide } from '../../../utils/trading/side.utils'; import { - getOrderCost, getRelativeOrderSize, - getTokensAmount + getTokensAmount, + getTokensPrice } from '../../../utils/trading/conversion.utils'; import { getTickerPrice } from '../../../utils/trading/ticker.utils'; import { filterBalances } from '../../../utils/trading/balance.utils'; +import { IBaseExchange } from '../../../interfaces/exchanges/base/base.exchange.interfaces'; -export abstract class BaseExchangeService { +export abstract class BaseExchangeService implements IBaseExchange { exchangeId: ExchangeId; defaultExchange: Exchange; sessions = new Map(); // account id, exchange session @@ -70,19 +73,7 @@ export abstract class BaseExchangeService { this.defaultExchange = new ccxt[exchangeId](); } - abstract getCloseOrderOptions( - account: Account, - ticker: Ticker, - trade: Trade - ): Promise; - - abstract handleReverseOrder( - account: Account, - ticker: Ticker, - trade: Trade - ): Promise; - - abstract handleOverflow( + abstract handleOrderModes( account: Account, ticker: Ticker, trade: Trade @@ -95,6 +86,12 @@ export abstract class BaseExchangeService { balance: number ): Promise; + abstract getCloseOrderOptions( + account: Account, + ticker: Ticker, + trade: Trade + ): Promise; + checkCredentials = async ( account: Account, instance: Exchange @@ -173,6 +170,28 @@ export abstract class BaseExchangeService { } }; + getTickerBalance = async ( + account: Account, + ticker: Ticker + ): Promise => { + const accountId = getAccountId(account); + const symbol = getSymbol(ticker.symbol); + try { + const balances = await this.getBalances(account); + const balance = balances.filter((b) => b.coin === symbol).pop(); + const size = Number(balance.free); + debug( + TICKER_BALANCE_READ_SUCCESS(this.exchangeId, accountId, symbol, balance) + ); + return size; + } catch (err) { + error(TICKER_BALANCE_READ_ERROR(this.exchangeId, accountId, symbol, err)); + throw new TickerFetchError( + TICKER_BALANCE_READ_ERROR(this.exchangeId, accountId, symbol, err) + ); + } + }; + getMarkets = async (): Promise => { try { const markets: ccxt.Market[] = await this.defaultExchange.fetchMarkets(); @@ -203,7 +222,7 @@ export abstract class BaseExchangeService { ): Promise => { const { symbol } = ticker; const accountId = getAccountId(account); - const quote = getSpotQuote(symbol); + const quote = getQuote(symbol); // TODO refacto let availableFunds = 0; if (this.exchangeId === ExchangeId.FTX && !isFTXSpot(ticker)) { @@ -212,60 +231,25 @@ export abstract class BaseExchangeService { ).result; availableFunds = Number(accountInfos.freeCollateral); } else { - const balances = await this.getBalances(account); - const balance = balances.filter((b) => b.coin === quote).pop(); - availableFunds = Number(balance.free); + availableFunds = await this.getTickerBalance(account, ticker); } info(AVAILABLE_FUNDS(accountId, this.exchangeId, quote, availableFunds)); return availableFunds; }; - // handleOrderModes = async ( - // account: Account, - // ticker: Ticker, - // trade: Trade - // ): Promise => { - // const { mode } = trade; - // if (mode === TradingMode.Reverse) { - // await this.handleReverseOrder(account, ticker, trade); - // } else if (mode === TradingMode.Overflow) { - // const isOverflowing = await this.handleOverflow(account, ticker, trade); - // if (isOverflowing) { - // return false; // on overflow we only close position - // } - // } - // return true; - // }; - getOpenOrderOptions = async ( account: Account, ticker: Ticker, trade: Trade ): Promise => { const { symbol } = ticker; - const { size, max, direction } = trade; + const { size, direction } = trade; const side = getSide(direction); try { - // TODO refacto - let orderSize = Number(size); - if (size.includes('%') || max) { - const funds = await this.getAvailableFunds(account, ticker); // avoid this call if possible - if (size.includes('%')) { - orderSize = getRelativeOrderSize(funds, size); - } - if (max) { - await this.handleMaxBudget(account, ticker, trade, funds); - } - } - // if (isSpotExchange(ticker, this.exchangeId) && orderSize > funds) { - // // TODO create dedicated error - // throw new Error('Insufficient funds'); - // } - /// - const tickerPrice = getTickerPrice(ticker, this.exchangeId); + const orderSize = Number(size); return { - size: getTokensAmount(symbol, tickerPrice, Number(orderSize)), + size: getTokensAmount(symbol, tickerPrice, orderSize), side: side as 'buy' | 'sell' }; } catch (err) { @@ -274,54 +258,71 @@ export abstract class BaseExchangeService { } }; + // TODO refacto openOrder = async (account: Account, trade: Trade): Promise => { await this.refreshSession(account); - const { symbol, direction } = trade; + const { symbol, size, direction, max, mode } = trade; const accountId = getAccountId(account); try { const ticker = await this.getTicker(symbol); - // const isOrderAllowed = await this.handleOrderModes( - // account, - // ticker, - // trade - // ); - // if (isOrderAllowed) { - // TODO refacto - // close on sell spot order + // handling sell if spot exchange if ( getSide(direction) === Side.Sell && isSpotExchange(ticker, this.exchangeId) ) { return await this.closeOrder(account, trade, ticker); } - const { side, size } = await this.getOpenOrderOptions( - account, - ticker, - trade - ); - const cost = getOrderCost(ticker, this.exchangeId, size); - const order: Order = await this.sessions - .get(accountId) - .exchange.createMarketOrder(symbol, side, size); - side === Side.Buy - ? long( - OPEN_LONG_TRADE_SUCCESS( - this.exchangeId, - accountId, - symbol, - cost.toFixed(2) - ) - ) - : short( - OPEN_SHORT_TRADE_SUCCESS( - this.exchangeId, - accountId, - symbol, - cost.toFixed(2) - ) - ); - return order; + // handling size / budget + if (size.includes('%') || max) { + const funds = await this.getAvailableFunds(account, ticker); + if (size.includes('%')) { + trade = { + ...trade, + size: getRelativeOrderSize(funds, size).toString() + }; + } + if (max) { + await this.handleMaxBudget(account, ticker, trade, funds); + } + } + // if (isSpotExchange(ticker, this.exchangeId) && orderSize > funds) { + // // TODO create dedicated error + // throw new Error('Insufficient funds'); // } + /// + // handling modes + const isOrderAllowed = mode + ? await this.handleOrderModes(account, ticker, trade) + : true; + if (isOrderAllowed) { + const { side, size } = await this.getOpenOrderOptions( + account, + ticker, + trade + ); + const order: Order = await this.sessions + .get(accountId) + .exchange.createMarketOrder(symbol, side, size); + const cost = getTokensPrice(ticker, this.exchangeId, size); + side === Side.Buy + ? long( + OPEN_LONG_TRADE_SUCCESS( + this.exchangeId, + accountId, + symbol, + cost.toFixed(2) + ) + ) + : short( + OPEN_SHORT_TRADE_SUCCESS( + this.exchangeId, + accountId, + symbol, + cost.toFixed(2) + ) + ); + return order; + } } catch (err) { error(OPEN_TRADE_ERROR(this.exchangeId, accountId, symbol), err); throw new OpenPositionError( @@ -347,7 +348,7 @@ export abstract class BaseExchangeService { ticker, trade ); - const cost = getOrderCost(ticker, this.exchangeId, size); + const price = getTokensPrice(ticker, this.exchangeId, size); const order = await this.sessions .get(accountId) .exchange.createMarketOrder(symbol, side, size); @@ -357,7 +358,7 @@ export abstract class BaseExchangeService { accountId, symbol, size, - cost.toFixed(2) + price.toFixed(2) ) ); return order; diff --git a/src/services/exchanges/base/composite.exchange.service.ts b/src/services/exchanges/base/composite.exchange.service.ts index 6190c36..78dd542 100644 --- a/src/services/exchanges/base/composite.exchange.service.ts +++ b/src/services/exchanges/base/composite.exchange.service.ts @@ -1,37 +1,7 @@ -import { Ticker } from 'ccxt'; -import { Account } from '../../../entities/account.entities'; -import { TickerFetchError } from '../../../errors/exchange.errors'; -import { - TICKER_BALANCE_READ_SUCCESS, - TICKER_BALANCE_READ_ERROR -} from '../../../messages/exchanges.messages'; -import { getAccountId } from '../../../utils/account.utils'; -import { getSpotSymbol } from '../../../utils/trading/symbol.utils'; -import { debug, error } from '../../logger.service'; +import { ISpotExchange } from '../../../interfaces/exchanges/base/spot.exchange.interfaces'; import { FuturesExchangeService } from './futures.exchange.service'; // FIXME can be replaced by a mixin -export abstract class CompositeExchangeService extends FuturesExchangeService { - getTickerBalance = async ( - account: Account, - ticker: Ticker - ): Promise => { - const accountId = getAccountId(account); - const symbol = getSpotSymbol(ticker.symbol); - try { - const balances = await this.getBalances(account); - const balance = balances.filter((b) => b.coin === symbol).pop(); - const size = Number(balance.free); - debug( - TICKER_BALANCE_READ_SUCCESS(this.exchangeId, accountId, symbol, balance) - ); - return size; - } catch (err) { - error(TICKER_BALANCE_READ_ERROR(this.exchangeId, accountId, symbol, err)); - throw new TickerFetchError( - TICKER_BALANCE_READ_ERROR(this.exchangeId, accountId, symbol, err) - ); - } - }; - // above declaration is the same as SpotExchangeService since I'm not playing with mixins for now -} +export abstract class CompositeExchangeService + extends FuturesExchangeService + implements ISpotExchange {} diff --git a/src/services/exchanges/base/futures.exchange.service.ts b/src/services/exchanges/base/futures.exchange.service.ts index ad78b56..07a746b 100644 --- a/src/services/exchanges/base/futures.exchange.service.ts +++ b/src/services/exchanges/base/futures.exchange.service.ts @@ -1,11 +1,10 @@ import { Exchange, Ticker } from 'ccxt'; +import { TradingMode } from '../../../constants/trading.constants'; import { Account } from '../../../entities/account.entities'; import { Trade } from '../../../entities/trade.entities'; import { PositionsFetchError } from '../../../errors/exchange.errors'; -import { - NoOpenPositionError, - OpenPositionError -} from '../../../errors/trading.errors'; +import { OpenPositionError } from '../../../errors/trading.errors'; +import { IFuturesExchange } from '../../../interfaces/exchanges/base/futures.exchange.interfaces'; import { NO_CURRENT_POSITION, POSITIONS_READ_ERROR, @@ -15,17 +14,37 @@ import { import { OPEN_TRADE_ERROR_MAX_SIZE } from '../../../messages/trading.messages'; import { FuturesPosition } from '../../../types/exchanges.types'; import { getAccountId } from '../../../utils/account.utils'; -import { getRelativeOrderSize } from '../../../utils/trading/conversion.utils'; +import { + getRelativeOrderSize, + getTokensAmount +} from '../../../utils/trading/conversion.utils'; import { filterPosition, filterPositions, getPositionSize } from '../../../utils/trading/position.utils'; -import { getSide } from '../../../utils/trading/side.utils'; +import { getSide, getInvertedSide } from '../../../utils/trading/side.utils'; import { debug, error, info } from '../../logger.service'; import { BaseExchangeService } from './base.exchange.service'; +import { getTickerPrice } from '../../../utils/trading/ticker.utils'; +import { IOrderOptions } from '../../../interfaces/trading.interfaces'; + +export abstract class FuturesExchangeService + extends BaseExchangeService + implements IFuturesExchange +{ + abstract handleReverseOrder( + account: Account, + ticker: Ticker, + trade: Trade + ): Promise; + + abstract handleOverflow( + account: Account, + ticker: Ticker, + trade: Trade + ): Promise; -export abstract class FuturesExchangeService extends BaseExchangeService { abstract fetchPositions(instance: Exchange): Promise; getPositions = async (account: Account): Promise => { @@ -56,11 +75,14 @@ export abstract class FuturesExchangeService extends BaseExchangeService { const position = filterPosition(positions, this.exchangeId, ticker); if (!position) { info(NO_CURRENT_POSITION(accountId, this.exchangeId, symbol)); - throw new NoOpenPositionError( - NO_CURRENT_POSITION(accountId, this.exchangeId, symbol) + // throw new NoOpenPositionError( + // NO_CURRENT_POSITION(accountId, this.exchangeId, symbol) + // ); + } else { + debug( + POSITION_READ_SUCCESS(accountId, this.exchangeId, symbol, position) ); } - debug(POSITION_READ_SUCCESS(accountId, this.exchangeId, symbol, position)); return position; }; @@ -69,7 +91,48 @@ export abstract class FuturesExchangeService extends BaseExchangeService { ticker: Ticker ): Promise => { const position = await this.getTickerPosition(account, ticker); - return getPositionSize(position, this.exchangeId); + return position ? getPositionSize(position, this.exchangeId) : 0; + }; + + getCloseOrderOptions = async ( + account: Account, + ticker: Ticker, + trade: Trade + ): Promise => { + const { size, direction } = trade; + const { symbol } = ticker; + const price = getTickerPrice(ticker, this.exchangeId); + const position = await this.getTickerPosition(account, ticker); + const current = getPositionSize(position, this.exchangeId); + + let orderSize = 0; + if (size && size.includes('%')) { + orderSize = getRelativeOrderSize(current, size); // relative + } else if (!size || Number(size) > current) { + orderSize = current; // 100% + } else { + orderSize = Number(size); // absolute + } + + return { + size: getTokensAmount(symbol, price, orderSize), + side: getInvertedSide(direction) as 'sell' | 'buy' + }; + }; + + handleOrderModes = async ( + account: Account, + ticker: Ticker, + trade: Trade + ): Promise => { + const { mode } = trade; + if (mode === TradingMode.Reverse) { + await this.handleReverseOrder(account, ticker, trade); + } else if (mode === TradingMode.Overflow) { + // on overflow we only close position so we don't need to open a new trade + return !(await this.handleOverflow(account, ticker, trade)); + } + return true; }; handleMaxBudget = async ( @@ -81,16 +144,10 @@ export abstract class FuturesExchangeService extends BaseExchangeService { const { symbol, max, direction, size } = trade; const accountId = getAccountId(account); const side = getSide(direction); - let current = 0; - // TODO refacto - try { - current = await this.getTickerPositionSize(account, ticker); - } catch (err) { - // silent - } + const current = await this.getTickerPositionSize(account, ticker); if ( Math.abs(current) + - (size.includes('%') // add the required position cost + (size.includes('%') ? getRelativeOrderSize(balance, size) : Number(size)) > Number(max) diff --git a/src/services/exchanges/base/spot.exchange.service.ts b/src/services/exchanges/base/spot.exchange.service.ts index d154c16..a7ea4d5 100644 --- a/src/services/exchanges/base/spot.exchange.service.ts +++ b/src/services/exchanges/base/spot.exchange.service.ts @@ -1,50 +1,27 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { Ticker } from 'ccxt'; import { Side } from '../../../constants/trading.constants'; import { Account } from '../../../entities/account.entities'; import { Trade } from '../../../entities/trade.entities'; -import { TickerFetchError } from '../../../errors/exchange.errors'; import { OpenPositionError } from '../../../errors/trading.errors'; +import { ISpotExchange } from '../../../interfaces/exchanges/base/spot.exchange.interfaces'; import { IOrderOptions } from '../../../interfaces/trading.interfaces'; -import { - TICKER_BALANCE_READ_SUCCESS, - TICKER_BALANCE_READ_ERROR -} from '../../../messages/exchanges.messages'; import { OPEN_TRADE_ERROR_MAX_SIZE } from '../../../messages/trading.messages'; import { getAccountId } from '../../../utils/account.utils'; import { - getOrderCost, + getTokensPrice, getRelativeOrderSize, getTokensAmount } from '../../../utils/trading/conversion.utils'; import { getSide } from '../../../utils/trading/side.utils'; -import { getSpotSymbol } from '../../../utils/trading/symbol.utils'; import { getTickerPrice } from '../../../utils/trading/ticker.utils'; -import { debug, error } from '../../logger.service'; +import { error } from '../../logger.service'; import { BaseExchangeService } from './base.exchange.service'; -export abstract class SpotExchangeService extends BaseExchangeService { - getTickerBalance = async ( - account: Account, - ticker: Ticker - ): Promise => { - const accountId = getAccountId(account); - const symbol = getSpotSymbol(ticker.symbol); - try { - const balances = await this.getBalances(account); - const balance = balances.filter((b) => b.coin === symbol).pop(); - const size = Number(balance.free); - debug( - TICKER_BALANCE_READ_SUCCESS(this.exchangeId, accountId, symbol, balance) - ); - return size; - } catch (err) { - error(TICKER_BALANCE_READ_ERROR(this.exchangeId, accountId, symbol, err)); - throw new TickerFetchError( - TICKER_BALANCE_READ_ERROR(this.exchangeId, accountId, symbol, err) - ); - } - }; - +export abstract class SpotExchangeService + extends BaseExchangeService + implements ISpotExchange +{ handleMaxBudget = async ( account: Account, ticker: Ticker, @@ -57,8 +34,8 @@ export abstract class SpotExchangeService extends BaseExchangeService { const side = getSide(direction); const current = await this.getTickerBalance(account, ticker); if ( - getOrderCost(ticker, this.exchangeId, current) + // get cost of current position - (size.includes('%') // add the required position cost + getTokensPrice(ticker, this.exchangeId, current) + + (size.includes('%') ? getRelativeOrderSize(balance, size) : Number(size)) > Number(max) @@ -90,4 +67,13 @@ export abstract class SpotExchangeService extends BaseExchangeService { : balance // default 100% }; }; + + handleOrderModes = async ( + account: Account, + ticker: Ticker, + trade: Trade + ): Promise => { + // TODO add trading modes for spot markets + return true; + }; } diff --git a/src/services/exchanges/binance-usdm.futures.exchange.service.ts b/src/services/exchanges/binance-usdm.futures.exchange.service.ts index 6592921..237eaa5 100644 --- a/src/services/exchanges/binance-usdm.futures.exchange.service.ts +++ b/src/services/exchanges/binance-usdm.futures.exchange.service.ts @@ -4,11 +4,6 @@ import { ExchangeId } from '../../constants/exchanges.constants'; import { Account } from '../../entities/account.entities'; import { Trade } from '../../entities/trade.entities'; import { IBinanceFuturesUSDPosition } from '../../interfaces/exchanges/binance.exchange.interfaces'; -import { IOrderOptions } from '../../interfaces/trading.interfaces'; -import { - getRelativeOrderSize, - getTokensAmount -} from '../../utils/trading/conversion.utils'; import { FuturesExchangeService } from './base/futures.exchange.service'; export class BinanceFuturesUSDMExchangeService extends FuturesExchangeService { @@ -20,35 +15,6 @@ export class BinanceFuturesUSDMExchangeService extends FuturesExchangeService { instance: Exchange ): Promise => await instance.fetchPositions(); - getCloseOrderOptions = async ( - account: Account, - ticker: Ticker, - trade: Trade - ): Promise => { - const { size } = trade; - const { symbol, info } = ticker; - const { lastPrice } = info; - const { contracts, notional, side } = (await this.getTickerPosition( - account, - ticker - )) as IBinanceFuturesUSDPosition; - - // TODO refacto - let orderSize = 0; - if (size && size.includes('%')) { - orderSize = getRelativeOrderSize(contracts, size); - } else if (!size || Number(size) > notional) { - orderSize = contracts; - } else { - orderSize = getTokensAmount(symbol, lastPrice, Number(size)); - } - - return { - size: orderSize, - side: side === 'long' ? 'sell' : 'buy' - }; - }; - handleReverseOrder( account: Account, ticker: Ticker, diff --git a/src/services/exchanges/binance.spot.exchange.service.ts b/src/services/exchanges/binance.spot.exchange.service.ts index 804c544..8553547 100644 --- a/src/services/exchanges/binance.spot.exchange.service.ts +++ b/src/services/exchanges/binance.spot.exchange.service.ts @@ -1,26 +1,8 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Ticker } from 'ccxt'; import { ExchangeId } from '../../constants/exchanges.constants'; -import { Account } from '../../entities/account.entities'; -import { Trade } from '../../entities/trade.entities'; import { SpotExchangeService } from './base/spot.exchange.service'; export class BinanceSpotExchangeService extends SpotExchangeService { constructor() { super(ExchangeId.Binance); } - handleReverseOrder( - account: Account, - ticker: Ticker, - trade: Trade - ): Promise { - throw new Error('Method not implemented.'); - } - handleOverflow( - account: Account, - ticker: Ticker, - trade: Trade - ): Promise { - throw new Error('Method not implemented.'); - } } diff --git a/src/services/exchanges/ftx.exchange.service.ts b/src/services/exchanges/ftx.exchange.service.ts index 4240d8d..db76ab7 100644 --- a/src/services/exchanges/ftx.exchange.service.ts +++ b/src/services/exchanges/ftx.exchange.service.ts @@ -5,19 +5,27 @@ import { getAccountId } from '../../utils/account.utils'; import { Exchange, Ticker } from 'ccxt'; import { Side } from '../../constants/trading.constants'; import { IOrderOptions } from '../../interfaces/trading.interfaces'; -import { error } from '../logger.service'; +import { error, info } from '../logger.service'; import { Trade } from '../../entities/trade.entities'; import { isFTXSpot } from '../../utils/exchanges/ftx.utils'; -import { OPEN_TRADE_ERROR_MAX_SIZE } from '../../messages/trading.messages'; +import { + OPEN_TRADE_ERROR_MAX_SIZE, + REVERSING_TRADE, + TRADE_OVERFLOW +} from '../../messages/trading.messages'; import { OpenPositionError } from '../../errors/trading.errors'; import { CompositeExchangeService } from './base/composite.exchange.service'; import { IFTXFuturesPosition } from '../../interfaces/exchanges/ftx.exchange.interfaces'; import { getTokensAmount, - getOrderCost, - getRelativeOrderSize + getRelativeOrderSize, + getTokensPrice } from '../../utils/trading/conversion.utils'; -import { getInvertedSide, getSide } from '../../utils/trading/side.utils'; +import { + getInvertedSide, + getSide, + isSideDifferent +} from '../../utils/trading/side.utils'; export class FTXExchangeService extends CompositeExchangeService { constructor() { @@ -69,11 +77,11 @@ export class FTXExchangeService extends CompositeExchangeService { } }; - // TODO refacto handleMaxBudget = async ( account: Account, ticker: Ticker, - trade: Trade + trade: Trade, + balance: number ): Promise => { const { max, direction, size } = trade; const { symbol } = ticker; @@ -83,16 +91,17 @@ export class FTXExchangeService extends CompositeExchangeService { let current = 0; if (isFTXSpot(ticker)) { const balance = await this.getTickerBalance(account, ticker); - current = getOrderCost(ticker, this.exchangeId, balance); + current = getTokensPrice(ticker, this.exchangeId, balance); } else { - // TODO refacto - try { - current = await this.getTickerPositionSize(account, ticker); - } catch (err) { - // silent - } + current = await this.getTickerPositionSize(account, ticker); } - if (Math.abs(current) + Number(size) > Number(max)) { + if ( + current + + (size.includes('%') + ? getRelativeOrderSize(balance, size) + : Number(size)) > + Number(max) + ) { error( OPEN_TRADE_ERROR_MAX_SIZE(this.exchangeId, accountId, symbol, side, max) ); @@ -102,82 +111,67 @@ export class FTXExchangeService extends CompositeExchangeService { } }; - handleReverseOrder( + handleReverseOrder = async ( account: Account, ticker: Ticker, trade: Trade - ): Promise { - throw new Error('Method not implemented.'); - } - handleOverflow( + ): Promise => { + const { direction } = trade; + const { symbol } = ticker; + const accountId = getAccountId(account); + try { + const { side, cost } = (await this.getTickerPosition( + account, + ticker + )) as IFTXFuturesPosition; + if (isSideDifferent(side as Side, direction)) { + info(REVERSING_TRADE(this.exchangeId, accountId, symbol)); + const size = Math.abs(Number(cost)).toString(); + await this.closeOrder(account, { ...trade, size }, ticker); + } + } catch (err) { + // ignore throw + } + }; + + handleOverflow = async ( account: Account, ticker: Ticker, trade: Trade - ): Promise { - throw new Error('Method not implemented.'); - } - - // handleReverseOrder = async ( - // account: Account, - // ticker: Ticker, - // trade: Trade - // ): Promise => { - // const { direction } = trade; - // const accountId = getAccountId(account); - // try { - // const position = (await this.getTickerPosition( - // account, - // ticker - // )) as IFTXFuturesPosition; - // if (position && isSideDifferent(position.side as Side, direction)) { - // info(REVERSING_TRADE(this.exchangeId, accountId, ticker.symbol)); - // await this.closeOrder(account, trade, ticker); - // } - // } catch (err) { - // // ignore throw - // } - // }; - - // handleOverflow = async ( - // account: Account, - // ticker: Ticker, - // trade: Trade - // ): Promise => { - // const { direction, size } = trade; - // const { symbol, info } = ticker; - // const accountId = getAccountId(account); - // try { - // if (isFTXSpot(ticker)) { - // const balance = await this.getTickerBalance(account, ticker); - // const cost = getOrderCost(ticker, this.exchangeId, balance); - // if (cost && getSide(direction) === Side.Sell && cost < Number(size)) { - // info(TRADE_OVERFLOW(this.exchangeId, accountId, symbol)); - // await this.closeOrder( - // account, - // { ...trade, size: balance.toString() }, - // ticker - // ); - // return true; - // } - // } else { - // const position = (await this.getTickerPosition( - // account, - // ticker - // )) as IFTXFuturesPosition; - // const { side, cost } = position; - // if ( - // position && - // isSideDifferent(side as Side, direction) && - // Number(size) > Math.abs(Number(cost)) - // ) { - // info(TRADE_OVERFLOW(this.exchangeId, accountId, symbol)); - // await this.closeOrder(account, trade, ticker); - // return true; - // } - // } - // } catch (err) { - // // ignore throw - // } - // return false; - // }; + ): Promise => { + const { direction, size } = trade; + const { symbol } = ticker; + const accountId = getAccountId(account); + try { + if (isFTXSpot(ticker)) { + const balance = await this.getTickerBalance(account, ticker); + const cost = getTokensPrice(ticker, this.exchangeId, balance); + if (cost && getSide(direction) === Side.Sell && cost < Number(size)) { + info(TRADE_OVERFLOW(this.exchangeId, accountId, symbol)); + await this.closeOrder( + account, + { ...trade, size: balance.toString() }, + ticker + ); + return true; + } + } else { + const { side, cost } = (await this.getTickerPosition( + account, + ticker + )) as IFTXFuturesPosition; + if ( + isSideDifferent(side as Side, direction) && + Number(size) > Math.abs(Number(cost)) + ) { + info(TRADE_OVERFLOW(this.exchangeId, accountId, symbol)); + await this.closeOrder(account, trade, ticker); + return true; + } + } + } catch (err) { + // ignore throw + } + return false; + }; } diff --git a/src/services/exchanges/kraken.exchange.service.ts b/src/services/exchanges/kraken.exchange.service.ts index dac8cc0..008db0a 100644 --- a/src/services/exchanges/kraken.exchange.service.ts +++ b/src/services/exchanges/kraken.exchange.service.ts @@ -1,8 +1,4 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Ticker } from 'ccxt'; import { ExchangeId } from '../../constants/exchanges.constants'; -import { Account } from '../../entities/account.entities'; -import { Trade } from '../../entities/trade.entities'; import { SpotExchangeService } from './base/spot.exchange.service'; // TODO replace by a composite exchange @@ -10,18 +6,4 @@ export class KrakenExchangeService extends SpotExchangeService { constructor() { super(ExchangeId.Kraken); } - handleReverseOrder( - account: Account, - ticker: Ticker, - trade: Trade - ): Promise { - throw new Error('Method not implemented.'); - } - handleOverflow( - account: Account, - ticker: Ticker, - trade: Trade - ): Promise { - throw new Error('Method not implemented.'); - } } diff --git a/src/services/exchanges/kucoin.exchange.service.ts b/src/services/exchanges/kucoin.exchange.service.ts index 3eb586d..3ee5b0d 100644 --- a/src/services/exchanges/kucoin.exchange.service.ts +++ b/src/services/exchanges/kucoin.exchange.service.ts @@ -1,8 +1,4 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Ticker } from 'ccxt'; import { ExchangeId } from '../../constants/exchanges.constants'; -import { Account } from '../../entities/account.entities'; -import { Trade } from '../../entities/trade.entities'; import { SpotExchangeService } from './base/spot.exchange.service'; // TODO replace by a composite exchange @@ -10,18 +6,4 @@ export class KuCoinExchangeService extends SpotExchangeService { constructor() { super(ExchangeId.KuCoin); } - handleReverseOrder( - account: Account, - ticker: Ticker, - trade: Trade - ): Promise { - throw new Error('Method not implemented.'); - } - handleOverflow( - account: Account, - ticker: Ticker, - trade: Trade - ): Promise { - throw new Error('Method not implemented.'); - } } diff --git a/src/utils/trading/conversion.utils.ts b/src/utils/trading/conversion.utils.ts index 6484c65..1210f14 100644 --- a/src/utils/trading/conversion.utils.ts +++ b/src/utils/trading/conversion.utils.ts @@ -16,9 +16,8 @@ export const getRelativeOrderSize = (balance: number, size: string): number => { error(TRADE_ERROR_SIZE(size)); throw new OrderSizeError(TRADE_ERROR_SIZE(size)); } - const orderSize = (balance * percent) / 100; // debug(TRADE_CALCULATED_RELATIVE_SIZE(balance.toFixed(2), size, orderSize)); - return orderSize; + return (balance * percent) / 100; }; export const getTokensAmount = ( @@ -36,10 +35,12 @@ export const getTokensAmount = ( }; export const getTokensPrice = ( - symbol: string, - price: number, + ticker: Ticker, + exchangeId: ExchangeId, tokens: number ): number => { + const { symbol } = ticker; + const price = getTickerPrice(ticker, exchangeId); const size = price * tokens; if (isNaN(size)) { error(TRADE_CALCULATED_SIZE_ERROR(symbol)); @@ -48,13 +49,3 @@ export const getTokensPrice = ( debug(TRADE_CALCULATED_SIZE(symbol, tokens, size.toFixed(2))); return size; }; - -export const getOrderCost = ( - ticker: Ticker, - exchangeId: ExchangeId, - tokens: number -): number => { - const { symbol } = ticker; - const price = getTickerPrice(ticker, exchangeId); - return getTokensPrice(symbol, price, tokens); -}; diff --git a/src/utils/trading/position.utils.ts b/src/utils/trading/position.utils.ts index 9e33e14..cf8ee29 100644 --- a/src/utils/trading/position.utils.ts +++ b/src/utils/trading/position.utils.ts @@ -35,6 +35,6 @@ export const filterPositions = ( const predicate = exchangeId === ExchangeId.BinanceFuturesUSD ? (p: IBinanceFuturesUSDPosition) => Number(p.notional) - : (p: IFTXFuturesPosition) => Number(p.size); + : (p: IFTXFuturesPosition) => Number(p.cost); return positions.filter(predicate); }; diff --git a/src/utils/trading/symbol.utils.ts b/src/utils/trading/symbol.utils.ts index 869c468..fe5ce4d 100644 --- a/src/utils/trading/symbol.utils.ts +++ b/src/utils/trading/symbol.utils.ts @@ -1,3 +1,3 @@ -export const getSpotSymbol = (symbol: string): string => symbol.split('/')[0]; +export const getSymbol = (symbol: string): string => symbol.split('/')[0]; -export const getSpotQuote = (symbol: string): string => symbol.split('/')[1]; +export const getQuote = (symbol: string): string => symbol.split('/')[1];