Skip to content

Commit

Permalink
Refactoring #1
Browse files Browse the repository at this point in the history
  • Loading branch information
ficzusistvan committed May 1, 2020
1 parent f8c9f6b commit 703474a
Show file tree
Hide file tree
Showing 33 changed files with 1,665 additions and 1,361 deletions.
60 changes: 60 additions & 0 deletions backend/WebSocketClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import WebSocket from 'ws'

function WebSocketClient() {
this.number = 0; // Message number
this.autoReconnectInterval = 5 * 1000; // ms
}
WebSocketClient.prototype.open = function (url: string) {
this.url = url;
this.instance = new WebSocket(this.url);
this.instance.on('open', () => {
this.onopen();
});
this.instance.on('message', (data: any, flags: any) => {
this.number++;
this.onmessage(data, flags, this.number);
});
this.instance.on('close', (e: any) => {
switch (e.code) {
case 1000: // CLOSE_NORMAL
console.log("WebSocket: closed");
break;
default: // Abnormal closure
this.reconnect(e);
break;
}
this.onclose(e);
});
this.instance.on('error', (e: any) => {
switch (e.code) {
case 'ECONNREFUSED':
this.reconnect(e);
break;
default:
this.onerror(e);
break;
}
});
}
WebSocketClient.prototype.send = function (data: any, option: any) {
try {
this.instance.send(data, option);
} catch (e) {
this.instance.emit('error', e);
}
}
WebSocketClient.prototype.reconnect = function (e: any) {
console.log(`WebSocketClient: retry in ${this.autoReconnectInterval}ms`, e);
this.instance.removeAllListeners();
var that = this;
setTimeout(function () {
console.log("WebSocketClient: reconnecting...");
that.open(that.url);
}, this.autoReconnectInterval);
}
WebSocketClient.prototype.onopen = function (e: any) { console.log("WebSocketClient: open", arguments); }
WebSocketClient.prototype.onmessage = function (data: any, flags: any, number: any) { console.log("WebSocketClient: message", arguments); }
WebSocketClient.prototype.onerror = function (e: any) { console.log("WebSocketClient: error", arguments); }
WebSocketClient.prototype.onclose = function (e: any) { console.log("WebSocketClient: closed", arguments); }

