搭建过程
每个交易者都应该形成一套自己的交易系统。
很多交易者也清楚知道,搭建自己交易系统的重要性。现实中,从0到1往往是最难跨越的一步。
授人鱼不如授人以渔,为了帮助大家跨出搭建量化系统的第一步,我们决定推出这个主题系列。
这个系列中,我们用Python从0开始一步步搭建出一套ETF量化交易系统(选择ETF标的是因为对于普通交易者来说,ETF相对于选强势股难度要小,而且没有退市风险)。大家可以跟随着我们的实现路径来一起学习,从过程中掌握方法。
掌握了方法之后,你可以换成期货系统、比特币系统、美股系统,然后在实战中不断去完善自己的系统了。
搭建一套ETF量化交易系统涉及多个模块和组件的协同工作,包括数据源模块、量化策略模块、可视化模块、数据库模块、回测评估模块、自动交易模块等等。
DAY1链接如下:15天搭建ETF量化交易系统Day1—数据源模块
DAY2链接如下:15天搭建ETF量化交易系统Day2—图形显示模块
DAY3我们开始涉及回测评估模块,先快速上手市面上非常经典的BackTrader,为下一步策略开发提供基础。
准备工作
量化交易系统中的回测模块是一个核心组成部分,用于在历史数据上测试和评估交易策略的性能。
回测模块通过模拟历史市场环境,让交易策略在这些历史数据上进行“实战演练”,从而得出策略在过去一段时间内的表现情况,为实际交易提供有力的决策支持。
作为新手,我们决定先使用现成的回测框架,因为现成的框架通常具有完善的文档和示例代码,我们可以快速了解和学习如何使用它们。如果后期发现现成的框架不能满足我们特有需求时候,我们可以选择编写自己的回测框架。
当然,现在学习现成回测框架所投入的时间并不会打水漂,对于我们搭建自己的回测框架也是有帮助的。
我们选择Backtrader作为我们的回测框架。backtrader属于功能相对完善的本地版Python量化回测框架。
首先,在Python环境中安装Backtrader库:
pip install backtrader
然后,在Python脚本中导入Backtrader库:
import backtrader as bt
接下来,我们用双均线策略来熟悉下BackTrader的使用。
单个股策略回测
定义数据加载函数:使用ak.fund_etf_hist_min_em函数从AKShare获取ETF的分钟级别历史数据,并将其转换为BackTrader可以使用的格式。
def GetStockDatApi(code, start='20240425', end='20240426'):
df = ak.fund_etf_hist_min_em(symbol=code, start_date=start, end_date=end)
# 将交易日期设置为索引值
df.index = pd.to_datetime(df["时间"])
df.sort_index(inplace=True)
df.drop(axis=1, columns='时间', inplace=True)
recon_data = {'High': df.最高, 'Low': df.最低, 'Open': df.开盘, 'Close': df.收盘, \
'Volume': df.成交量}
df_recon = pd.DataFrame(recon_data)
return df_recon
backtrader的数据加载非常灵活,提供了多种形式的数据接口,可以是CSV文件格式的数据,也可以是DataFrame格式数据。此处我们使用DataFrame格式数据,如下所示:
High Low Open Close Volume OpenInterest
时间
2024-04-25 09:35:00 3.51 3.50 3.50 3.50 321378 0
2024-04-25 09:40:00 3.50 3.50 3.50 3.50 268571 0
2024-04-25 09:45:00 3.50 3.50 3.50 3.50 187412 0
2024-04-25 09:50:00 3.50 3.50 3.50 3.50 208152 0
2024-04-25 09:55:00 3.50 3.50 3.50 3.50 153200 0
定义双均线策略:构建策略的类是继承backtrader.Strategy,然后根据自己的需要重写其中的方法。比如__init__、log、notify_order、notify_trade、next等等。
我们创建一个继承自bt.Strategy的类,在里面定义双均线策略。
关于策略中的指标,backtrader内置了很多类型,直接调用即可。比如移动平均线:
self.sma = bt.indicators.SimpleMovingAverage(
self.datas[0], period=self.params.maperiod)
next方法中,我们实现一个简单的双均线策略作为交易的逻辑。比如买入条件是MA5上穿MA10(5日收盘价移动平均线和10日收盘价移动平均线形成均线金叉);卖出条件是MA10下穿MA5(5日收盘价移动平均线和10日收盘价移动平均线形成均线死叉)。
class dua_ma_strategy(bt.Strategy):
# 全局设定交易策略的参数
params=(
('ma_short',30),
('ma_long', 60),
)
def __init__(self):
# 指定价格序列
self.dataclose=self.datas[0].close
# 初始化交易指令、买卖价格和手续费
self.order = None
self.buyprice = None
self.buycomm = None
# 添加移动均线指标
# 5日移动平均线
self.sma5 = bt.indicators.SimpleMovingAverage(
self.datas[0], period=self.params.ma_short)
# 10日移动平均线
self.sma10 = bt.indicators.SimpleMovingAverage(
self.datas[0], period=self.params.ma_long)
self.rsi5 = bt.indicators.RSI_EMA(self.datas[0], period=self.params.ma_short,subplot=True)
self.william_ad = bt.indicators.WilliamsAD(self.datas[0], subplot=True)
def log(self, txt, dt=None, doprint=False):
# 日志函数,用于统一输出日志格式
if doprint:
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def notify_order(self, order):
"""
订单状态处理
Arguments:
order {object} -- 订单状态
"""
if order.status in [order.Submitted, order.Accepted]:
# 如订单已被处理,则不用做任何事情
return
# 检查订单是否完成
if order.status in [order.Completed]:
if order.isbuy():
self.buyprice = order.executed.price
self.buycomm = order.executed.comm
self.log(f'买入:\n价格:{order.executed.price},\
成本:{order.executed.value},\
手续费:{order.executed.comm}', doprint=True)
else:
self.log(f'卖出:\n价格:{order.executed.price},\
成本: {order.executed.value},\
手续费{order.executed.comm}', doprint=True)
self.bar_executed = len(self)
# 订单因为缺少资金之类的原因被拒绝执行
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
self.log('Order Canceled/Margin/Rejected')
# 订单状态处理完成,设为空
self.order = None
# 记录交易收益情况
def notify_trade(self, trade):
"""
交易成果
Arguments:
trade {object} -- 交易状态
"""
if not trade.isclosed:
return
# 显示交易的毛利率和净利润
self.log('Operation Profit, Gross%.2f, Net %.2f' %
(trade.pnl, trade.pnlcomm), doprint=True)
def next(self):
# 记录收盘价
self.log('Close, %.2f' % self.dataclose[0])
# Access -1, because drawdown[0] will be calculated after "next"
#self.log('DrawDown: %.2f' % self.stats.drawdown.drawdown[-1], doprint=True)
#self.log('MaxDrawDown: %.2f' % self.stats.drawdown.maxdrawdown[-1], doprint=True)
if self.order: # 是否有指令等待执行
return
# 是否持仓
if not self.position: # 没有持仓
# 执行买入条件判断:MA5上扬突破MA10,买入
if self.sma5[0] > self.sma10[0]:
self.order = self.buy() # 执行买入
self.log('Buy Create, %.2f' % self.dataclose[0], doprint = True)
else:
# 执行卖出条件判断:MA5下穿跌破MA10,卖出
if self.sma5[0] < self.sma10[0]:
self.order = self.sell() # 执行卖出
self.log('Sell Create, %.2f' % self.dataclose[0], doprint = True)
# 回测结束后输出
def stop(self):
self.log(u'金叉死叉策略 %2d Ending Value %.2f' %
(self.params.ma_long, self.broker.getvalue()), doprint=True)
log方法用于打印日志,包含一个使能标志位,可以选择是否打印出日期时间以及txt变量传递值。
notify_order方法用于跟踪交易指令(order)的状态。order具有提交,接受,买入/卖出执行和价格,已取消/拒绝等状态。
notify_trade方法用于跟踪交易的状态,任何已平仓的交易都将报告毛利和净利润。
设置Cerebro引擎:关于策略回测,把数据和策略添加到Cerebro中之外,还有设置一些参数。比如broker的设置,像初始资金、交易佣金,也可以用addsizer设定每次交易买入的股数。
此处,我们创建一个Cerebro对象,并加载ETF数据、添加策略和设置佣金、滑点等参数。
cerebro = bt.Cerebro() # 初始化cerebro回测系统设置 默认 stdstats=True
cerebro.adddata(data) # 将数据传入回测系统
cerebro.addstrategy(dua_ma_strategy) # 将交易策略加载到回测系统中
cerebro.broker.setcash(10000) # 设置初始资本为10,000
cerebro.addsizer(bt.sizers.FixedSize, stake=500) # 设定每次交易买入的股数
cerebro.broker.setcommission(commission=0.002) # 设置交易手续费为 0.2%
cerebro.addobserver(bt.observers.DrawDown)
print('初始资金: %.2f' % cerebro.broker.getvalue())
运行回测:调用Cerebro对象的run()方法来执行回测。
results = cerebro.run() #运行回测系统
回测结束后返回得到执行交易策略时积累的总资金。
初始资金: 10000.00
2024-04-26, Buy Create,
3.54
2024-04-26, 买入:
价格:3.545, 成本:1772.5, 手续费:3.545
2024-04-30, Sell Create,
3.62
2024-04-30, 卖出:
价格:3.617, 成本: 1772.5, 手续费3.617
2024-04-30, Operation
Profit, Gross36.00, Net 28.84
......
2024-05-10, Buy Create,
3.66
2024-05-10, 买入:
价格:3.658, 成本:1829.0, 手续费:3.658
2024-05-10, 金叉死叉策略 60 Ending Value
9980.52
最终资金: 9980.52
此处我们回测的是沪深300ETF(510300)2024年4月25日到2024年5月10日期间的策略执行效果,最终资金从10000变成了9980.52。
由于backtrader内置了Matplotlib,因此我们也可以可视化回测的效果,如下所示:
Backtrader可视化回测结果后会产生包含了三个子图的图表上,如图所示:由上往下分别为资金变动图、交易盈亏图、买卖区间图。
批量股策略回测
当要对A股市场全部ETF执行批量回测时,只需把单只ETF的回测封装好,循环调用即可。比如我们要回测"159998", "159997", "159996", "159995", "159994"这几只ETF,那就循环调用单只ETF回测的全部步骤即可。
code_names = ["159998", "159997", "159996", "159995", "159994"]
# 构建一个空的字典用来装数据
result = {}
for name, code in code_names:
feed_pandas = GetStockDatApi(code, '20240101', '20240510')
feed_pandas['OpenInterest'] = 0
# 加载回测期间的数据
data = bt.feeds.PandasData(dataname=feed_pandas, fromdate=datetime.datetime(2024, 1, 1, 9, 35),
todate=datetime.datetime(2024, 5, 10, 15, 0))
回测的结果转换成收益率,用字典存储。有了这个数据集合之后,我们就可以去统计这个策略下有多少ETF上涨、多少ETF下跌、多少ETF涨幅大于10%、多少ETF跌幅大于10%等等。如下所示:
159998 回测
初始资金: 10000.00
最终资金: 9900.3
159997 回测
初始资金: 10000.00
最终资金: 9894.39
159996 回测
初始资金: 10000.00
最终资金: 9945.54
159995 回测
初始资金: 10000.00
最终资金: 9907.6
159994 回测
初始资金: 10000.00
最终资金: 9893.24
{'159998': -0.01,
'159997': -0.01, '159996': -0.01, '159995': -0.01, '159994': -0.01}
最高收益的股票:159998, 达到 -0.01
正收益数量: 0, 负收益数量:5
+10%数量: 0, -10%数量:0
收益10%以上的股票: {}
以上就是量化系统中经典的回测框架Backtrader的入门使用方法,后续随着搭建量化系统设计策略的深入,我们会频繁地调用到这部分内容,大家一定要熟悉。
说明
此系列为连载专栏,完整代码会上传知识星球《玩转股票量化交易》!作为会员们的学习资料。
想要加入知识星球《玩转股票量化交易》的小伙伴记得先微信call我获取福利!
非星球会员需要的话,需要单独联系我购买!
知识星球介绍点击:知识星球《玩转股票量化交易》精华内容概览