Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add: refactor backtesting to a different fork #281

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/command/backtest.js
Original file line number Diff line number Diff line change
@@ -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
});
});
43 changes: 28 additions & 15 deletions src/modules/backtest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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(),
Expand All @@ -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

Expand All @@ -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
Expand All @@ -247,15 +260,15 @@ 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;
}

// Set the PNL
pnlValue = exitValue - workingCapital;
} else if (entrySignalType == 'short') {
} else if (entrySignalType === 'short') {
if (exitPrice < entryPrice) {
// Short Trade is Profitable
trades.profitableCount += 1;
Expand All @@ -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;
}
Expand Down Expand Up @@ -309,15 +322,15 @@ 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));

const summary = {
sharpeRatio: sharpeRatio,
averagePNLPercent: averagePNLPercent,
netProfit: netProfit,
initialCapital: initialCapital,
initialCapital: initialCapitalNumber,
finalCapital: Number(workingCapital.toFixed(2)),
trades: trades
};
Expand Down
71 changes: 52 additions & 19 deletions src/modules/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 }));
Expand Down Expand Up @@ -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');
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just call the code directly instead of forking.
"LOCKING_MODE = EXCLUSIVE" is needed for performance reason.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you call the code directly it doesn't benefit from parallel execution for various pairs at the same time, right? Also, the UI was struggling when a backtest was configured for various thousand hours

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then it should have some sort of lock or limiter, if this is not handled carefully it may induce too much load onto the server and cause a heap overflow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'm completely unaware of that, I'll dig a bit into it to see if I can figure out how to implement it neatly, thanks!

Copy link

@Wladastic Wladastic Jan 17, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can setup a queue that consists out of an array holding each job.
That would be easiest, to just pop each job after the prior got done.
Main benefit of the array would be that the jobs just die away once you kill the server


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;
Expand Down
2 changes: 1 addition & 1 deletion src/modules/services.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down
65 changes: 65 additions & 0 deletions templates/backtest-pending-results.html.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{% extends './layout.html.twig' %}

{% block title %}Backtesting | Crypto Bot{% endblock %}

{% block content %}
<!-- Content Wrapper. Contains page content -->
<div class="content-wrapper">
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="container">
<div class="row mb-2">
<div class="col-sm-6">
<h1>Backtesting</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><a href="{{ '/' }}">Dashboard</a></li>
<li class="breadcrumb-item active">Backtesting</li>
</ol>
</div>
</div>
</div><!-- /.container-fluid -->
</section>
<!-- /.Content Header (Page header) -->

<!-- Main content -->
<div class="content">
<div class="container">
<h3>Waiting results for backtest id {{ key }}</h3>
</div><!-- /.container-fluid -->
</div>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->

{% endblock %}

{% block javascript %}
<script src="/js/backtest-form.js?v={{ asset_version() }}"></script>
<script>
var ready = false;
const intervalId = setInterval(call, 2000);
function call() {
$.ajax({
type: "GET",
url: '/backtest/{{ key }}',
dataType: "json",
success: function(data, textStatus) {
ready = data.ready;
if (data.ready === true) {
clearInterval(intervalId);
window.location.href = '/backtest/result/{{ key }}'
}
}
})
};

</script>

<script src="https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.jquery.min.js" integrity="sha512-rMGGF4wg1R73ehtnxXBt5mbUfN9JUJwbk21KMlnLZDJh7BkPmeovBuddZCENJddHYYMkCh9hPFnPmS9sspki8g==" crossorigin="anonymous"></script>
{% endblock %}

{% block stylesheet %}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/chosen/1.8.7/chosen.min.css" integrity="sha512-yVvxUQV0QESBt1SyZbNJMAwyKvFTLMyXSyBHDO4BG5t7k/Lw34tyqlSDlKIrIENIzCl+RVUNjmCPG+V/GMesRw==" crossorigin="anonymous" />
{% endblock %}
14 changes: 9 additions & 5 deletions templates/backtest_submit.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
<!-- /.card-body -->
</div>
<!-- /.card -->

<div class="card">
<div class="card-header">
<h3 class="card-title">Chart</h3>
Expand Down Expand Up @@ -83,10 +83,14 @@
</div>
<!-- /.card-header -->
<div class="card-body table-responsive p-0">
{% 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 %}
<span style="margin: 10px">Too many rows detected, rendering process skipped.</span>
{% endif %}
</div>
<!-- /.card-body -->
</div>
Expand Down
10 changes: 5 additions & 5 deletions templates/components/backtest_table.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
<td class="no-wrap">{{ row.price|default }}</td>
<td class="no-wrap">
{% if row.profit is defined %}
<span class="{{ row.profit > 0 ? 'text-success' : 'text-danger' }} {{ row.result.signal|default == 'close' ? ' font-weight-bold' : '' }}">{{ row.profit|round(2) }} %</span>
<span class="{{ row.profit > 0 ? 'text-success' : 'text-danger' }} {{ row.result._signal|default == 'close' ? ' font-weight-bold' : '' }}">{{ row.profit|round(2) }} %</span>
{% endif %}

{% if row.lastPriceClosed is defined %}
Expand All @@ -30,11 +30,11 @@
</td>
<td>
{% if row.result is defined %}
{% if row.result.signal == 'long' %}
{% if row.result._signal == 'long' %}
<i class="fas fa-chevron-circle-up text-success"></i>
{% elseif row.result.signal == 'short' %}
{% elseif row.result._signal == 'short' %}
<i class="fas fa-chevron-circle-down text-danger"></i>
{% elseif row.result.signal == 'close' %}
{% elseif row.result._signal == 'close' %}
<i class="fa fa-times"></i>
{% endif %}
{% endif %}
Expand Down Expand Up @@ -76,4 +76,4 @@
</tr>
{% endfor %}
</tbody>
</table>
</table>