Instantly share code, notes, and snippets.

Embed
What would you like to do?
This notebook exported from Data-Forge Notebook is an example trading strategy that demonstrates the grademark API for backtesting financial trading strategies.

Grademark mean reversion example

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

Prepare your data

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-indicators');
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

Visualize the input data

Now let's make a plot of the example data and see the shape of it.

display(inputSeries.tail(100).plot({}, { y: "close" }));

Chart

Compute indicators

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...

const movingAverage = inputSeries
    .deflate(bar => bar.close)          // Extract closing price series.
    .sma(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

Plot indicators

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" ] }));

Chart

Define your trading strategy

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.
    },
};

Backtest your strategy

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

Plot your trades

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" }));

Chart

Analyze performance

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(analysis);

Data

Equity curve

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());

Chart

Drawdown chart

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());

Chart

Advanced backtesting

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment