This notebook demonstrate backtesting of very simple mean reversion trading strategy.
It uses the Grademark JavaScript API for backtesting.
For a version of this code runnable on Node.js - please see the Grademark first example repo.
To keep up with what I'm doing checkout my blog or YouTube channel.
This markdown was exported from Data-Forge Notebook
First we need some code to load, parse and prep your data as needed. Then preview your data as a table to make sure it looks ok.
The data used in this example is three years of daily prices for STW from 2015 to end 2017. STW is an exchange traded fund for the ASX 200.
const dataForge = require('data-forge');
require('data-forge-fs');
require('data-forge-plot');
let inputSeries = (await dataForge.readFile("STW.csv").parseCSV())
.parseDates("date", "DD/MM/YYYY")
.parseFloats(["open", "high", "low", "close", "volume"])
.setIndex("date") // Index so we can later merge on date.
.renameSeries({ date: "time" })
.bake();
display(inputSeries.head(5));
index | time | open | high | low | close | volume |
---|---|---|---|---|---|---|
2001-08-27 00:00:00 | 2001-08-27 00:00:00 | 33.98 | 33.99 | 33.75 | 33.75 | 674400 |
2001-08-28 00:00:00 | 2001-08-28 00:00:00 | 33.75 | 33.85 | 33.73 | 33.84 | 47863 |
2001-08-29 00:00:00 | 2001-08-29 00:00:00 | 33.72 | 33.72 | 33.58 | 33.58 | 117161 |
2001-08-30 00:00:00 | 2001-08-30 00:00:00 | 33.59 | 33.59 | 33.3 | 33.33 | 75887 |
2001-08-31 00:00:00 | 2001-08-31 00:00:00 | 33.02 | 33.02 | 32.83 | 32.83 | 135033 |
Now let's make a plot of the example data and see the shape of it.
display(inputSeries.tail(100).plot({}, { y: "close" }));
We are going to need some indicators from which to make trading decisions!
We are testing a simple and naive mean reversion strategy. The first thing we need is a simple moving average of the STW closing proice. Let's compute that now...
function sma(series, period) {
return series.rollingWindow(period)
.select(window => [window.getIndex().last(), window.average()])
.withIndex(pair => pair[0])
.select(pair => pair[1]);
}
const movingAverage = sma(
inputSeries.deflate(bar => bar.close), // Extract closing price series.
30 // 30 day moving average.
)
.bake();
inputSeries = inputSeries
.skip(30) // Skip blank sma entries.
.withSeries("sma", movingAverage) // Integrate moving average into data, indexed on date.
.bake();
display(inputSeries.head(5));
index | time | open | high | low | close | volume | sma |
---|---|---|---|---|---|---|---|
2001-10-09 00:00:00 | 2001-10-09 00:00:00 | 31.66 | 31.81 | 31.66 | 31.73 | 79506 | 31.56866666666667 |
2001-10-10 00:00:00 | 2001-10-10 00:00:00 | 31.71 | 31.76 | 31.62 | 31.65 | 87312 | 31.495666666666672 |
2001-10-11 00:00:00 | 2001-10-11 00:00:00 | 32.04 | 32.23 | 32 | 32.23 | 70199 | 31.450666666666674 |
2001-10-12 00:00:00 | 2001-10-12 00:00:00 | 32.66 | 32.66 | 32.49 | 32.57 | 26658 | 31.42533333333334 |
2001-10-15 00:00:00 | 2001-10-15 00:00:00 | 32.31 | 32.31 | 32.15 | 32.25 | 71891 | 31.406000000000006 |
Now we'll plot a preview of our indicator against the closing price to get an idea of what it looks like.
display(inputSeries.tail(100).plot({}, { y: ["close", "sma" ] }));
It's time to define our trading strategy. A Grademark strategy is a simple JavaScript object that defines rules for entry and exit. We are going to enter a position when the price is below average and then exit as soon as the price is above average. I told you this was a simple strategy!
We also set a 2% stop loss that will help limit our losses for trades that go against us.
const { backtest, analyze, computeEquityCurve, computeDrawdown } = require('grademark');
const strategy = {
entryRule: (enterPosition, bar, lookback) => {
if (bar.close < bar.sma) { // Buy when price is below average.
enterPosition();
}
},
exitRule: (exitPosition, position, bar, lookback) => {
if (bar.close > bar.sma) {
exitPosition(); // Sell when price is above average.
}
},
stopLoss: (entryPrice, latestBar, lookback) => { // Intrabar stop loss.
return entryPrice * (2 / 100); // Stop out on 2% loss from entry price.
},
};
Now that we have a strategy we can simulate trading it on historical data and see how it performs!
Calling the backtest
function gives us the collection of trades that would have been executed on the historical data according to the strategy that we defined.
const trades = backtest(strategy, inputSeries);
display("# trades: " + trades.count());
display(trades.head(5));
# trades: 300
index | entryTime | entryPrice | exitTime | exitPrice | profit | profitPct | growth | holdingPeriod | exitReason |
---|---|---|---|---|---|---|---|---|---|
0 | 2001-12-14 00:00:00 | 33.4 | 2001-12-20 00:00:00 | 33.76 | 0.35999999999999943 | 1.0778443113772438 | 1.0107784431137725 | 3 | exit-rule |
1 | 2002-02-08 00:00:00 | 34.7 | 2002-02-12 00:00:00 | 35.31 | 0.6099999999999994 | 1.7579250720461077 | 1.017579250720461 | 1 | exit-rule |
2 | 2002-02-21 00:00:00 | 34.84 | 2002-02-27 00:00:00 | 34.83 | -0.010000000000005116 | -0.02870264064295383 | 0.9997129735935705 | 3 | exit-rule |
3 | 2002-03-01 00:00:00 | 34.663 | 2002-03-06 00:00:00 | 35.11 | 0.44700000000000273 | 1.289559472636537 | 1.0128955947263654 | 2 | exit-rule |
4 | 2002-03-18 00:00:00 | 35.07 | 2002-03-20 00:00:00 | 35.35 | 0.28000000000000114 | 0.7984031936127777 | 1.0079840319361277 | 1 | exit-rule |
Let's plot the profit on our trades to visualize what they look like... you can see that all the loosing trades are clamped to 2%, that shows that our stop loss is working.
display(trades.plot({ chartType: "bar" }, { y: "profitPct" }));
To get a clear picture of how well (or not) the strategy performed we can use the analyze
function to produce a summary of results.
Here we need to specify an amount of starting capital. This is the amount of (virtual) cash that we invest in the strategy at the start of trading. You can see that executing this strategy from 2015 to end 2016 delivers us a nice profit of 83%. This is fairly optimistic (it doesn't include fees and slippage) but it's a good start.
const startingCapital = 10000;
const analysis = analyze(startingCapital, trades); // Analyse the performance of this set of trades.
display.table(analysis);
Property | Value |
---|---|
startingCapital | 10000 |
finalCapital | 18358.19035677701 |
profit | 8358.19035677701 |
profitPct | 83.5819035677701 |
growth | 1.8358190356777009 |
totalTrades | 300 |
barCount | 1246 |
maxDrawdown | -3442.804793012392 |
maxDrawdownPct | -28.80723435550751 |
maxRisk | |
maxRiskPct | |
profitFactor | 1.2047109827075624 |
percentProfitable | 54.333333333333336 |
For a more visual understanding of the strategy's performance over time, let's compute and plot the equity curve. This shows the total value of our (virtual) trading account over time.
const equityCurve = computeEquityCurve(10000, trades);
display(equityCurve.plot({ chartType: "area", y: { min: 9500 } }));
We can also compute and render a drawdown chart. This gives us a feel for the amount of risk in the strategy. It shows the amount of time the strategy has spent in negative territory, that's those times when a strategy has reached a peak but is now losing value (drawing down) from that peak.
const drawdown = computeDrawdown(10000, trades);
display(drawdown.plot({ chartType: "area" }));
We are only just getting started here and in future notebooks, videos and blog posts we'll explore some of the more advanced aspects of backtesting including:
- Market filters
- Ranking and portfolio simulation
- Position sizing
- Optimization
- Walk-forward testing
- Monte-carlo testing
- Comparing systems
- Eliminating bias
Follow my blog and YouTube channel to keep up.
This markdown was exported from Data-Forge Notebook