Take you to analyze the Sharpe ratio, maximum drawdown, return rate and other indicator algorithms in the strategy backtesting
Some performance indicator algorithms of the strategy are often discussed by group members, and an algorithm has also been disclosed in the API document of FMZ. However, it is not easy to understand without comments. In this article, I will take you to analyze the algorithm. I believe that you shall have a clear understanding of the concepts of Sharpe ratio, maximum drawdown, return rate and the calculation logic after reading this article.
We will start with the source code, which is written in JavaScript language. The backtesting system of FMZ also adopts this algorithm to generate backtesting performance data automatically.
returnAnalyze function
javascriptfunction returnAnalyze(totalAssets, profits, ts, te, period, yearDays)
https://www.fmz.com/api#Backtesting system Sharpe algorithm
Since it is a calculation function, there must be inputs and outputs. Let's look at the input of the function first:
totalAssets, profits, ts, te, period, yearDays
totalAssets
This parameter is the initial total assets when the strategy starts running.profits
This parameter is an important one, because a series of performance indicators are calculated based on this original data. This parameter is a two-dimensional array in the following format:[[timestamp1, profit1], [timestamp2, profit2], [timestamp3, profit3],..., [timestampN, profitN]]
. It can be seen that the returnAnalyze function needs such a data structure that record the chronological order of returns at each time. Timestamp1 to timestampN are in chronological order from far to near. There is a profit value at each time point. For example, the third time point in the returns record is [timestamp 3, profit3]. In the online backtesting system of FMZ, the data of the profits array is provided to this function by the backtesting system. Of course, if you record the return data by yourself and form such an array structure, you can also provide it to the calculation function to calculate the results.ts
The start timestamp of the backtest.te
The end timestamp of the backtest.
period
Calculation period at the millisecond level.
- yearDays
Trading days in a year.
Next, let's look at the output of this function together:
javascriptreturn {
totalAssets: totalAssets,
yearDays: yearDays,
totalReturns: totalReturns,
annualizedReturns: annualizedReturns,
sharpeRatio: sharpeRatio,
volatility: volatility,
maxDrawdown: maxDrawdown,
maxDrawdownTime: maxDrawdownTime,
maxAssetsTime: maxAssetsTime,
maxDrawdownStartTime: maxDrawdownStartTime,
winningRate: winningRate
}
totalAssets: total assets
yearDays: trading days
totalReturns: total returns
annualizedReturns: annualized returns
sharpeRatio: Sharpe ratio
volatility: volatility
maxDrawdown: maximum drawdown
maxDrawdownTime: timestamp at maximum drawdown
maxAssetsTime: timestamp at maximum assets
maxDrawdownStartTime: start time of maximum drawdown
winningRate: winning rate
Knowing the input and output, we can understand what the function is used for. To put it simply, it gives the function some original records, such as the return statistics array. The function will give you a result to show the performance of the backtest.
Next, let's see how the code is calculated:
javascriptfunction returnAnalyze(totalAssets, profits, ts, te, period, yearDays) {
// force by days
period = 86400000 // The number of milliseconds in a day, that is 60 * 60 * 24 * 1000
if (profits.length == 0) { // If the length of the array of profits is 0, it cannot be calculated and it will return to the null value directly
return null
}
var freeProfit = 0.03 // Risk-free interest rate, which can also be set according to the demand, such as 3% annualized national debt
var yearRange = yearDays * 86400000 // Milliseconds of all accumulated trading days in a year
var totalReturns = profits[profits.length - 1][1] / totalAssets // Total return rate
var annualizedReturns = (totalReturns * yearRange) / (te - ts) // The annualized rate of return, the expected rate of return obtained by scaling the time of profit statistics to the scale of one year
// MaxDrawDown
var maxDrawdown = 0 // Initialize the maximum drawdown variable to 0
var maxAssets = totalAssets // Initialize maximum asset variable with initial net value assignment
var maxAssetsTime = 0 // Timestamp of the time when the maximum asset is initialized
var maxDrawdownTime = 0 // Timestamp of the time when the maximum drawdown is initialized
var maxDrawdownStartTime = 0 // Timestamp of the time when the maximum start time is initialized
var winningRate = 0 // Initialized win rate is 0
var winningResult = 0 // Record the number of win time
for (var i = 0; i < profits.length; i++) { // Traverse the return array
if (i == 0) {
if (profits[i][1] > 0) { // If the first return record point, the return is greater than 0, it means the profit
winningResult++ // The number of win times accumulates 1
}
} else { // If it is not the first returns record point, as long as the returns of the current point is greater than the returns of the previous moment (return point), it means profit, and the number of win times accumulates 1
if (profits[i][1] > profits[i - 1][1]) {
winningResult++
}
}
if ((profits[i][1] + totalAssets) > maxAssets) { // If the return plus the initial net value at that moment is greater than the largest asset recorded to have occurred, the value of the largest asset is updated and the timestamp of this moment is recorded
maxAssets = profits[i][1] + totalAssets
maxAssetsTime = profits[i][0]
}
if (maxAssets > 0) { // When the maximum asset value recorded is greater than 0, the drawdown is calculated
var drawDown = 1 - (profits[i][1] + totalAssets) / maxAssets
if (drawDown > maxDrawdown) { // If the current drawdown is greater than the recorded maximum drawdown, update the maximum drawdown, maximum drawdown time, etc
maxDrawdown = drawDown
maxDrawdownTime = profits[i][0]
maxDrawdownStartTime = maxAssetsTime
}
}
}
if (profits.length > 0) { // Calculate the winning rate
winningRate = winningResult / profits.length
}
// trim profits
var i = 0
var datas = []
var sum = 0
var preProfit = 0
var perRatio = 0
var rangeEnd = te
if ((te - ts) % period > 0) {
rangeEnd = (parseInt(te / period) + 1) * period // Process rangeEnd as an integer multiple of period
}
for (var n = ts; n < rangeEnd; n += period) {
var dayProfit = 0.0
var cut = n + period
while (i < profits.length && profits[i][0] < cut) { // Ensure that when the timestamp is not out of bounds, the array length is also not out of bounds
dayProfit += (profits[i][1] - preProfit) // Calculate the daily returns
preProfit = profits[i][1] // Record yesterday's returns
i++ // Accumulate i for accessing the next profits node
}
perRatio = ((dayProfit / totalAssets) * yearRange) / period // Calculate the annualized rate of return at that time
sum += perRatio // Accumulation
datas.push(perRatio) // Put in the array datas
}
var sharpeRatio = 0 // Initial Sharpe ratio is 0
var volatility = 0 // Initial volatility is 0
if (datas.length > 0) {
var avg = sum / datas.length; // Find the mean value
var std = 0;
for (i = 0; i < datas.length; i++) {
std += Math.pow(datas[i] - avg, 2); // The std is used to calculate the following variance. The following std/datas.length is the variance, and the square root of the number is the standard deviation
}
volatility = Math.sqrt(std / datas.length); // In terms of years, volatility is the standard deviation
if (volatility !== 0) {
sharpeRatio = (annualizedReturns - freeProfit) / volatility // Sharpe formula to calculate Sharpe ratio: (annualized return rate - risk-free rate) / standard deviation
}
}
return {
totalAssets: totalAssets,
yearDays: yearDays,
totalReturns: totalReturns,
annualizedReturns: annualizedReturns,
sharpeRatio: sharpeRatio,
volatility: volatility,
maxDrawdown: maxDrawdown,
maxDrawdownTime: maxDrawdownTime,
maxAssetsTime: maxAssetsTime,
maxDrawdownStartTime: maxDrawdownStartTime,
winningRate: winningRate
}
}
On the whole, the algorithm is not complex, and there may be several concepts that need to be understood in advance.
Variance:
It can be understood as a set of return data.
The group of samples, 1, 2, 3, 4 and 5, has a mean value of (1+2+3+4+5)/5 = 3, while the variance is the mean value of the sum of the squares of the differences between each data and its sum respectively, which is [(1-3)^2 + (2-3)^2 + (3-3)^2 + (4-3)^2 + (5-3)^2]/5 = 2, and the variance is 2.Standard deviation:
Calculate the arithmetic square root of variance, which is the standard deviation.Volatility:
When the calculation scale is annualized, the volatility is the standard deviation.
With the understanding of these concepts and calculation formulas, the Sharpe calculation part of the function will be clear at a glance.
Sharpe formula to calculate Sharpe ratio: (annualized return rate - risk-free rate) / standard deviation
Have you learned it yet?
Subscribe to my newsletter
Read articles from FMZ Quant directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
FMZ Quant
FMZ Quant
Quantitative Trading For Everyone