结果解读
回测结束后,回测页面显示收益曲线、统计指标、状态信息、日志信息和账户信息。
收益曲线
收益曲线由策略调用LogProfit()记录的收益值组成(LogProfit)。策略不调用LogProfit()时没有收益曲线,下面依赖收益序列的统计指标也无法计算,只能从账户信息中查看回测结束时的资产。MCP工具get_backtest返回的profit、max_drawdown同样来自LogProfit()。
统计指标
统计指标由收益序列profits(每个元素为[时间戳, 收益])和初始资产totalAssets按下面的算法计算:
| 指标 | 含义 |
|---|---|
| 收益率(totalReturns) | 最后一个收益值 ÷ 初始资产。 |
| 年化收益(annualizedReturns) | 收益率 × 一年的时长(yearDays天)÷ 回测时长,按比例线性折算。 |
| 最大回撤(maxDrawdown) | 资产(初始资产 + 收益)相对此前最高点下跌的最大比例。maxDrawdownStartTime为该最高点的时间,maxDrawdownTime为回撤最深的时间。 |
| 胜率(winningRate) | 收益序列中高于前一个点的点所占的比例(第一个点与0比较)。统计的是收益记录,不是逐笔交易的胜率。 |
| 波动率(volatility) | 把回测时间按天切分,每天的收益额 ÷ 初始资产,再乘以yearDays折算为年化值(没有收益记录的日子按0计),取这些值的总体标准差。 |
| 夏普比率(sharpeRatio) | (年化收益 − 无风险利率3%)÷ 波动率;波动率为0时为0。 |
yearDays是年化时使用的一年天数,由回测页面传入。注意波动率是把每日收益率直接乘以yearDays年化,而不是常见的乘以√yearDays,因此这里的夏普比率不宜与其他平台的数值直接比较。
算法源码:
javascript
function returnAnalyze(totalAssets, profits, ts, te, period, yearDays) {
// force by days
period = 86400000
if (profits.length == 0) {
return null
}
var freeProfit = 0.03 // 0.04
var yearRange = yearDays * 86400000
var totalReturns = profits[profits.length - 1][1] / totalAssets
var annualizedReturns = (totalReturns * yearRange) / (te - ts)
// MaxDrawDown
var maxDrawdown = 0
var maxAssets = totalAssets
var maxAssetsTime = 0
var maxDrawdownTime = 0
var maxDrawdownStartTime = 0
var winningRate = 0
var winningResult = 0
for (var i = 0; i < profits.length; i++) {
if (i == 0) {
if (profits[i][1] > 0) {
winningResult++
}
} else {
if (profits[i][1] > profits[i - 1][1]) {
winningResult++
}
}
if ((profits[i][1] + totalAssets) > maxAssets) {
maxAssets = profits[i][1] + totalAssets
maxAssetsTime = profits[i][0]
}
if (maxAssets > 0) {
var drawDown = 1 - (profits[i][1] + totalAssets) / maxAssets
if (drawDown > maxDrawdown) {
maxDrawdown = drawDown
maxDrawdownTime = profits[i][0]
maxDrawdownStartTime = maxAssetsTime
}
}
}
if (profits.length > 0) {
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
}
for (var n = ts; n < rangeEnd; n += period) {
var dayProfit = 0.0
var cut = n + period
while (i < profits.length && profits[i][0] < cut) {
dayProfit += (profits[i][1] - preProfit)
preProfit = profits[i][1]
i++
}
perRatio = ((dayProfit / totalAssets) * yearRange) / period
sum += perRatio
datas.push(perRatio)
}
var sharpeRatio = 0
var volatility = 0
if (datas.length > 0) {
var avg = sum / datas.length;
var std = 0;
for (i = 0; i < datas.length; i++) {
std += Math.pow(datas[i] - avg, 2);
}
volatility = Math.sqrt(std / datas.length);
if (volatility !== 0) {
sharpeRatio = (annualizedReturns - freeProfit) / volatility
}
}
return {
totalAssets: totalAssets,
yearDays: yearDays,
totalReturns: totalReturns,
annualizedReturns: annualizedReturns,
sharpeRatio: sharpeRatio,
volatility: volatility,
maxDrawdown: maxDrawdown,
maxDrawdownTime: maxDrawdownTime,
maxAssetsTime: maxAssetsTime,
maxDrawdownStartTime: maxDrawdownStartTime,
winningRate: winningRate
}
}
数据下载
- 状态栏数据下载:回测结束后,在「状态信息」栏右上角点击「下载表格」,下载回测结束时状态栏数据的CSV文件。
- 日志数据下载:在「日志信息」栏右上角点击「下载表格」,下载回测日志的CSV文件。