上一篇回顾
上一篇是数据获取篇,在上一篇里,我们初步接触了backtrader的回测逻辑,重点放在了回测的数据获取的问题上,确保了我们在用合适且有效的正规数据在做回测,我们的目的是要通过backtrader深入讨论量化交易的内容,并在实践中逐步完善自己的交易系统和养成正确的交易思路。
- 第1节里讨论了backtrader的数据来源,对pandas的数据要求进行了分析,做到能把相应的数据处理成backtrader接受的格式。
- 第2节里通过akshare获取了全股票列表/ETF列表,使用pypinyin库制作了中文名/拼音缩写/代码都能查找的功能,制作了自选股列表并且通过akshare获取了日线的历史行情数据。
- 第3节爬取数据是支线拓展与主线任务关系不太。
- 第4节结合通达信的应用,为backtrader更深入、多样化应用提供了新的思路。
那么,我们会以数据获取篇为基础,继续我们的backtrader实践。
基础回测加强
01_数据及列表准备
从第一篇已经知道怎样获取用于回测的数据,但单一的数据没有代表性,这里先准备5支股票+2个指数用于后续的实践,并且直接制作函数通过akshare下载并处理数据,存放在df_list中,对于Notebook而言,它的workspace会保存我们在操作过程中的变量,数据和函数,让我们可以在其他格子里代码调用它们。
001_选股
一般在股票软件中都是选股的功能,不同的软件都有其各自的特色。而且现在又出来AI选股的功能,比如通达信的小达,同花顺的问财等。
选股是交易的前置条件,如果选中了一支接下来趋势向上的股票,那么多半最终收益是正的,而你应用的策略胜率和盈亏比都会比较高,否则就是相反,多半是亏损的。
以小达为例,我们可以通过”热点“,”主题“,“板块” 及“热搜” 针对当前的热点及进选股,也可以根据基本面(市盈率,市净率...)或者资金面,或者K线形态(红三兵,出水芙蓉...)以及技术指标策略(MACD金叉死叉...)进行选股操作。
后续我们的策略也会参考着对指标,基本面以及热度进行回测。不过当前,我们需要从5000多支股票中选出5支股票。这里我们采用akshare读取股票热度排名,然后以第500名开始每隔1000名选择一个。
# 获取问财最近交易日的热度排名
import akshare as ak
stock_hot_rank_wc_df = ak.stock_hot_rank_wc(date="20240913")
stock_hot_rank_wc_df
-----------------------
序号 股票代码 股票简称 现价 涨跌幅 个股热度 个股热度排名 排名日期
0 1 000536 华映科技 3.63 10.00 30762469.0 1/5359 20240913
1 2 600611 大众交通 6.76 -9.99 27546732.5 2/5359 20240913
2 3 000062 深圳华强 29.48 -9.98 27264696.5 3/5359 20240913
3 4 000158 常山北明 8.30 -4.16 23935850.0 4/5359 20240913
4 5 600550 保变电气 8.51 9.95 17938728.5 5/5359 20240913
... ... ... ... ... ... ... ... ...
4995 4996 688100 威胜信息 34.31 -0.70 14574.0 4996/5359 20240913
4996 4997 688432 有研硅 8.75 -0.79 14512.5 4997/5359 20240913
4997 4998 600051 宁波联合 5.20 -0.76 14504.0 4998/5359 20240913
4998 4999 688130 晶华微 16.60 -2.24 14503.5 4999/5359 20240913
4999 5000 002200 ST交投 4.86 -0.20 14477.5 5000/5359 20240913
5000 rows × 8 columns
获取5支股票,使用pandas的列表索引功能获取这5支股票的信息如下:
sel_index = [500,1500,2500,3500,4500]
df_sel = stock_hot_rank_wc_df.iloc[sel_index,:]
df_sel
-----------------------------
序号 股票代码 股票简称 现价 涨跌幅 个股热度 个股热度排名 排名日期
500 501 001287 中电港 15.51 -2.76 537239.5 501/5359 20240913
1500 1501 002179 中航光电 36.95 -2.28 169509.0 1501/5359 20240913
2500 2501 600860 京城股份 7.53 -1.57 89909.0 2501/5359 20240913
3500 3501 300233 金城医药 10.99 -0.81 50961.5 3501/5359 20240913
4500 4501 002774 快意电梯 5.39 -1.46 26007.5 4501/5359 20240913
通过pandas的tolist()把选中的股票代码列(Series类型)转成列表,得到我们的自选股列表:
myStockList = df_sel.股票代码.tolist()
myStockList
---------------------
['001287','002179','600860','300233','002774']
002_选指数
除了5支股票外,我们还需要选择2个指数,用于基准。在后续的“评价”里会详细说关于基准日收益的问题,现在只需要知道我们要一个上证指数,一个沪深300指数作为回测股票的对比基准即可。
指数有很多,在通达信里打开市场--主要指数的页面,就会把沪深京的主要指数列出来,从这里我们先记录下这些指数的代码:
- 上证指数: 000001 (这个就是sh000001,深市的平安银行也是000001(sz000001?))
- 沪深300指数: 000300
- 深证成指:399001
- 创业板指:399006
- 5年国债指数:000140
仍然可以采用akshare来获取指数的历史行情数据,函数从stock开头换成index开头:
df_index_em = ak.index_zh_a_hist('000300',start_date='20220101')
df_index_em
------------------------
日期 开盘 收盘 最高 最低 成交量 成交额 振幅 涨跌幅 涨跌额 换手率
0 2022-01-04 4957.98 4917.77 4961.45 4874.53 151534776 3.365170e+11 1.76 -0.46 -22.60 0.48
1 2022-01-05 4907.93 4868.12 4916.28 4851.98 178816100 3.639445e+11 1.31 -1.01 -49.65 0.57
2 2022-01-06 4842.16 4818.23 4857.56 4786.43 157665825 3.217501e+11 1.46 -1.02 -49.89 0.50
3 2022-01-07 4824.32 4822.37 4856.65 4818.19 187139412 3.322189e+11 0.80 0.09 4.14 0.60
4 2022-01-10 4812.23 4844.05 4844.39 4780.82 156211697 3.050338e+11 1.32 0.45 21.68 0.50
... ... ... ... ... ... ... ... ... ... ... ...
651 2024-09-09 3214.80 3192.95 3222.93 3180.93 108673069 1.465161e+11 1.30 -1.19 -38.40 0.35
652 2024-09-10 3193.46 3195.76 3205.10 3171.79 97290078 1.336105e+11 1.04 0.09 2.81 0.31
653 2024-09-11 3187.05 3186.13 3197.34 3174.03 102482110 1.415138e+11 0.73 -0.30 -9.63 0.33
654 2024-09-12 3189.43 3172.47 3201.55 3171.88 92868657 1.363872e+11 0.93 -0.43 -13.66 0.30
655 2024-09-13 3174.03 3159.25 3188.91 3159.25 88336677 1.374489e+11 0.93 -0.42 -13.22 0.28
656 rows × 11 column
003_用宽指ETF代替指数
简单的说,就是可以用ETF在某些情况下代替指数,且ETF是可以交易的。我们可以首先获取到ETF的实时行情,并只取代码,名称和前几列的数据:
import akshare as ak
import pandas as pd
fund_etf_spot_em_df = ak.fund_etf_spot_em()
df_etf = fund_etf_spot_em_df.iloc[:,:5]
---------------------
代码 名称 最新价 IOPV实时估值 基金折价率
0 159321 黄金股票ETF 0.834 0.8272 -0.82
1 159315 黄金股ETF基金 0.873 0.8695 -0.40
2 159562 黄金股ETF 1.141 1.1395 -0.13
3 517400 黄金股票ETF 0.846 0.8452 -0.09
4 517520 黄金股ETF 1.050 1.0508 0.08
... ... ... ... ... ...
951 159327 半导体设备ETF基金 0.856 0.8583 0.27
952 159775 新能源车电池ETF 0.423 0.4233 0.07
953 517880 品牌消费ETF 0.727 0.7256 -0.19
954 515030 新能源车ETF 0.909 0.9110 0.22
955 588830 科创新能源ETF 0.919 0.9221 0.34
956 rows × 5 columns
然后使用Pandas的DataFrame进行查询,得到含有“沪深300”的项或者含有“上证”的项
df_hs300 = df_etf.loc[df_etf['名称'].str.contains('沪深300'),:]
df_hs300.head(10)
---------------------
代码 名称 最新价 IOPV实时估值 基金折价率
181 560330 沪深300价值ETF申万菱信 0.959 0.9586 -0.04
270 562320 沪深300价值ETF 1.028 1.0294 0.14
275 159330 沪深300ETF基金 0.951 0.9490 -0.21
284 159510 沪深300价值ETF 0.898 0.8968 -0.13
301 561900 沪深300ESGETF 0.678 0.6776 -0.06
342 512530 沪深300红利ETF 1.251 1.2506 -0.03
388 515360 方正沪深300ETF 4.273 4.2649 -0.19
394 515390 沪深300ETF指数基金 0.894 0.8937 -0.03
395 561000 沪深300ETF增强基金 0.892 0.8929 0.10
406 159673 沪深300ETF鹏华 0.845 0.8434 -0.19
比如这里我们就选择515390这支ETF做沪深300指数的替代数据。
004_数据与列表保存
myStockList = ['001287','002179','600860','300233','002774']
myIndexList = ['000001','000300']
df_stock_list = [] # 用于保存每支股票的dataframe数据
df_index_list = []
def get_df_from_stock(code1):
stock_df = ak.stock_zh_a_hist(symbol=code1, period="daily", start_date="20210301", adjust="qfq")
stock_df.rename(columns={
'日期':'date','开盘':'open','收盘':'close',
'最高':'high','最低':'low', '成交量':'volume',
},inplace=True)
stock_df.index = pd.to_datetime(stock_df.date)
stock_df['openinterest'] = 0
stock_df = stock_df[['open','high','low','close','volume','openinterest']]
return stock_df
def get_df_from_index(code1): # 与上面函数只是请求函数名不同index_zh_a_hist
pass # 略
for x in myIndexList:
df_tmp = get_df_from_index(x)
df_index_list.append(df_tmp)
for x in myStockList:
df_tmp = get_df_from_stock(x)
df_stock_list.append(df_tmp)
到这里,我们就把后续回测要用到的5支股票和2支指数的数据准备好了。
02_最简回测系统及问题
001_生成最简回测代码
我们还是通过AI助手生成backtrader双均线策略的回测代码,这里的代码就相当于一个最简回测系统。然后在上面把我们的数据放进去,这次用的是myStockList[2],即“600860-京城股份”的数据,开始日期为2021/9/1,结束日期默认当前最后一个交易日,跨度差不多为3年。
from datetime import datetime
import backtrader as bt
class SmaCross(bt.Strategy):
params = (
('fast', 5),
('slow', 10),
)
def __init__(self):
self.fast_ma = bt.indicators.SMA(self.data.close, period=self.params.fast)
self.slow_ma = bt.indicators.SMA(self.data.close, period=self.params.slow)
self.crossover = bt.indicators.CrossOver(self.fast_ma, self.slow_ma)
def next(self):
if not self.position:
if self.crossover > 0:
self.buy()
else:
if self.crossover < 0:
self.close()
cerebro = bt.Cerebro() # 创建Cerebro引擎
# 将数据源设置为PandasData,并加载数据
data = bt.feeds.PandasData(dataname=df_stock_list[2],fromdate=datetime(2021, 9, 1))
cerebro.adddata(data) # 将数据添加到Cerebro
cerebro.addstrategy(SmaCross) # 添加策略
cerebro.broker.setcash(100000.0) # 设置初始资金
cerebro.broker.setcommission(commission=0.001) # 设置交易佣金
cerebro.addsizer(bt.sizers.PercentSizer, percents=50) # 设置每次交易使用资金的比例为50%
print(f'Start Portfolio Value: {cerebro.broker.getvalue():.2f}') # 打印起始组合价值
cerebro.run() # 运行分析
print(f'Final Portfolio Value: {cerebro.broker.getvalue():.2f}') # 打印最后组合价值
cerebro.plot() # 绘制结果图
----------------------------
Start Portfolio Value: 100000.00
Final Portfolio Value: 138102.81
从结果上看这次回测是盈利的,而且利润率达到惊人的38%,但从图形输出上看,恰好开局在低位不到7块,然后拉升到了25之上,如果回测的时间点往后2个月(2021/11/1)再开始,那么从高位掉到最后的7.53,最终的资产是 67172.88,反而亏了30%多。所以,单一的时间跨度可以有很大的概率出现妖的情况,并不代表策略的整体水平,这也是评价指标里大多会有近半年,近1年,近2年收益的指标,从而更多方位,较全面的策略进行分析。策略是需要择时的。
002_最简回测的问题
由于上面只是一个最简的回测系统,只是刚刚能把一个策略跑起来,因此它必然存在很多的疑问和问题,我们要一个一个的来看,并通过实践理解、解决或改善它们。
我们从代码顺序和显示图像的从上至下的顺序来看一下:
- bt.feeds.PandasData() - 把准备好的股票DataFrame数据转换成backtrader的格式,涉及到起止日期的选择,datetime类型的应用,另外如果对多支股票(我们已经准备好了5支股票)进行回测如何变化等。
- adddata(data) - 可以添加多个data吗?多个data数据怎么获取和处理?
- addstrategy() - 策略类怎么写,可以添加多个strtegy吗?几个data能用同一个策略吗?
- broker.setcash , setcommission - 怎么设置资金、佣金、滑点...与交易相关的数值
- cerebro.plot() - 绘制图形,(观察器 observer)
- 图的第一栏是资产和现金变化曲线,一开始就冲到240000去了,后面全是在回撤
- 所以有没有显示回撤的曲线,需要添加
- 交易的成功/失败图标和颜色不易于观察,需要修改
- 主图上没有K线,默认是收盘close的连线,需要修改
- 主图上buy/sell的图标与颜色是国际的涨绿跌红,与我们国情相反,需要修改
- 主图上的volume柱状图的颜色也反了,也需要修改
- 哪些指标在主图上显示,哪些在副图上?怎么设置?
03_后续主要实践内容与目标
001_后续实践内容
由上面最简回测系统初步观察到的问题,我们把本篇后续的实践内容制定了初步的计划:
- 数据(data)进阶 - 包括feeds多个股票数据,adddata多个data,这些数据怎么获取和应用
- 策略(strategy)进阶 - strategy类包含哪些,多策略循环
- 交易设置(broker)进阶 - 各种交易的设置,建立自己的交易体系
- 观察器与绘图进阶 - 绘制符合我们需要的图形
002_本篇的目标
通过完成本篇的基础加强的实践后,我们可以把所有的更改和添加的数据、类、函数等都制作到一个xx.py文件,这样后续在notebook里%run xx.py文件就能把增强的配置load进Notebook中,每次新的回测只需要写策略类的核心代码即可,就能完成股票的回测,且能够循环回测。
在完成本篇实践后,对交易系统有更深层的理解,并能把策略的研究作为重点,可以忽略其他一些繁琐的小问题,把时间和精力集中放在策略的编写和优化上,能大大提升我们的效率。