import akshare as ak
29 滚动测试范例
这里做一个滚动测试的范例。
29.1 滚动测试
滚动测试:向后看一段时间,找出模型在过去一段时间的最优参数,利用这个参数,向前做一段时间的交易。
例如:向后看3个月(黄色格),向前交易2个月(绿色格)
- 测试1、2、3月,找出这3个月的最优参数,用来执行4、5月的交易。
- 测试3、4、5月,找出这3个月的最优参数,用来执行6、7月的交易。
- 如此类推,最后把4、5、6、7 …的交易结果组合起来,就是最终的滚动测试的结果。
29.2 需求
- 用单均线系统,进行滚动测试。
- 最优参数(均线周期),由最高的夏普比率决定。
- 向后和向前的时间是整月,数量由外部决定。
计算移动平均和夏普比率的函数
def sma(x,period):
return x.rolling(period).mean()
def sharpe_ratio(lrets, rf = 0.03):
return (np.mean(lrets) * 250 - rf) / (np.std(lrets) * np.sqrt(250))
29.3 简单数据处理
# 读取宁德时代
# 见:https://akshare.xyz/data/stock/stock.html#id20
= ak.stock_zh_a_hist(symbol="300750",start_date='20180101',end_date='20241212', adjust='qfq')
df
df
日期 | 股票代码 | 开盘 | 收盘 | 最高 | 最低 | 成交量 | 成交额 | 振幅 | 涨跌幅 | 涨跌额 | 换手率 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2018-06-11 | 300750 | 9.64 | 12.99 | 12.99 | 9.64 | 788 | 2.845471e+06 | 48.98 | 89.91 | 6.15 | 0.04 |
1 | 2018-06-12 | 300750 | 15.00 | 15.00 | 15.00 | 15.00 | 266 | 1.058375e+06 | 0.00 | 15.47 | 2.01 | 0.01 |
2 | 2018-06-13 | 300750 | 17.21 | 17.21 | 17.21 | 17.21 | 450 | 1.972314e+06 | 0.00 | 14.73 | 2.21 | 0.02 |
3 | 2018-06-14 | 300750 | 19.64 | 19.64 | 19.64 | 19.64 | 743 | 3.578184e+06 | 0.00 | 14.12 | 2.43 | 0.03 |
4 | 2018-06-15 | 300750 | 22.32 | 22.32 | 22.32 | 22.32 | 2565 | 1.359503e+07 | 0.00 | 13.65 | 2.68 | 0.12 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
1563 | 2024-11-19 | 300750 | 262.02 | 268.72 | 272.00 | 258.00 | 348129 | 9.211079e+09 | 5.32 | 2.21 | 5.80 | 0.89 |
1564 | 2024-11-20 | 300750 | 266.95 | 267.55 | 269.90 | 264.60 | 227088 | 6.060616e+09 | 1.97 | -0.44 | -1.17 | 0.58 |
1565 | 2024-11-21 | 300750 | 265.66 | 268.26 | 271.43 | 263.66 | 205508 | 5.510534e+09 | 2.90 | 0.27 | 0.71 | 0.53 |
1566 | 2024-11-22 | 300750 | 267.93 | 259.10 | 269.00 | 258.88 | 225551 | 5.937580e+09 | 3.77 | -3.41 | -9.16 | 0.58 |
1567 | 2024-11-25 | 300750 | 263.06 | 263.16 | 266.56 | 261.18 | 236629 | 6.243969e+09 | 2.08 | 1.57 | 4.06 | 0.61 |
1568 rows × 12 columns
'日期'] = pd.to_datetime(df['日期'])
df['日期',inplace = True)
df.set_index(= df[['涨跌幅']]/100
df 0,inplace=True)
df.fillna('price'] = (df['涨跌幅'] + 1).cumprod() # 用涨跌幅反算出复权价,上市日前一天为1。
df['lrets'] = np.log(df['涨跌幅'] + 1)
df[ df.tail()
涨跌幅 | price | lrets | |
---|---|---|---|
日期 | |||
2024-11-19 | 0.0221 | 39.304715 | 0.021859 |
2024-11-20 | -0.0044 | 39.131774 | -0.004410 |
2024-11-21 | 0.0027 | 39.237430 | 0.002696 |
2024-11-22 | -0.0341 | 37.899434 | -0.034695 |
2024-11-25 | 0.0157 | 38.494455 | 0.015578 |
29.4 构造滚动窗口
需求:构造出回看月份,和测试月份的序列
例如:回看[4,5,6]月,交易[7,8]月;回看[6,7,8]月,交易[9,10]月,那么
= [[4,5,6],[6,7,8]]
train_time = [[7,8],[9,10]] test_time
相同的索引i,就可以取出回看区间和交易区间。
# 获得所有月份
= df.resample('M').last().index.strftime('%Y-%m')
all_months all_months
/tmp/ipykernel_1616841/2225266909.py:3: FutureWarning: 'M' is deprecated and will be removed in a future version, please use 'ME' instead.
all_months = df.resample('M').last().index.strftime('%Y-%m')
Index(['2018-06', '2018-07', '2018-08', '2018-09', '2018-10', '2018-11',
'2018-12', '2019-01', '2019-02', '2019-03', '2019-04', '2019-05',
'2019-06', '2019-07', '2019-08', '2019-09', '2019-10', '2019-11',
'2019-12', '2020-01', '2020-02', '2020-03', '2020-04', '2020-05',
'2020-06', '2020-07', '2020-08', '2020-09', '2020-10', '2020-11',
'2020-12', '2021-01', '2021-02', '2021-03', '2021-04', '2021-05',
'2021-06', '2021-07', '2021-08', '2021-09', '2021-10', '2021-11',
'2021-12', '2022-01', '2022-02', '2022-03', '2022-04', '2022-05',
'2022-06', '2022-07', '2022-08', '2022-09', '2022-10', '2022-11',
'2022-12', '2023-01', '2023-02', '2023-03', '2023-04', '2023-05',
'2023-06', '2023-07', '2023-08', '2023-09', '2023-10', '2023-11',
'2023-12', '2024-01', '2024-02', '2024-03', '2024-04', '2024-05',
'2024-06', '2024-07', '2024-08', '2024-09', '2024-10', '2024-11'],
dtype='object', name='日期')
= [] # 回看的月份
train_time = [] # 前向交易的月份
test_time
# 回看周期
= 2
back_n
# 前向交易周期
= 1
test_n
# 遍历所有月份
# 把第0月到back_n-1月,放入train_time
# 把back_n月到back_n + test_n月,放入test_time
for i in range(0,len(all_months) - (back_n + test_n) + 1,test_n):
+back_n)])
train_time.append(all_months[i:(i+back_n:(i+back_n+test_n)])
test_time.append(all_months[i
print(train_time[0])
print(test_time[0])
print(train_time[1])
print(test_time[1])
Index(['2018-06', '2018-07'], dtype='object', name='日期')
Index(['2018-08'], dtype='object', name='日期')
Index(['2018-07', '2018-08'], dtype='object', name='日期')
Index(['2018-09'], dtype='object', name='日期')
29.5 构造一个测试窗口的最优化函数
需求:给出测试的时间段,问哪个均线的夏普比率最高?
思路:
- 暴力测试全部均线
- 返回均线周期,和对应时间段的夏普比率
# 对于指定的起点和终点,找出最佳的均线
def get_best_ma(start, end):
# 暴力优化,穷举60,70,...250的所有周期
= range(60,250,10)
ma_period_list
= []
result
for ma_period in ma_period_list:
# 单均线交易系统,很简单
'ma'] = sma(df.price, ma_period)
df['pos'] = np.where(df.price > df.ma,1,0) # 只允许做多
df['traded'] = df['lrets'] * df.pos.shift()
df[
# 计算测试区间[start:end]的夏普比率
result.append((ma_period, sharpe_ratio(df.loc[start:end].traded)))
= pd.DataFrame(result)
res = ['ma_period','sharpe']
res.columns return res
'2021-05','2021-11').sort_values(['sharpe','ma_period'],ascending=False).head() get_best_ma(
ma_period | sharpe | |
---|---|---|
18 | 240 | 2.026973 |
17 | 230 | 2.026973 |
16 | 220 | 2.026973 |
15 | 210 | 2.026973 |
14 | 200 | 2.026973 |
29.6 构造一个窗口测试函数
需求:对于一个测试窗口i,包括训练集(回看最优)和测试集(前向交易),计算前向交易的结果
def test_a_window(i):
"""测试一个窗口i。
例如,寻找7月和8月的最佳均线,用来执行9月的交易,返回9月的交易结果。
"""
# 前面算好的测试用窗口i,取起点和终点的时间
= train_time[i][0]
start = train_time[i][-1]
end # 问,这段时间内,什么均线的参数最优?
= get_best_ma(train_time[i][0],train_time[i][-1]
res 'sharpe','ma_period'],ascending=False).iloc[0]
).sort_values([
#print(f"在{start}到{end}之间,最佳的均线是{int(res['ma_period'])},区间年化夏普比率是{round(res['sharpe'],4)}。")
# 利用上述最优的参数,我们进行交易
= int(res['ma_period'])
ma_period 'ma'] = sma(df.price, ma_period)
df['pos'] = np.where(df.price > df.ma,1,0)
df['traded'] = df['lrets'] * df.pos.shift()
df[#print(f"使用上述设置,在{list(test_time[i])}内的对数收益率是{round(df.loc[test_time[i][0]:test_time[i][-1]].traded.sum(),4)}")
# 返回测试集对应时间段的持仓情况。
return df['pos'].loc[test_time[i][0]:test_time[i][-1]]
4).head() test_a_window(
/tmp/ipykernel_1616841/2749032695.py:5: RuntimeWarning: divide by zero encountered in scalar divide
return (np.mean(lrets) * 250 - rf) / (np.std(lrets) * np.sqrt(250))
日期
2018-12-03 1
2018-12-04 1
2018-12-05 1
2018-12-06 1
2018-12-07 1
Name: pos, dtype: int64
29.7 测试所有的窗口
最后把所有滚动交易的结果组合在一起
= []
pos_final for i in range(len(train_time)):
pos_final.append(test_a_window(i))
= pd.concat(pos_final) pos_final
/tmp/ipykernel_1616841/2749032695.py:5: RuntimeWarning: divide by zero encountered in scalar divide
return (np.mean(lrets) * 250 - rf) / (np.std(lrets) * np.sqrt(250))
29.8 结果
'strategy'] = df.lrets * pos_final.shift() # 注意不要作弊
df.loc[:,'lrets','strategy']].loc['2020':].cumsum().apply(np.exp).plot(); df[[
计算一些比率
'lrets','strategy']].loc['2020':].agg("sum") df[[
lrets 1.620477
strategy 1.904934
dtype: float64
'lrets','strategy']].loc['2020':].agg(sharpe_ratio) df[[
lrets 0.627031
strategy 0.873380
dtype: float64