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

WIP add: Calculate fees on backtest #282

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
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
  • Loading branch information
juandelacruz-calvo committed Dec 28, 2021
commit 0866a662039a4423371302192d79b8d124823fbd
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
@@ -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,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;
@@ -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,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
};
71 changes: 52 additions & 19 deletions src/modules/http.js
Original file line number Diff line number Diff line change
@@ -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
})
);

Choose a reason for hiding this comment

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

I am not a big fan of refactoring code if it doesn't have anything to do with the feature.
Please create another pull request for this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think this might be the prettier plugin

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;
2 changes: 1 addition & 1 deletion src/modules/services.js
Original file line number Diff line number Diff line change
@@ -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;');

Choose a reason for hiding this comment

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

why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I specified it on the commit message bit it got lost in the log :) It's basically cos sqlite3 is for some reason not allowing multiple connections to the database, one from the fork and from the main app. Probably worth locking the queries instead of the connection to the db. Happy to change to for a different approach if you have any ideas


return (db = myDb);
},
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
@@ -45,7 +45,7 @@
<!-- /.card-body -->
</div>
<!-- /.card -->

<div class="card">
<div class="card-header">
<h3 class="card-title">Chart</h3>
@@ -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>
10 changes: 5 additions & 5 deletions templates/components/backtest_table.html.twig
Original file line number Diff line number Diff line change
@@ -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 %}
@@ -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 %}
@@ -76,4 +76,4 @@
</tr>
{% endfor %}
</tbody>
</table>
</table>