export default WebSocketClient
8 changes: 4 additions & 4 deletions backend/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import express from 'express'
import path from 'path'
//import { em, HTTP_SERVER_INITIALISED } from './event-handler'
import { em, HTTP_SERVER_INITIALISED } from './event-handler'
import nconf from 'nconf'
nconf.file({
file: 'config.json',
Expand All @@ -15,11 +15,11 @@ const APP_PORT = nconf.get('ports:http_server');
// Serve static files from the React app
app.use(express.static(path.join(__dirname, '../client/arbiter/build')));

//import routes from './routes'
import routes from './routes'

//app.use(routes);
app.use(routes);

app.listen(APP_PORT, function () {
console.log('Example app listening on port ' + APP_PORT + '!');
//em.emit(HTTP_SERVER_INITIALISED, APP_PORT);
em.emit(HTTP_SERVER_INITIALISED, APP_PORT);
});
173 changes: 25 additions & 148 deletions backend/bot.ts
Original file line number Diff line number Diff line change
@@ -1,168 +1,45 @@
import Big from 'big.js'
import { CronJob } from 'cron';
import nconf from 'nconf'
nconf.file({
file: 'config.json',
search: true
});

// DEBUGGING
const debug = require('debug')('bot')
import Debug from 'debug'
const debug = Debug('bot')

// STRATEGY
const strategy = require('./strategies/' + nconf.get('bot:strategy'))
import * as xapi from './xapi'

// EXCHANGE
const exchangeHelpers = require('./exchanges/helpers')

var exchangePath = 'binance';
if (nconf.get('bot:mode') === exchangeHelpers.MODE_BACKTESTING) {
exchangePath = 'backtest';
}
const exchange = require('./exchanges/' + exchangePath + '/exchange')

// APP RELATED
const socketio = require('./socketio')
const logger = require('./logger');
const eventHandler = require('./event-handler')
const candlestickHandler = require('./candlestick-handler')

const SYMBOL = nconf.get('bot:symbol');
const INTERVAL_TRADING = nconf.get('bot:kline_trading_interval');
const STR_BASE_ASSET = nconf.get('bot:base_asset');
const STR_QUOTE_ASSET = nconf.get('bot:quote_asset');
const MIN_TRADE_AMOUNT = nconf.get('bot:min_trade_amount');
const INVEST_PERCENT = nconf.get('bot:invest_percent');
import * as logger from './logger'
import * as eventHandler from './event-handler'

/**
* Updated indicators.
* Structure depends on the selected strategy.
*/
var indicators = new Map();
/**
* Currently tracked pair info, null otherwise
*/
var trackedPair: any = null;
/**
* Total number of trades
*/
var nrOfTrades = 0;
/**
* Quantity of current assets
*/
var baseAssetQuantity: any, quoteAssetQuantity: any;
let isGetCandlesJobEnabled = false;

function startTracking(p_s_side: any, p_n_quantity: any, p_n_stopLossPrice: any, p_n_price: any) {
logger.silly('Start tracking [' + SYMBOL + '] side[' + p_s_side + ']; quantity[' + p_n_quantity + ' ' + STR_BASE_ASSET + ']; stop loss price[' + p_n_stopLossPrice + ']; buy/sell price[' + p_n_price + ' ' + STR_QUOTE_ASSET + ']');
trackedPair = {
pair: SYMBOL,
side: p_s_side,
quantity: p_n_quantity,
stop_loss_price: p_n_stopLossPrice,
price: p_n_price
};
let xtbLogin = function () {
xapi.login();
}

function stopTracking() {
logger.silly('Stop tracking [' + SYMBOL + ']');
trackedPair = null;
}

// TODO: check if order book is ok
/**
*
* @param {string} side
* @param {Big.js} price
* @param {Big.js} fee - specified in percent
*/
function getQuantityAndFeeAmountForOrder(side: any, price: any, fee: any) {
var maxAmountToInvest = (side === exchangeHelpers.SIDE_BUY ? quoteAssetQuantity : baseAssetQuantity).times(INVEST_PERCENT).div(100);
var q = Big(0);
var f = Big(0);
if (
(side === exchangeHelpers.SIDE_BUY && maxAmountToInvest.gt(MIN_TRADE_AMOUNT)) ||
(side === exchangeHelpers.SIDE_SELL && maxAmountToInvest.times(price).gt(MIN_TRADE_AMOUNT))
) {
q = maxAmountToInvest.div(price).div(fee.div(100).plus(1));
f = q.times(price).times(fee).div(100);
}
return { quantity: q, fee: f };
let xtbGetCandle = function(streamSessionId: any) {
//xapi.startGetCandlesStreaming(streamSessionId);
isGetCandlesJobEnabled = true;
getCandlesJob.start();
}

function getExitFee(quantity: any, price: any, fee: any) {
return quantity.times(price).times(fee).div(100);
let run = function () {

}

module.exports.run = async function () {
var kLineBars = candlestickHandler.getKLineBars(candlestickHandler.TYPE_TRADING);
var lastPrice = Big(kLineBars[kLineBars.length - 1].close);
socketio.sendToBrowser('PRICE_UPDATED', {
currentPrice: lastPrice
});
if (kLineBars.length >= 200) {
indicators = await strategy.runIndicators(kLineBars);
if (trackedPair === null) {
var resEnter = strategy.runEnterStrategy();
if (resEnter.long) {
logger.info('---------------------------------------------ENTER---------------------------------------------');
var qf = getQuantityAndFeeAmountForOrder(exchangeHelpers.SIDE_BUY, lastPrice, exchange.getTakerFee());
if (qf.quantity.eq(0) && qf.fee.eq(0)) {
logger.warn('Not enough quoteAssetQuantity [' + quoteAssetQuantity.toFixed(5) + ' ' + STR_QUOTE_ASSET + '] !!! (Min trade amount [' + MIN_TRADE_AMOUNT + '])');
eventHandler.emit(eventHandler.MIN_TRADE_AMOUNT_REACHED, '');
} else {
var stopLossPrice = strategy.initStopLoss();
logger.info('Enter: Opening BUY order for [' + SYMBOL + '] price [' + lastPrice.toFixed(2) + ' ' + STR_QUOTE_ASSET + '] quantity [' + qf.quantity.toFixed(5) + ' ' + STR_BASE_ASSET + '] fee [' + qf.fee.toFixed(5) + ' ' + STR_QUOTE_ASSET + ']');
await exchange.newOrder(SYMBOL, exchangeHelpers.SIDE_BUY, exchangeHelpers.ORDER_TYPE_MARKET, qf.quantity, exchangeHelpers.TIME_IN_FORCE_GTC, stopLossPrice, lastPrice);
await startTracking(exchangeHelpers.SIDE_BUY, qf.quantity, stopLossPrice, lastPrice);
baseAssetQuantity = baseAssetQuantity.plus(qf.quantity);
quoteAssetQuantity = quoteAssetQuantity.minus(qf.quantity.times(lastPrice).plus(qf.fee));
logger.info('baseAssetQuantity [' + baseAssetQuantity.toFixed(5) + ' ' + STR_BASE_ASSET + '] quoteAssetQuantity [' + quoteAssetQuantity.toFixed(5) + ' ' + STR_QUOTE_ASSET + ']');
socketio.sendToBrowser('STARTED_TRACKING_BUY_ORDER', trackedPair);
}
}
if (resEnter.short) {
// TODO
}
} else {
var resExit = strategy.runExitStrategy(trackedPair.side);
if (resExit.long) {
logger.info('---------------------------------------------EXIT----------------------------------------------');
nrOfTrades++;
var fee = getExitFee(trackedPair.quantity, lastPrice, exchange.getTakerFee())
logger.info('Exit: Opening SELL order for [' + SYMBOL + '] price [' + lastPrice.toFixed(2) + ' ' + STR_QUOTE_ASSET + '] quantity [' + trackedPair.quantity.toFixed(5) + ' ' + STR_BASE_ASSET + '] fee [' + fee.toFixed(5) + ' ' + STR_QUOTE_ASSET + ']');
await exchange.newOrder(SYMBOL, exchangeHelpers.SIDE_SELL, exchangeHelpers.ORDER_TYPE_MARKET, trackedPair.quantity, exchangeHelpers.TIME_IN_FORCE_GTC, 0, lastPrice);
baseAssetQuantity = baseAssetQuantity.minus(trackedPair.quantity);
quoteAssetQuantity = quoteAssetQuantity.plus(trackedPair.quantity.times(lastPrice).minus(fee));
await stopTracking();
logger.info('baseAssetQuantity [' + baseAssetQuantity.toFixed(5) + ' ' + STR_BASE_ASSET + '] quoteAssetQuantity [' + quoteAssetQuantity.toFixed(5) + ' ' + STR_QUOTE_ASSET + '] Number of trades: [' + nrOfTrades + ']');
socketio.sendToBrowser('STOPPED_TRACKING_BUY_ORDER', { b: baseAssetQuantity, q: quoteAssetQuantity });
}
if (resExit.short) {
// TODO
}
}
} else {
debug('KLineBars length [' + kLineBars.length + '] not enough... Skipping...');
const getCandlesJob = new CronJob('0 * * * * *', function () {
debug('Running job [getCandles]');
if (isGetCandlesJobEnabled) {
xapi.getChartLastRequest(1);
}
eventHandler.emit(eventHandler.BOT_RUN_END, '');
}

async function initFeesAndBalances() {
var res = await exchange.getFeesAndBalances();
baseAssetQuantity = res.balances.baseAsset;
quoteAssetQuantity = res.balances.quoteAsset;
logger.info('Starting with balances', res.balances);
logger.info('Starting with fees', res.fees);
}

module.exports.start = async function () {
await initFeesAndBalances();
await exchange.downloadPastKLines(candlestickHandler.TYPE_TRADING, SYMBOL, INTERVAL_TRADING);
exchange.startKLineStreaming(candlestickHandler.TYPE_TRADING, SYMBOL, INTERVAL_TRADING);
}
});

/**
* Functions for the Frontend
*/
module.exports.getIndicators = function () {
return indicators;
}
export {
xtbLogin,
xtbGetCandle,
run
};
23 changes: 23 additions & 0 deletions backend/candles-handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as i from './interfaces'
import Debug from 'debug'
const debug = Debug('candles-handler')

import { em, UPDATED_CANDLES } from './event-handler'

const CIRCULAR_BUFFER_SIZE = 200;
let candles: Array<i.ICommonCandle> = [];

let updateLastCandle = function (candle: i.ICommonCandle) {
candles.push(candle);

if (candles.length > CIRCULAR_BUFFER_SIZE) { // See: https://github.com/binance-exchange/binance-official-api-docs/blob/master/rest-api.md#klinecandlestick-data
candles.shift();
}
debug('Updated candles; new length[' + candles.length + ']');

em.emit(UPDATED_CANDLES, candle);
}

export {
updateLastCandle
}
43 changes: 0 additions & 43 deletions backend/candlestick-handler.js

This file was deleted.

19 changes: 1 addition & 18 deletions backend/config.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,12 @@
"is_production_build": "no",
"use_morgan": false
},
"binance": {
"xtb": {
"api_key": "xxx",
"secret_key": "yyy",
"ws_timeout_ms": {
"ping": 210000,
"restart": 5000
}
},
"bot": {
"mode": "backtesting",
"strategy": "my-strategy",
"symbol": "BTCUSDT",
"base_asset": "BTC",
"quote_asset": "USDT",
"kline_trading_interval": "1m",
"kline_anchor_interval": "15m",
"min_trade_amount": 10,
"invest_percent": 90
},
"backtest": {
"initial_base_asset_quantity": 0.75,
"initial_quote_asset_quantity": 3000,
"maker_fee": 0.075,
"taker_fee": 0.075
}
}
5 changes: 3 additions & 2 deletions backend/cron-jobs.js → backend/cron-jobs.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var CronJob = require('cron').CronJob;
const debug = require('debug')('cron-jobs');
var bot = require('./bot.js');
import Debug from 'debug'
const debug = Debug('cron-jobs');
import * as bot from './bot'
const logger = require('./logger');

/** GET EXCHANGE INFO */
Expand Down
Loading

0 comments on commit 703474a

Please sign in to comment.