From 0866a662039a4423371302192d79b8d124823fbd Mon Sep 17 00:00:00 2001 From: Juan Calvo Date: Sun, 26 Dec 2021 15:13:54 +0100 Subject: [PATCH] add: refactor backtesting to a different fork This commit updates backtesting to be executed in a different fork, it allows to start up several different executions independently, either by different tabs or even different pairs at the same time. In the future, this process could be persisted in database with the intention of running long backtestings of years of information on several dozen pairs. The exclusive locking had to be disabled so that each fork also has access to transact with the database. It would be interesting to move it to a Postgres, including a potential docker file --- src/command/backtest.js | 23 ++++++ src/modules/backtest.js | 43 +++++++---- src/modules/http.js | 71 ++++++++++++++----- src/modules/services.js | 2 +- templates/backtest-pending-results.html.twig | 65 +++++++++++++++++ templates/backtest_submit.html.twig | 14 ++-- templates/components/backtest_table.html.twig | 10 +-- 7 files changed, 183 insertions(+), 45 deletions(-) create mode 100644 src/command/backtest.js create mode 100644 templates/backtest-pending-results.html.twig diff --git a/src/command/backtest.js b/src/command/backtest.js new file mode 100644 index 000000000..cb763e5b2 --- /dev/null +++ b/src/command/backtest.js @@ -0,0 +1,23 @@ +const services = require('../modules/services'); + +process.on('message', async msg => { + const p = msg.pair.split('.'); + + 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..c4805dc00 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,7 @@ module.exports = class Backtest { }; }); - const backtestSummary = await this.getBacktestSummary(signals, initial_capital); + const backtestSummary = await this.getBacktestSummary(signals, initialCapital); resolve({ summary: backtestSummary, rows: rows.slice().reverse(), @@ -205,10 +218,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) { + 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 @@ -227,17 +240,17 @@ module.exports = class Backtest { // 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 entrySignalType = lastPosition.result.getSignal(); // Long or Short const entryPrice = lastPosition.price; // Price during the trade entry - const tradedQuantity = Number((workingCapital / entryPrice)); // Quantity + const tradedQuantity = Number(workingCapital / entryPrice); // Quantity // Exit Details const exitPrice = signalObject.price; // Price during trade exit @@ -247,7 +260,7 @@ module.exports = class Backtest { let pnlValue = 0; // Profit or Loss Value // When the position is Long - if (entrySignalType == 'long') { + if (entrySignalType === 'long') { if (exitPrice > entryPrice) { // Long Trade is Profitable trades.profitableCount += 1; @@ -255,7 +268,7 @@ module.exports = class Backtest { // Set the PNL pnlValue = exitValue - workingCapital; - } else if (entrySignalType == 'short') { + } else if (entrySignalType === 'short') { if (exitPrice < entryPrice) { // Short Trade is Profitable trades.profitableCount += 1; @@ -276,7 +289,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 +322,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 +330,7 @@ module.exports = class Backtest { sharpeRatio: sharpeRatio, averagePNLPercent: averagePNLPercent, netProfit: netProfit, - initialCapital: initialCapital, + 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_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 +