回放历史
回测要问的是:一条交易规则在过去的数据上会赚多少。向量化版本一次处理整个数组:不对天数循环,只有每天一个信号、每天一个仓位,再与收益率做一次乘法。它简短、快速、容易检查,所以是筛选想法的第一件工具。它的局限(没有订单、没有现金、没有部分成交)正是下一节要补上的。
import numpy as np
returns = np.array([0.01, 0.02, -0.01, 0.03]) # the asset's daily returns
signal = np.array([2.5, 0.4, -1.0, 0.0]) # any score: positive = buy, negative = sell
position = np.sign(signal) # +1, +1, -1, 0
print(position)一天的延迟
用今天收盘价算出的信号,只能在今天收盘之后交易,所以赚到的是明天的收益,而不是今天的。在数组里这就是一次平移:strategy[t] = position[t - 1] * returns[t],第一天之前没有仓位。忘掉这一步,策略就是在交易它已经看到的收益,回测会好看得离谱。这次平移只有一行,却是整个函数里最重要的一行。
# Yesterday's position earns today's return
import numpy as np
returns = np.array([0.01, 0.02, -0.01, 0.03])
position = np.array([1.0, 1.0, -1.0, 0.0])
strategy = np.zeros(len(returns))
strategy[1:] = position[:-1] * returns[1:]
print(strategy) # [ 0. 0.02 -0.01 -0.03]
print(position * returns) # the same-day version: [ 0.01 0.02 0.01 0. ] looks better, and is cheating净值与统计
每日收益率会复利累乘:每天结束时的价值,是初始资金乘以 1 + 收益率 的累积乘积。从这条净值曲线,三个数就能概括一次回测。总收益是期末净值减 1。夏普比率是日均收益除以其标准差,再乘以 sqrt(periods) 年化。最大回撤是从此前高点出发的最大跌幅 equity / running max - 1,也就是投资者本该经历的损失。
# Equity curve and the three statistics for a short run
import numpy as np
strategy = np.array([0.0, 0.02, -0.01, -0.03, 0.04])
equity = np.cumprod(1 + strategy)
print(np.round(equity, 5))
print(round(float(equity[-1] - 1), 5)) # total return
print(round(float(strategy.mean() / strategy.std(ddof=1) * np.sqrt(252)), 3)) # annualised Sharpe
print(round(float((equity / np.maximum.accumulate(equity) - 1).min()), 5)) # maximum drawdown# A moving-average rule on a simulated price path, with its statistics
import numpy as np
rng = np.random.default_rng(7)
returns = rng.normal(0.0004, 0.01, size=500)
prices = 100 * np.cumprod(1 + returns)
fast = np.convolve(prices, np.ones(10) / 10, mode="valid")[-(len(prices) - 49):] # 10-day mean, aligned
slow = np.convolve(prices, np.ones(50) / 50, mode="valid") # 50-day mean
position = np.zeros(len(prices))
position[49:] = np.sign(fast - slow)
strategy = np.zeros(len(returns))
strategy[1:] = position[:-1] * returns[1:]
equity = np.cumprod(1 + strategy)
drawdown = equity / np.maximum.accumulate(equity) - 1
print(round(float(equity[-1] - 1), 4), round(float(strategy.mean() / strategy.std(ddof=1) * np.sqrt(252)), 4), round(float(drawdown.min()), 4))向量化回测是筛选,而不是定论它假设你在收盘时按所见价格、零成本地交易全部仓位。真实交易有成本、滑点和限制,后面几节会讲;而在同一段历史上试很多规则,总会碰到一条靠运气看起来不错的。有希望的结果是继续检验的理由,而不是交易的理由。
- 仓位 = 信号的符号;策略收益为
position[t - 1] * returns[t]:今天的收益属于昨天的决策。 - 净值是
start * cumprod(1 + r);用总收益、年化夏普和最大回撤来概括。 - 向量化回测是快速筛选:忽略成本和成交,同一段历史上多次尝试会产生运气。
下一节:事件驱动回测器,逐根K线回放历史,带有现金、订单和成交。
实验:评分内容
- 用
signal_to_position把信号变成仓位25 分 - 用
strategy_returns计算前一日仓位的收益25 分 - 用
equity_curve把各日复利相乘25 分 - 用
backtest_stats汇总回测结果25 分