Overview
Once you have a dataset that has asset prices and whatever indicators you wish to use to trade, you can calculate the trades of a given trading algorithm.
Trade Algorithm
A trade algorithm is a function that takes a record that contains the prices of all assets (and includes any indicators that have been calculated) and determines the relevant amount of each asset to hold over the coming period. Here we are assuming that our dataset is of the merged form.
let trade = function(price, portfolio){
//calculate trades
//return an object that has keys representing the asset ids
//with values that represent the number of units invested
}
Calculating the Trades
Once a trade algorithm has been designed, you can calculate the trades once you have a datset. Typically this is done by iterating through each price in the dataset, and running the trade algorithm on that price, retaining the results.
The following code implements a function that captures the results of a backtest using an inputted trade function.
let calculateTrades = async function(data, trade){
let nav = 1;
let portfolio = {cash:1};
let stats = {};
let results = [];
for(let i=0;i<prices.length;i++){
nav = 0;
let price = prices[i];
if('cash' in portfolio) nav+=portfolio.cash;
for(let key in portfolio){
if(key!=='cash') nav += price.close*portfolio[key];
}
portfolio = await trade(price, portfolio, nav, stats);
results.push({
nav:nav,
portfolio:{...portfolio}
});
}
return results;
}
This algorithm assumes that the prices are closing prices and that trades occur at the close. It can be found in the trade module. as the "calculateTrades" function.
let td = await import('/lib/finance/backtest/v1.0.0/trade.mjs');
let trades = td.calculateTrades(price, trade);
Example
A complete example of calculating a backtest is given below.
Starting with a set of prices, we add indicators to that set (in this example, we calculate backward looking returns) and append them to th price object. Then we run the backtest, using a trade function that fully invests if the current return is less than one, otherwise it goes to cash. (not an interesting or even intelligent trade strategy, but it demonstrates how to do the calculation)
let td = await import("/lib/finance/backtest/v1.0.0/trade.mjs");
let prices = [
{date:'2000-01-01', close:100, id:'IBM'},
{date:'2000-01-02', close:101, id:'IBM'},
{date:'2000-01-03', close:100, id:'IBM'},
{date:'2000-01-04', close:99, id:'IBM'},
{date:'2000-01-05', close:102, id:'IBM'},
];
//calculate the necessary indicators needed by the trade algorithm
//in this case, we need to calculate the returns
prices = prices.map((p,i,data)=>{
let result = {
date:p.date,
IBM:p.close,
"return":null
};
if(i>0) result.return = p.close/data[i-1].close
return result;
});
let results = await td.calculateTrades(prices, (price, nav, portfolio)=>{
if(price.return !== null && price.return < 1) return {IBM:nav/price.IBM};
else return {cash:nav}
});
Try it!
Example Trade Functions
The following are simple and commonly used trade strategies that demonstrate the principles of backtesting.
Extracting the NAV
Once you have calculated your trading strategy as above, you will want to compute various statistics to help assess the effectiveness of the strategy. The primary statistic is probably the NAV, from which you can compute a number of other statistics.
If you calculated the nav as above and set that value on each record, you can just use the map function to retrieve the NAV's as a separate array.
let nav = prices.map(p=>p.nav)