diff --git a/instance.js.dist_trade_bybit.js b/instance.js.dist_trade_bybit.js new file mode 100644 index 000000000..aa655fc9e --- /dev/null +++ b/instance.js.dist_trade_bybit.js @@ -0,0 +1,16 @@ +const c = (module.exports = {}); + +c.symbols = []; + +c.init = async () => { + const j = ['BTC/USDT:USDT', 'BTC/USDC:USDC']; + + j.forEach(pair => { + c.symbols.push({ + symbol: pair, + periods: ['1m', '15m', '1h'], + exchange: 'bybit_unified', + state: 'watch' + }); + }); +}; diff --git a/instance.js.dist_trade_bybit_inverse b/instance.js.dist_trade_bybit_inverse deleted file mode 100644 index 21bc895a5..000000000 --- a/instance.js.dist_trade_bybit_inverse +++ /dev/null @@ -1,426 +0,0 @@ -var c = module.exports = {} - -c.symbols = [] - -const InstanceUtil = require('./src/utils/instance_util'); - -c.init = async () => { - - c.symbols.push( - ...(await InstanceUtil.bybitInit(pair => { - - if (['ADAUSD'].includes(pair.symbol)) { - pair.extra = { - 'bybit_leverage': 10 - }; - pair.trade = { - 'currency_capital': 1, - 'strategies': [ - { - 'strategy': 'dip_catcher', - 'interval': '15m', - 'options': { - 'period': '15m', - 'trend_cloud_multiplier': 4, - 'hma_high_period': 9, - 'hma_high_candle_source': 'close', - 'hma_low_period': 9, - 'hma_low_candle_source': 'close' - } - }, - { - 'strategy': 'dca_dipper', - 'interval': '15m', - 'options': { - 'period': '15m', - 'amount_currency': '2', - 'percent_below_price': 0.1, - 'hma_period': 9, - 'hma_source': 'close' - } - } - ] - }; - pair.watchdogs = [ - { -// 'name': 'stoploss_watch', -// 'stop': 1.2 - } - ]; - return pair; - } - - if (['BITUSD'].includes(pair.symbol)) { - pair.extra = { - 'bybit_leverage': 10 - }; - pair.trade = { - 'currency_capital': 1, - 'strategies': [ - { - 'strategy': 'dip_catcher', - 'interval': '15m', - 'options': { - 'period': '15m', - 'trend_cloud_multiplier': 4, - 'hma_high_period': 9, - 'hma_high_candle_source': 'close', - 'hma_low_period': 9, - 'hma_low_candle_source': 'close' - } - }, - { - 'strategy': 'dca_dipper', - 'interval': '15m', - 'options': { - 'period': '15m', - 'amount_currency': '2', - 'percent_below_price': 0.1, - 'hma_period': 9, - 'hma_source': 'close' - } - } - ] - }; - pair.watchdogs = [ - { -// 'name': 'stoploss_watch', -// 'stop': 1.2 - } - ]; - return pair; - } - - if (['BTCUSD'].includes(pair.symbol)) { - pair.extra = { - 'bybit_leverage': 10 - }; - pair.trade = { - 'currency_capital': 1, - 'strategies': [ - { - 'strategy': 'dip_catcher', - 'interval': '15m', - 'options': { - 'period': '15m', - 'trend_cloud_multiplier': 4, - 'hma_high_period': 9, - 'hma_high_candle_source': 'close', - 'hma_low_period': 9, - 'hma_low_candle_source': 'close' - } - }, - { - 'strategy': 'dca_dipper', - 'interval': '15m', - 'options': { - 'period': '15m', - 'amount_currency': '2', - 'percent_below_price': 0.1, - 'hma_period': 9, - 'hma_source': 'close' - } - } - ] - }; - pair.watchdogs = [ - { -// 'name': 'stoploss_watch', -// 'stop': 1.2 - } - ]; - return pair; - } - - if (['DOTUSD'].includes(pair.symbol)) { - pair.extra = { - 'bybit_leverage': 10 - }; - pair.trade = { - 'currency_capital': 1, - 'strategies': [ - { - 'strategy': 'dip_catcher', - 'interval': '15m', - 'options': { - 'period': '15m', - 'trend_cloud_multiplier': 4, - 'hma_high_period': 9, - 'hma_high_candle_source': 'close', - 'hma_low_period': 9, - 'hma_low_candle_source': 'close' - } - }, - { - 'strategy': 'dca_dipper', - 'interval': '15m', - 'options': { - 'period': '15m', - 'amount_currency': '2', - 'percent_below_price': 0.1, - 'hma_period': 9, - 'hma_source': 'close' - } - } - ] - }; - pair.watchdogs = [ - { -// 'name': 'stoploss_watch', -// 'stop': 1.2 - } - ]; - return pair; - } - - if (['EOSUSD'].includes(pair.symbol)) { - pair.extra = { - 'bybit_leverage': 10 - }; - pair.trade = { - 'currency_capital': 1, - 'strategies': [ - { - 'strategy': 'dip_catcher', - 'interval': '15m', - 'options': { - 'period': '15m', - 'trend_cloud_multiplier': 4, - 'hma_high_period': 9, - 'hma_high_candle_source': 'close', - 'hma_low_period': 9, - 'hma_low_candle_source': 'close' - } - }, - { - 'strategy': 'dca_dipper', - 'interval': '15m', - 'options': { - 'period': '15m', - 'amount_currency': '2', - 'percent_below_price': 0.1, - 'hma_period': 9, - 'hma_source': 'close' - } - } - ] - }; - pair.watchdogs = [ - { -// 'name': 'stoploss_watch', -// 'stop': 1.2 - } - ]; - return pair; - } - - if (['ETHUSD'].includes(pair.symbol)) { - pair.extra = { - 'bybit_leverage': 10 - }; - pair.trade = { - 'currency_capital': 1, - 'strategies': [ - { - 'strategy': 'dip_catcher', - 'interval': '15m', - 'options': { - 'period': '15m', - 'trend_cloud_multiplier': 4, - 'hma_high_period': 9, - 'hma_high_candle_source': 'close', - 'hma_low_period': 9, - 'hma_low_candle_source': 'close' - } - }, - { - 'strategy': 'dca_dipper', - 'interval': '15m', - 'options': { - 'period': '15m', - 'amount_currency': '2', - 'percent_below_price': 0.1, - 'hma_period': 9, - 'hma_source': 'close' - } - } - ] - }; - pair.watchdogs = [ - { -// 'name': 'stoploss_watch', -// 'stop': 1.2 - } - ]; - return pair; - } - - if (['LTCUSD'].includes(pair.symbol)) { - pair.extra = { - 'bybit_leverage': 10 - }; - pair.trade = { - 'currency_capital': 1, - 'strategies': [ - { - 'strategy': 'dip_catcher', - 'interval': '15m', - 'options': { - 'period': '15m', - 'trend_cloud_multiplier': 4, - 'hma_high_period': 9, - 'hma_high_candle_source': 'close', - 'hma_low_period': 9, - 'hma_low_candle_source': 'close' - } - }, - { - 'strategy': 'dca_dipper', - 'interval': '15m', - 'options': { - 'period': '15m', - 'amount_currency': '2', - 'percent_below_price': 0.1, - 'hma_period': 9, - 'hma_source': 'close' - } - } - ] - }; - pair.watchdogs = [ - { -// 'name': 'stoploss_watch', -// 'stop': 1.2 - } - ]; - return pair; - } - - if (['MANAUSD'].includes(pair.symbol)) { - pair.extra = { - 'bybit_leverage': 10 - }; - pair.trade = { - 'currency_capital': 1, - 'strategies': [ - { - 'strategy': 'dip_catcher', - 'interval': '15m', - 'options': { - 'period': '15m', - 'trend_cloud_multiplier': 4, - 'hma_high_period': 9, - 'hma_high_candle_source': 'close', - 'hma_low_period': 9, - 'hma_low_candle_source': 'close' - } - }, - { - 'strategy': 'dca_dipper', - 'interval': '15m', - 'options': { - 'period': '15m', - 'amount_currency': '2', - 'percent_below_price': 0.1, - 'hma_period': 9, - 'hma_source': 'close' - } - } - ] - }; - pair.watchdogs = [ - { -// 'name': 'stoploss_watch', -// 'stop': 1.2 - } - ]; - return pair; - } - - if (['SOLUSD'].includes(pair.symbol)) { - pair.extra = { - 'bybit_leverage': 10 - }; - pair.trade = { - 'currency_capital': 1, - 'strategies': [ - { - 'strategy': 'dip_catcher', - 'interval': '15m', - 'options': { - 'period': '15m', - 'trend_cloud_multiplier': 4, - 'hma_high_period': 9, - 'hma_high_candle_source': 'close', - 'hma_low_period': 9, - 'hma_low_candle_source': 'close' - } - }, - { - 'strategy': 'dca_dipper', - 'interval': '15m', - 'options': { - 'period': '15m', - 'amount_currency': '2', - 'percent_below_price': 0.1, - 'hma_period': 9, - 'hma_source': 'close' - } - } - ] - }; - pair.watchdogs = [ - { -// 'name': 'stoploss_watch', -// 'stop': 1.2 - } - ]; - return pair; - } - - if (['XRPUSD'].includes(pair.symbol)) { - pair.extra = { - 'bybit_leverage': 10 - }; - pair.trade = { - 'currency_capital': 1, - 'strategies': [ - { - 'strategy': 'dip_catcher', - 'interval': '15m', - 'options': { - 'period': '15m', - 'trend_cloud_multiplier': 4, - 'hma_high_period': 9, - 'hma_high_candle_source': 'close', - 'hma_low_period': 9, - 'hma_low_candle_source': 'close' - } - }, - { - 'strategy': 'dca_dipper', - 'interval': '15m', - 'options': { - 'period': '15m', - 'amount_currency': '2', - 'percent_below_price': 0.1, - 'hma_period': 9, - 'hma_source': 'close' - } - } - ] - }; - pair.watchdogs = [ - { -// 'name': 'stoploss_watch', -// 'stop': 1.2 - } - ]; - return pair; - } - - return undefined; - - })) - ); -}; diff --git a/src/dict/exchange_order.js b/src/dict/exchange_order.js index 0210f6375..277d49299 100644 --- a/src/dict/exchange_order.js +++ b/src/dict/exchange_order.js @@ -51,21 +51,7 @@ module.exports = class ExchangeOrder { return 'trailing_stop'; } - constructor( - id, - symbol, - status, - price, - amount, - retry, - ourId, - side, - type, - createdAt, - updatedAt, - raw = undefined, - options = {} - ) { + constructor(id, symbol, status, price, amount, retry, ourId, side, type, createdAt, updatedAt, raw = undefined, options = {}) { if (side !== 'buy' && side !== 'sell') { throw `Invalid order direction given:${side}`; } @@ -122,6 +108,10 @@ module.exports = class ExchangeOrder { return this.getLongOrShortSide() === 'short'; } + getStatus() { + return this.status; + } + getLongOrShortSide() { switch (this.side) { case 'buy': @@ -178,17 +168,7 @@ module.exports = class ExchangeOrder { side = 'sell'; } - return new ExchangeOrder( - order.id, - order.symbol, - 'canceled', - order.price, - order.amount, - false, - order.ourId, - side, - order.type - ); + return new ExchangeOrder(order.id, order.symbol, 'canceled', order.price, order.amount, false, order.ourId, side, order.type); } static createRejectedFromOrder(order, message = undefined) { @@ -204,17 +184,6 @@ module.exports = class ExchangeOrder { raw.message = message; } - return new ExchangeOrder( - order.id, - order.symbol, - 'rejected', - order.price, - order.amount, - false, - order.ourId, - side, - order.type, - raw - ); + return new ExchangeOrder(order.id, order.symbol, 'rejected', order.price, order.amount, false, order.ourId, side, order.type, raw); } }; diff --git a/src/exchange/bybit_unified.js b/src/exchange/bybit_unified.js index be4bd43af..cd4dff62a 100644 --- a/src/exchange/bybit_unified.js +++ b/src/exchange/bybit_unified.js @@ -3,6 +3,12 @@ const ccxtpro = require('ccxt').pro; const Ticker = require('../dict/ticker'); const TickerEvent = require('../event/ticker_event'); const ExchangeCandlestick = require('../dict/exchange_candlestick'); +const Position = require('../dict/position'); +const CommonUtil = require('../utils/common_util'); +const ExchangeOrder = require('../dict/exchange_order'); +const OrderBag = require('./utils/order_bag'); +const Order = require('../dict/order'); +const orderUtil = require('../utils/order_util'); module.exports = class BybitUnified { constructor(eventEmitter, requestClient, candlestickResample, logger, queue, candleImporter, throttler) { @@ -30,6 +36,7 @@ module.exports = class BybitUnified { const { lotSizes } = this; this.intervals = []; const me = this; + this.orderbag = new OrderBag(); this.symbols = symbols; this.positions = {}; @@ -97,15 +104,11 @@ module.exports = class BybitUnified { // const tickers = await exchange.watchTickers(['BTC/USDT:USDT']); // console.log(tickers); - /** - * if (config.key && config.secret && config.key.length > 0 && config.secret.length > 0) { - * me.logger.info('BybitLinear: sending auth request'); - * me.apiKey = config.key; - * me.apiSecret = config.secret; - * } else { - * me.logger.info('BybitLinear: Starting as anonymous; no trading possible'); - * } - */ + if (config.key && config.secret && config.key.length > 0 && config.secret.length > 0) { + this.authInit(config.key, config.secret); + } else { + me.logger.info('BybitLinear: Starting as anonymous; no trading possible'); + } symbols.forEach(symbol => { symbol.periods.forEach(period => { @@ -129,24 +132,189 @@ module.exports = class BybitUnified { }); } - async getOrders() { - return []; + authInit(apiKey, secret) { + const exchange = new ccxtpro.bybit({ + apiKey: apiKey, + secret: secret, + newUpdates: true + }); + + this.exchangeAuth = exchange; + const me = this; + + setTimeout(async () => { + await this.updatePostionsViaRest(exchange); + await this.updateOrderViaRest(exchange); + }, 2000); + + setInterval(() => this.updatePostionsViaRest(exchange), 31700); + setInterval(() => this.updateOrderViaRest(exchange), 32900); + + setTimeout(async () => { + while (true) { + try { + const positions = await exchange.watchPositions(); + BybitUnified.createPositionsWithOpenStateOnly(positions).forEach(position => { + me.positions[position.symbol] = position; + }); + } catch (e) { + console.error(`${this.getName()}: watchPositions error: ${e.message}`); + this.logger.error(`${this.getName()}: watchPositions error: ${e.message}`); + } + } + }, 1000); + + setTimeout(async () => { + while (true) { + try { + const orders = await exchange.watchOrders(); + BybitUnified.createOrders(orders).forEach(o => me.orderbag.triggerOrder(o)); + } catch (e) { + console.error(`${this.getName()}: watchOrders error: ${e.message}`); + this.logger.error(`${this.getName()}: watchOrders error: ${e.message}`); + } + } + }, 1000); } - async findOrderById(id) { - return null; + async updateOrderViaRest(exchange, me) { + try { + const orders = await exchange.fetchOpenOrders(); + this.orderbag.set(BybitUnified.createOrders(orders)); + this.logger.debug(`${this.getName()}: orders via API updated: ${Object.keys(this.positions).length}`); + } catch (e) { + console.log(`${this.getName()}: orders via API error: ${e.message}`); + this.logger.error(`${this.getName()}: orders via API error: ${e.message}`); + } } - async getOrdersForSymbol(symbol) { - return null; + async updatePostionsViaRest(exchange) { + try { + const positions = await exchange.fetchPositions(); + + const positionsFinal = {}; + BybitUnified.createPositionsWithOpenStateOnly(positions).forEach(position => { + positionsFinal[position.symbol] = position; + }); + + this.positions = positionsFinal; + this.logger.debug(`${this.getName()}: positions via API updated: ${Object.keys(this.positions).length}`); + } catch (e) { + console.log(`${this.getName()}: positions via API error: ${e.message}`); + this.logger.error(`${this.getName()}: positions via API error: ${e.message}`); + } + } + + static createOrders(orders) { + const myOrders = []; + + orders.forEach(order => { + let status; + switch (order.status) { + case 'open': + status = ExchangeOrder.STATUS_OPEN; + break; + case 'closed': + status = ExchangeOrder.STATUS_DONE; + break; + case 'canceled': + status = ExchangeOrder.STATUS_CANCELED; + break; + case 'rejected': + case 'expired': + status = ExchangeOrder.STATUS_REJECTED; + break; + default: + console.error(`invalid order status: ${order.status}`); + return; + } + + let orderType; + switch (order.type) { + case 'limit': + orderType = ExchangeOrder.TYPE_LIMIT; + break; + case 'market': + orderType = ExchangeOrder.TYPE_MARKET; + break; + default: + console.error(`invalid order type: ${order.type}`); + return; + } + + myOrders.push( + new ExchangeOrder( + order.id, + order.symbol, + order.status, + order.price, + order.qty, + status === ExchangeOrder.STATUS_REJECTED, + order.clientOrderId ? order.clientOrderId : undefined, + order.side.toLowerCase() === 'buy' ? 'buy' : 'sell', // secure the value, + orderType, + new Date(isNaN(order.timestamp) ? order.timestamp : parseInt(order.timestamp)), + new Date(), + JSON.parse(JSON.stringify(order)), + { + post_only: order.postOnly || false, + reduce_only: order.reduceOnly || false + } + ) + ); + }); + + return myOrders; + } + + static createPositionsWithOpenStateOnly(positions) { + return positions + .filter(position => ['short', 'long'].includes(position.side?.toLowerCase())) + .map(position => { + const side = position.side.toLowerCase(); + let size = position.contracts; + + if (side === 'short') { + size *= -1; + } + + return new Position( + position.symbol, + side, + size, + position.markPrice && position.entryPrice ? CommonUtil.getProfitAsPercent(side, position.markPrice, position.entryPrice) : undefined, + new Date(), + parseFloat(position.entryPrice), + new Date(), + position + ); + }); + } + + getOrders() { + return this.orderbag.getOrders(); + } + + findOrderById(id) { + return this.orderbag.findOrderById(id); + } + + getOrdersForSymbol(symbol) { + return this.orderbag.getOrdersForSymbol(symbol); } async getPositions() { - return []; + return Object.values(this.positions); } async getPositionForSymbol(symbol) { - return null; + for (const position of await this.getPositions()) { + if (position.symbol === symbol) { + return position; + } + } + + return undefined; } /** @@ -157,16 +325,11 @@ module.exports = class BybitUnified { * @returns {*} */ calculatePrice(price, symbol) { - throw Error('not supported'); - } + if (!(symbol in this.tickSizes)) { + return undefined; + } - /** - * Force an order update only if order is "not closed" for any reason already by exchange - * - * @param order - */ - triggerOrder(order) { - throw Error('not supported'); + return orderUtil.calculateNearestSize(price, this.tickSizes[symbol]); } /** @@ -177,7 +340,11 @@ module.exports = class BybitUnified { * @returns {*} */ calculateAmount(amount, symbol) { - throw Error('not supported'); + if (!(symbol in this.lotSizes)) { + return undefined; + } + + return orderUtil.calculateNearestSize(amount, this.lotSizes[symbol]); } getName() { @@ -185,14 +352,64 @@ module.exports = class BybitUnified { } async order(order) { - throw Error('not supported'); + let orderType; + switch (order.getType()) { + case Order.TYPE_LIMIT: + orderType = 'limit'; + break; + case Order.TYPE_MARKET: + orderType = 'market'; + break; + default: + console.error(`${this.getName()}: invalid orderType: ${order.getType()}`); + this.logger.error(`${this.getName()}: invalid orderType: ${order.getType()}`); + return undefined; + } + + const params = { + postOnly: order.isPostOnly(), + reduceOnly: order.isReduceOnly() + }; + + let placedOrder; + try { + placedOrder = await this.exchangeAuth.createOrder( + order.getSymbol(), + orderType, + order.isLong() ? 'buy' : 'sell', + order.getAmount(), + order.getPrice(), + params + ); + } catch (e) { + this.logger.error(`${this.getName()}: order place error: ${e.message} ${JSON.stringify(order)}`); + return ExchangeOrder.createRejectedFromOrder(order, e.message); + } + + // wait what we get + await this.exchangeAuth.sleep(1000); + const o = await this.exchangeAuth.fetchOpenOrder(placedOrder.id); + return BybitUnified.createOrders([o])[0]; } async cancelOrder(id) { - throw Error('not supported'); + const order = await this.findOrderById(id); + try { + await this.exchangeAuth.cancelOrder(id, order.getSymbol()); + } catch (e) { + this.logger.error(`${this.getName()}: order cancel error: ${e.message} ${JSON.stringify(id)}`); + } } async cancelAll(symbol) { - throw Error('not supported'); + try { + await this.exchangeAuth.cancelAllOrders(symbol); + } catch (e) { + this.logger.error(`${this.getName()}: order cancel all error: ${e.message} ${JSON.stringify(symbol)}`); + } + } + + isInverseSymbol(symbol) { + return false; } }; diff --git a/src/modules/http.js b/src/modules/http.js index 3e8082539..87bf49330 100644 --- a/src/modules/http.js +++ b/src/modules/http.js @@ -8,20 +8,7 @@ const moment = require('moment'); const OrderUtil = require('../utils/order_util'); module.exports = class Http { - constructor( - systemUtil, - ta, - signalHttp, - backtest, - exchangeManager, - pairsHttp, - logsHttp, - candleExportHttp, - candleImporter, - ordersHttp, - tickers, - projectDir - ) { + constructor(systemUtil, ta, signalHttp, backtest, exchangeManager, pairsHttp, logsHttp, candleExportHttp, candleImporter, ordersHttp, tickers, projectDir) { this.systemUtil = systemUtil; this.ta = ta; this.signalHttp = signalHttp; @@ -37,7 +24,7 @@ module.exports = class Http { } start() { - twig.extendFilter('price_format', function(value) { + twig.extendFilter('price_format', value => { if (parseFloat(value) < 1) { return Intl.NumberFormat('en-US', { useGrouping: false, @@ -58,31 +45,19 @@ module.exports = class Http { .update(String(Math.floor(Date.now() / 1000))) .digest('hex') .substring(0, 8); - twig.extendFunction('asset_version', function() { - return assetVersion; - }); + twig.extendFunction('asset_version', () => assetVersion); const desks = this.systemUtil.getConfig('desks', []).map(desk => desk.name); - twig.extendFunction('desks', function() { - return desks; - }); + twig.extendFunction('desks', () => desks); - twig.extendFunction('node_version', function() { - return process.version; - }); + twig.extendFunction('node_version', () => process.version); - twig.extendFunction('memory_usage', function() { - return Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100; - }); + twig.extendFunction('memory_usage', () => Math.round((process.memoryUsage().heapUsed / 1024 / 1024) * 100) / 100); const up = new Date(); - twig.extendFunction('uptime', function() { - return moment(up).toNow(true); - }); + twig.extendFunction('uptime', () => moment(up).toNow(true)); - twig.extendFilter('format_json', function(value) { - return JSON.stringify(value, null, '\t'); - }); + twig.extendFilter('format_json', value => JSON.stringify(value, null, '\t')); const app = express(); @@ -116,10 +91,7 @@ module.exports = class Http { const { ta } = this; app.get('/', async (req, res) => { - res.render( - '../templates/base.html.twig', - await ta.getTaForPeriods(this.systemUtil.getConfig('dashboard.periods', ['15m', '1h'])) - ); + res.render('../templates/base.html.twig', await ta.getTaForPeriods(this.systemUtil.getConfig('dashboard.periods', ['15m', '1h']))); }); app.get('/backtest', async (req, res) => { @@ -136,23 +108,21 @@ module.exports = class Http { pairs = [pairs]; } - const asyncs = pairs.map(pair => { - return async () => { - const p = pair.split('.'); - - return { - pair: pair, - result: await this.backtest.getBacktestResult( - parseInt(req.body.ticker_interval, 10), - req.body.hours, - req.body.strategy, - req.body.candle_period, - p[0], - p[1], - req.body.options ? JSON.parse(req.body.options) : {}, - req.body.initial_capital - ) - }; + const asyncs = pairs.map(pair => async () => { + const p = pair.split('.'); + + return { + pair: pair, + result: await this.backtest.getBacktestResult( + parseInt(req.body.ticker_interval, 10), + req.body.hours, + req.body.strategy, + req.body.candle_period, + p[0], + p[1], + req.body.options ? JSON.parse(req.body.options) : {}, + req.body.initial_capital + ) }; }); @@ -219,21 +189,13 @@ module.exports = class Http { app.get('/tools/candles', async (req, res) => { const options = { pairs: await this.candleExportHttp.getPairs(), - start: moment() - .subtract(7, 'days') - .toDate(), + start: moment().subtract(7, 'days').toDate(), end: new Date() }; if (req.query.pair && req.query.period && req.query.period && req.query.start && req.query.end) { const [exchange, symbol] = req.query.pair.split('.'); - const candles = await this.candleExportHttp.getCandles( - exchange, - symbol, - req.query.period, - new Date(req.query.start), - new Date(req.query.end) - ); + const candles = await this.candleExportHttp.getCandles(exchange, symbol, req.query.period, new Date(req.query.start), new Date(req.query.end)); if (req.query.metadata) { candles.map(c => { @@ -365,12 +327,12 @@ module.exports = class Http { app.get('/orders/:pair/cancel/:id', async (req, res) => { const foo = await this.ordersHttp.cancel(req.params.pair, req.params.id); - res.redirect(`/orders/${req.params.pair}`); + res.redirect(`/orders/${encodeURIComponent(req.params.pair)}`); }); app.get('/orders/:pair/cancel-all', async (req, res) => { await this.ordersHttp.cancelAll(req.params.pair); - res.redirect(`/orders/${req.params.pair}`); + res.redirect(`/orders/${encodeURIComponent(req.params.pair)}`); }); app.get('/trades', async (req, res) => { @@ -393,10 +355,7 @@ module.exports = class Http { let currencyValue; let currencyProfit; - if ( - (exchangeName.includes('bitmex') && ['XBTUSD', 'ETHUSD'].includes(position.symbol)) || - exchangeName === 'bybit' - ) { + if ((exchangeName.includes('bitmex') && ['XBTUSD', 'ETHUSD'].includes(position.symbol)) || exchangeName === 'bybit') { // inverse exchanges currencyValue = Math.abs(position.amount); } else if (position.amount && position.entry) { @@ -407,9 +366,7 @@ module.exports = class Http { exchange: exchangeName, position: position, currency: currencyValue, - currencyProfit: position.getProfit() - ? currencyValue + (currencyValue / 100) * position.getProfit() - : undefined + currencyProfit: position.getProfit() ? currencyValue + (currencyValue / 100) * position.getProfit() : undefined }); }); @@ -463,11 +420,20 @@ module.exports = class Http { mySymbol += 'PERP'; } + if (mySymbol.includes('bybit_unified') && mySymbol.endsWith(':USDT')) { + mySymbol = mySymbol.replace(':USDT', '.P').replace('/', ''); + } + + if (mySymbol.includes('bybit_unified') && mySymbol.endsWith(':USDC')) { + mySymbol = mySymbol.replace(':USDC', '.P').replace('/', ''); + } + return mySymbol .replace('-', '') .replace('coinbase_pro', 'coinbase') .replace('binance_margin', 'binance') .replace('bybit_linear', 'bybit') + .replace('bybit_unified', 'bybit') .toUpperCase(); } }; diff --git a/templates/base.html.twig b/templates/base.html.twig index 7abf9fe92..aec7f5aa2 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -57,7 +57,7 @@ {% for row in rows %} - {{ row.symbol }} + {{ row.symbol }} {{ row.ticker.bid|price_format }} {% if row.percentage_change %}{{ row.percentage_change|round(1) }} %{% endif %} diff --git a/templates/pairs.html.twig b/templates/pairs.html.twig index 421521bb5..28ea2b9d7 100644 --- a/templates/pairs.html.twig +++ b/templates/pairs.html.twig @@ -66,7 +66,7 @@ {% for pair in pairs %} {{ pair.exchange }} - {{ pair.symbol }} + {{ pair.symbol }} {{ pair.is_trading ? '' : '' }} {% if pair.is_trading %}{{ pair.trade_capital|default(0) }} / {{ pair.trade_currency_capital|default(0) }} / {{ pair.trade_balance_percent|default(0) }}% {% endif %} {% if pair.strategies.length > 0 %}{{ pair.strategies|json_encode }}{% endif %} @@ -124,4 +124,4 @@ {% block javascript %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/web/static/img/exchanges/bybit_unified.png b/web/static/img/exchanges/bybit_unified.png new file mode 100644 index 000000000..4fabd0f67 Binary files /dev/null and b/web/static/img/exchanges/bybit_unified.png differ diff --git a/web/static/js/trades.vue b/web/static/js/trades.vue index a6b579317..58d4f3b76 100644 --- a/web/static/js/trades.vue +++ b/web/static/js/trades.vue @@ -25,7 +25,7 @@ - {{ position.position.symbol }} + {{ position.position.symbol }} {{ position.position.amount }} @@ -106,7 +106,7 @@ {{ order.order.symbol }} {{ order.order.type }} {{ order.order.id }} - {{ order.order.price }} {{ round(order.percent_to_price, 1) }} % + {{ order.order.price }} {{ round(order.percent_to_price, 1) }} % {{ order.order.amount }} {{ order.order.retry }} {{ order.order.ourId }} @@ -181,6 +181,9 @@ export default { }, date(value) { return new Date(value).toLocaleString(); + }, + urlEncode(value) { + return encodeURIComponent(value); } }, mounted() {