实现双均线回测(附python代码)
发布时间:2023-05-25 07:22:42
回测的意思是用历史的价格数据来模拟你的交易策略,看看你在过去的时间里如果按照你的买卖规则操作,最后会赚还是会亏。回测实际上很复杂,需要考虑很多因素,比如交易成本,税收,价格波动,股票停牌等,但我们先从简单的开始,只关注价格。
均线可以帮助我们平滑价格的波动,识别出价格的走势,很多有效的策略其实就是基于均线的变化。
最常见的双均线系统就是当短期均线从下往上穿过长期均线的时候买入,从上往下穿过长期均线的时候卖出。这个有一定的金融学逻辑,长期均线反映了更多人的平均持仓成本,当价格跌破这个水平的时候有部分人更倾向于卖出。
今天我们用python复现这个策略。代码附上,使用的是20天均线和50天均线。
代码如下:
import baostock as bs # 导入baostock模块
import pandas as pd # 导入pandas模块
#### 登陆系统 ####
lg = bs.login() # 登陆系统
#### 获取沪深A股历史K线数据 ####
# 周月线指标:date,code,open,high,low,close,volume,amount,adjustflag,turn,pctChg
rs = bs.query_history_k_data_plus("sh.000300", "date,code,open,high,low,close,preclose,volume,amount,adjustflag,turn,tradestatus,pctChg,isST", start_date='2020-01-01', end_date='2023-03-15', frequency="d", adjustflag="3") # 获取沪深300指数的历史K线数据
#### 打印结果集 ####
data_list = []
while (rs.error_code == '0') & rs.next(): # 循环获取数据并将记录合并在一起
data_list.append(rs.get_row_data())
result = pd.DataFrame(data_list, columns=rs.fields) # 将记录合并在一起,并转换为DataFrame格式
#### 结果集输出到csv文件 ####
result.to_csv("300.csv", index=False) # 将结果输出到csv文件中,不包含索引列
#### 登出系统 ####
bs.logout() # 登出系统
# 导入 backtrader 模块,用于构建回测系统
import backtrader as bt
# 导入 datetime 模块,用于处理日期和时间
import datetime
# 定义一个函数,用于将数值按照指定的单位向下取整
def downcast(amount, lot):
return abs(amount // lot * lot)
# 定义一个类,继承自 bt.Strategy,用于实现自己的策略逻辑
class TestStrategy(bt.Strategy):
"""
构建自己的策略
"""
# 定义一个日志函数,用于统一输出日志格式
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 __init__(self):
# 初始化相关数据
# 获取第一个数据源(默认为收盘价)的引用
self.dataclose = self.datas[0].close
# 设置初始订单为空
self.order = None
# 设置初始买入价格为空
self.buyprice = None
# 设置初始买入佣金为空
self.buycomm = None
# 五日移动平均线
# 使用 bt.indicators.SimpleMovingAverage 模块计算第一个数据源的 18 周期(天)的简单移动平均线,并赋值给 self.sma5
self.sma5 = bt.indicators.SimpleMovingAverage(
self.datas[0], period=20)
# 十日移动平均线
# 使用 bt.indicators.SimpleMovingAverage 模块计算第一个数据源的 47 周期(天)的简单移动平均线,并赋值给 self.sma10
self.sma10 = bt.indicators.SimpleMovingAverage(
self.datas[0], period=50)
# 定义一个通知订单状态的函数,用于处理订单的各种状态变化
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.bar_executed = len(self)
# 如果是卖出订单,则不需要记录任何信息
# 打印出订单执行的信息,包括价格、数量、佣金等
self.log(
'执行,%s, 价格: %.2f, 数量: %d, 佣金: %.2f' %
(order.info['name'],
order.executed.price,
order.executed.size,
order.executed.comm))
# 订单因为缺少资金之类的原因被取消或被拒绝执行
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
# 打印出订单取消或被拒绝的信息
self.log('订单取消或被拒绝', doprint=True)
# 订单状态处理完成,设为空,以便下次下单时不会冲突
self.order = None
# 定义一个通知交易结果的函数,用于处理交易的盈亏情况
def notify_trade(self, trade):
"""
交易成果
Arguments:
trade {object} -- 交易状态
"""
# 如果交易还没有结束(即还没有平仓),则不做任何事情,直接返回
if not trade.isclosed:
return
# 显示交易的毛利率和净利润(扣除佣金后)
self.log('交易中,毛利%.2f, 净利 %.2f' %
(trade.pnl, trade.pnlcomm), doprint=True)
# 定义一个执行策略逻辑的函数,用于每个周期(天)执行一次
def next(self):
''' 下一次执行 '''
# 记录收盘价到日志中(不打印)
self.log('Close, %.2f' % self.dataclose[0])
# 是否正在下单,如果是的话不能提交第二次订单,直接返回
if self.order:
return
# 是否已经买入(持有仓位)
if not self.position:
# 还没买,如果 MA5 > MA10 说明涨势,买入
if self.sma5[0] > self.sma10[0]:
# 计算可以买入的数量(按照账户价值的98%和当前价格向下取整到100股)
order_value = self.broker.getvalue() * 0.98
order_amount = downcast(order_value / self.datas[0].close[0], 100)
# 下达买入订单,并记录到 self.order 中,并指定数据源名称为订单信息之一(方便后续打印)
self.order = self.buy(self.datas[0], size=order_amount, name=self.datas[0]._name)
# 打印出买入信息(包括价格)
self.log('买入, %.2f' % self.dataclose[0], doprint=True)
else:
# 已经买了,如果 MA5 < MA10 ,说明跌势,卖出
if self.sma5[0] < self.sma10[0]:
# 下达卖出订单,并记录到 self.order 中,并指定数据源名称为订单信息之一(方便后续打印)
# 卖出数量为当前持有数量的百分比(这里设为0%,即全部卖出)
self.order = self.order_target_percent(self.datas[0], 0, name=self.datas[0]._name)
# 打印出卖出信息(包括价格和百分比)
self.log(f"卖{self.datas[0]._name}, price:{self.datas[0].close[0]:.2f}, pct: 0")
# 定义一个停止策略执行的函数,用于在策略结束时打印最终账户资产情况
def stop(self):
self.log(u'最终的账户资产为%.2f' %
(self.broker.getvalue()), doprint=True)
# 如果是直接运行本文件,则执行以下代码:
if __name__ == '__main__':
# 初始化模型对象 cerebro ,使用 bt.Cerebro 类创建实例
cerebro = bt.Cerebro()
# 构建策略对象 strats ,使用 cerebro.addstrategy 方法添加 TestStrategy 类作为策略
strats = cerebro.addstrategy(TestStrategy)
# 设置每次买入数量为固定值(这里设为0.1股),使用 cerebro.addsizer 方法添加 bt.sizers.FixedSize 类作为仓位控制器
cerebro.addsizer(bt.sizers.FixedSize, stake=0.1)
# 加载数据到模型中,使用 bt.feeds.GenericCSVData 类创建数据对象 data ,并指定数据文件名、起止日期、日期格式、各列对应的数据类型等参数
data = bt.feeds.GenericCSVData(
dataname='300.csv',
fromdate=datetime.datetime(2020, 1, 1),
todate=datetime.datetime(2023,5, 24),
dtformat='%Y-%m-%d',
datetime=0,
open=2,
high=3,
low=4,
close=5,
volume=7
)
# 使用 cerebro.adddata 方法将数据对象 data 添加到模型中
cerebro.adddata(data)
# 设定初始资金和佣金,使用 cerebro.broker.setcash 和 cerebro.broker.setcommission 方法分别设置账户初始现金为2000000.0元和每笔交易的佣金为0.005(即0.5%)
cerebro.broker.setcash(2000000.0)
cerebro.broker.setcommission(0.005)
# 策略执行前的资金,使用 cerebro.broker.getvalue 方法获取账户价值,并打印出来
print('启动资金: %.2f' % cerebro.broker.getvalue())
# 策略执行,使用 cerebro.run 方法运行策略逻辑
cerebro.run()
# 定义一些颜色值,用于绘制图表时的样式
colors = ['#729ece', '#ff9e4a', '#67bf5c', '#ed665d', '#ad8bc9', '#a8786e', '#ed97ca', '#a2a2a2', '#cdcc5d',
'#6dccda']
tab10_index = [3, 0, 2, 1, 2, 4, 5, 6, 7, 8, 9]
# 绘制图表,使用 cerebro.plot 方法生成可视化结果,并指定一些参数,如:
# iplot=False 表示不使用 ipython notebook 的内嵌模式
# style='line' 表示绘制线型价格走势,可改为 'candel' 样式
# lcolors=colors 表示使用自定义的颜色列表
# plotdist=0.1 表示图表之间的间距为0.1
# bartrans=0.2 表示柱状图的透明度为0.2
# volup='#ff9896' 表示成交量上涨的颜色为红色
# voldown='#98df8a' 表示成交量下跌的颜色为绿色
# loc='#5f5a41' 表示图例的颜色为深灰色
# grid=False 表示删除水平网格线
cerebro.plot(iplot=False,
style='line',
lcolors=colors,
plotdist=0.1,
bartrans=0.2,
volup='#ff9896',
voldown='#98df8a',
loc='#5f5a41',
grid=False)
下一篇:应对价格波动风险和真实风险的方法