diff --git a/README.md b/README.md index 07fd5b0aa..d44a8a723 100644 --- a/README.md +++ b/README.md @@ -338,6 +338,7 @@ c.symbols = [ symbol: 'ETHUSDT', exchange: 'binance_futures', periods: ['1m', '15m', '1h'], + feesPerTrade: 0.04, trade: { currency_capital: 10, strategies: [ @@ -369,6 +370,7 @@ Per pair you can set used margin before orders are created; depending on exchang c.symbols.push({ 'symbol': 'BTCUSD', 'exchange': 'bitmex', + 'feesPerTrade': 0.05, 'extra': { 'bitmex_leverage': 5, }, @@ -377,12 +379,24 @@ Per pair you can set used margin before orders are created; depending on exchang c.symbols.push({ 'symbol': 'EOSUSD', 'exchange': 'bybit', + 'feesPerTrade': 0.075, 'extra': { 'bybit_leverage': 5, }, }) ``` +### Fees + +Fees can be configured on `instance.js` file including the field `feesPerTrade`, it is important to +mention that prices of fees are for Taker, not for Maker + +For example: + +Binance futures: has a Maker/Taker 0.0200%/0.0400%. + +'feesPerTrade': 0.04, + ## Tools ### Fill data diff --git a/instance.js.dist b/instance.js.dist index 621c0854f..1600a0630 100644 --- a/instance.js.dist +++ b/instance.js.dist @@ -94,6 +94,7 @@ c.init = () => { 'periods': ['1m', '15m', '1h'], 'exchange': 'binance', 'state': 'watch', + 'feesPerTrade': 0.04, 'watchdogs': [ { 'name': 'stoploss_watch', @@ -144,6 +145,7 @@ y.forEach((pair) => { 'symbol': pair, 'periods': ['1m', '15m', '1h'], 'exchange': 'bitmex', + 'feesPerTrade': 0.1, 'state': 'watch', 'extra': { 'bitmex_leverage': 5, @@ -193,6 +195,7 @@ l.forEach((pair) => { 'symbol': pair, 'periods': ['1m', '15m', '1h'], 'exchange': 'bitmex_testnet', + 'feesPerTrade': 0.1, 'state': 'watch', 'watchdogs': [ { @@ -235,6 +238,7 @@ z.forEach((pair) => { 'symbol': pair, 'periods': ['1m', '15m', '1h'], 'exchange': 'binance', + 'feesPerTrade': 0.1, 'state': 'watch', 'strategies': [ { diff --git a/src/command/backtest.js b/src/command/backtest.js new file mode 100644 index 000000000..66ed827fd --- /dev/null +++ b/src/command/backtest.js @@ -0,0 +1,25 @@ +const services = require('../modules/services'); + +process.on('message', async msg => { + const p = msg.pair.split('.'); + + services.boot(msg.projectDir); + + const results = await services + .getBacktest() + .getBacktestResult( + msg.tickIntervalInMinutes, + msg.hours, + msg.strategy, + msg.candlePeriod, + p[0], + p[1], + msg.options, + msg.initialCapital, + msg.projectDir + ); + + process.send({ + results: results + }); +}); diff --git a/src/modules/backtest.js b/src/modules/backtest.js index 974a4e351..3d190e757 100644 --- a/src/modules/backtest.js +++ b/src/modules/backtest.js @@ -44,7 +44,20 @@ module.exports = class Backtest { }); } - getBacktestResult(tickIntervalInMinutes, hours, strategy, candlePeriod, exchange, pair, options, initial_capital) { + getBacktestResult( + tickIntervalInMinutes, + hours, + strategy, + candlePeriod, + exchange, + pair, + options, + initialCapital, + projectDir + ) { + if (projectDir) { + this.projectDir = projectDir; + } return new Promise(async resolve => { const start = moment() .startOf('hour') @@ -186,7 +199,10 @@ module.exports = class Backtest { }; }); - const backtestSummary = await this.getBacktestSummary(signals, initial_capital); + const instance = this.instances.symbols.filter(i => i.symbol === pair && i.exchange === exchange)[0]; + const fees = instance.feesPerTrade === undefined ? 0 : instance.feesPerTrade; + + const backtestSummary = await this.getBacktestSummary(signals, initialCapital, fees); resolve({ summary: backtestSummary, rows: rows.slice().reverse(), @@ -205,10 +221,10 @@ module.exports = class Backtest { }); } - getBacktestSummary(signals, initial_capital) { - return new Promise(async resolve => { - const initialCapital = Number(initial_capital); // 1000 $ Initial Capital - let workingCapital = initialCapital; // Capital that changes after every trade + getBacktestSummary(signals, initialCapital, feesPerTrade) { + return new Promise(resolve => { + const initialCapitalNumber = Number(initialCapital); // 1000 $ Initial Capital + let workingCapital = initialCapitalNumber; // Capital that changes after every trade let lastPosition; // Holds Info about last action @@ -222,47 +238,52 @@ module.exports = class Backtest { }; let cumulativePNLPercent = 0; // Sum of all the PNL Percentages + let cumulativeNetFees = 0; const pnlRateArray = []; // Array of all PNL Percentages of all the trades - // Iterate over all the signals for (let s = 0; s < signals.length; s++) { const signalObject = signals[s]; - const signalType = signalObject.result._signal; // Can be long,short,close + const signalType = signalObject.result.getSignal(); // Can be long,short,close // When a trade is closed - if (signalType == 'close') { + if (signalType === 'close') { // Increment the total trades counter trades.total += 1; // Entry Position Details - const entrySignalType = lastPosition.result._signal; // Long or Short const entryPrice = lastPosition.price; // Price during the trade entry - const tradedQuantity = Number((workingCapital / entryPrice)); // Quantity - + const tradedQuantity = Number(workingCapital / entryPrice); // Quantity + const entrySignalType = lastPosition.result.getSignal(); // Long or Short + const entryValue = Number((tradedQuantity * entryPrice).toFixed(2)); // Price * Quantity + const entryFee = (entryValue * feesPerTrade) / 100; // Exit Details const exitPrice = signalObject.price; // Price during trade exit const exitValue = Number((tradedQuantity * exitPrice).toFixed(2)); // Price * Quantity + const exitFee = (exitValue * feesPerTrade) / 100; + + const totalFee = entryFee + exitFee; + cumulativeNetFees += totalFee; // Trade Details let pnlValue = 0; // Profit or Loss Value // When the position is Long - if (entrySignalType == 'long') { - if (exitPrice > entryPrice) { + if (entrySignalType === 'long') { + if (exitValue - totalFee > entryValue) { // Long Trade is Profitable trades.profitableCount += 1; } // Set the PNL - pnlValue = exitValue - workingCapital; - } else if (entrySignalType == 'short') { - if (exitPrice < entryPrice) { + pnlValue = exitValue - totalFee - workingCapital; + } else if (entrySignalType === 'short') { + if (exitValue - totalFee < entryValue) { // Short Trade is Profitable trades.profitableCount += 1; } // Set the PNL - pnlValue = -(exitValue - workingCapital); + pnlValue = -(exitValue - totalFee - workingCapital); } // Percentage Return @@ -276,7 +297,7 @@ module.exports = class Backtest { // Update Working Cap workingCapital += pnlValue; - } else if (signalType == 'long' || signalType == 'short') { + } else if (signalType === 'long' || signalType === 'short') { // Enter into a position lastPosition = signalObject; } @@ -309,7 +330,7 @@ module.exports = class Backtest { // -- End of Sharpe Ratio Calculation // Net Profit - const netProfit = Number((((workingCapital - initialCapital) / initialCapital) * 100).toFixed(2)); + const netProfit = Number((((workingCapital - initialCapitalNumber) / initialCapitalNumber) * 100).toFixed(2)); trades.profitabilityPercent = Number(((trades.profitableCount * 100) / trades.total).toFixed(2)); @@ -317,7 +338,8 @@ module.exports = class Backtest { sharpeRatio: sharpeRatio, averagePNLPercent: averagePNLPercent, netProfit: netProfit, - initialCapital: initialCapital, + netFees: cumulativeNetFees, + initialCapital: initialCapitalNumber, finalCapital: Number(workingCapital.toFixed(2)), trades: trades }; diff --git a/src/modules/http.js b/src/modules/http.js index c176b1053..d73cd9806 100644 --- a/src/modules/http.js +++ b/src/modules/http.js @@ -5,8 +5,12 @@ const auth = require('basic-auth'); const cookieParser = require('cookie-parser'); const crypto = require('crypto'); const moment = require('moment'); +const { fork } = require('child_process'); const OrderUtil = require('../utils/order_util'); +const backtestPendingPairs = {}; +const backtestResults = {}; + module.exports = class Http { constructor( systemUtil, @@ -92,7 +96,13 @@ module.exports = class Http { strict_variables: false }); - app.use(express.urlencoded({ limit: '12mb', extended: true, parameterLimit: 50000 })); + app.use( + express.urlencoded({ + limit: '12mb', + extended: true, + parameterLimit: 50000 + }) + ); app.use(cookieParser()); app.use(compression()); app.use(express.static(`${this.projectDir}/web/static`, { maxAge: 3600000 * 24 })); @@ -136,29 +146,52 @@ module.exports = class Http { pairs = [pairs]; } - const asyncs = pairs.map(pair => { - return async () => { - const p = pair.split('.'); + const key = moment().unix(); + + backtestPendingPairs[key] = []; + backtestResults[key] = []; + + pairs.forEach(pair => { + backtestPendingPairs[key].push(pair); + + const forked = fork('src/command/backtest.js'); - return { + forked.send({ + pair, + tickIntervalInMinutes: parseInt(req.body.ticker_interval, 10), + hours: req.body.hours, + strategy: req.body.strategy, + candlePeriod: req.body.candle_period, + options: req.body.options ? JSON.parse(req.body.options) : {}, + initialCapital: req.body.initial_capital, + projectDir: this.projectDir + }); + + forked.on('message', msg => { + backtestPendingPairs[key].splice(backtestPendingPairs[key].indexOf(pair), 1); + backtestResults[key].push({ 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 - ) - }; - }; + result: msg.results + }); + }); }); - const backtests = await Promise.all(asyncs.map(fn => fn())); + res.render('../templates/backtest-pending-results.html.twig', { + key: key + }); + }); + + app.get('/backtest/:backtestKey', async (req, res) => { + res.send({ + ready: + backtestPendingPairs[req.params.backtestKey] === undefined + ? false + : backtestPendingPairs[req.params.backtestKey].length === 0 + }); + }); - // single details view + app.get('/backtest/result/:backtestKey', (req, res) => { + const backtests = backtestResults[req.params.backtestKey]; if (backtests.length === 1) { res.render('../templates/backtest_submit.html.twig', backtests[0].result); return; diff --git a/src/modules/services.js b/src/modules/services.js index 95040b276..d90f749a8 100644 --- a/src/modules/services.js +++ b/src/modules/services.js @@ -155,7 +155,7 @@ module.exports = { myDb.pragma('journal_mode = WAL'); myDb.pragma('SYNCHRONOUS = 1;'); - myDb.pragma('LOCKING_MODE = EXCLUSIVE;'); + // myDb.pragma('LOCKING_MODE = EXCLUSIVE;'); return (db = myDb); }, diff --git a/templates/backtest-pending-results.html.twig b/templates/backtest-pending-results.html.twig new file mode 100644 index 000000000..cdcb326c0 --- /dev/null +++ b/templates/backtest-pending-results.html.twig @@ -0,0 +1,65 @@ +{% extends './layout.html.twig' %} + +{% block title %}Backtesting | Crypto Bot{% endblock %} + +{% block content %} + +
+ +
+
+
+
+

Backtesting

+
+
+ +
+
+
+
+ + + +
+
+

Waiting results for backtest id {{ key }}

+
+
+ +
+ + +{% endblock %} + +{% block javascript %} + + + + +{% endblock %} + +{% block stylesheet %} + +{% endblock %} diff --git a/templates/backtest_submit.html.twig b/templates/backtest_submit.html.twig index adcb33512..6a536385f 100644 --- a/templates/backtest_submit.html.twig +++ b/templates/backtest_submit.html.twig @@ -45,7 +45,7 @@ - +

Chart

@@ -83,10 +83,14 @@
- {% include 'components/backtest_table.html.twig' with { - 'rows': rows, - 'extra_fields': extra_fields, - } only %} + {% if rows|length > 1000 %} + {% include 'components/backtest_table.html.twig' with { + 'rows': rows, + 'extra_fields': extra_fields, + } only %} + {% else %} + Too many rows detected, rendering process skipped. + {% endif %}
diff --git a/templates/components/backtest_summary.html.twig b/templates/components/backtest_summary.html.twig index 355a2aae8..2e4690ded 100644 --- a/templates/components/backtest_summary.html.twig +++ b/templates/components/backtest_summary.html.twig @@ -17,6 +17,14 @@ {{ summary.finalCapital }} $ + + + Net Fees + + + {{ summary.netFees|round(2) }} $ + + Net Return on Investment diff --git a/templates/components/backtest_table.html.twig b/templates/components/backtest_table.html.twig index aa1dc6437..146160797 100644 --- a/templates/components/backtest_table.html.twig +++ b/templates/components/backtest_table.html.twig @@ -21,7 +21,7 @@ {{ row.price|default }} {% if row.profit is defined %} - {{ row.profit|round(2) }} % + {{ row.profit|round(2) }} % {% endif %} {% if row.lastPriceClosed is defined %} @@ -30,11 +30,11 @@ {% if row.result is defined %} - {% if row.result.signal == 'long' %} + {% if row.result._signal == 'long' %} - {% elseif row.result.signal == 'short' %} + {% elseif row.result._signal == 'short' %} - {% elseif row.result.signal == 'close' %} + {% elseif row.result._signal == 'close' %} {% endif %} {% endif %} @@ -76,4 +76,4 @@ {% endfor %} - \ No newline at end of file +