前言:
先放效果图:
这次更新主要是增加了多股跟全市股票回测,策略方面还是跟上篇文章的一样,21日跟55日均线相交的策略。多股方面考虑到了均线的周期跟实际股票的数据数量是否适合问题,例如假设55日的均线就必须有55根日K线才能计算出55日均线,但是当实际多股回测时回测到没有55日均线的新股票就会报错,于是采用了try…except…语句处理。还有一种情况就是单股在绘图时遇到一次也没有交易的情况或只有一个买的信号时绘图出现错误,也是用的try…except…语句处理。关于的backtrader的细节方面还有很多没有考虑到,后面遇到再慢慢解决,一个人自学搞开发真的累,中途找不到解决办法跟资料时真的有想过放弃,后来想想还是再努力下吧,就当是业余爱好也好,锻炼锻炼下脑子,或许比只玩游戏好,说不定解决了一个问题我就能LV1 UP一次,哈哈哈。
当多股回测股票代码输入框没有输入代码只输入时间跟日期就是全市股票回测,估计要3个小时左右才能测完,我的电脑回测了很久,这个问题后面还是得想办法解决,像是增加筛选条件之类的吧。还有多股输入代码那个逗号时小写的, 不是大写的,输入时请注意。
这次只修改了stock_backtrader.py,其他的没文件没修改,其他的文件代码请到上一版本复制下。
代码如下:
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import datetime
import pandas as pd
import backtrader as bt
import tushare as ts
import tk_window
import tkinter as tk
from tkinter import ttk
import matplotlib.pyplot as plt
import mplfinance as mpf
import matplotlib as mpl # 用于设置曲线参数
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk) # 使用后端TkAgg
# pd.set_option()就是pycharm输出控制显示的设置
pd.set_option('expand_frame_repr', False) # True就是可以换行显示。设置成False的时候不允许换行
pd.set_option('display.max_columns', None) # 显示所有列
# pd.set_option('display.max_rows', None) # 显示所有行
pd.set_option('colheader_justify', 'centre') # 显示居中
pro = ts.pro_api('数据用的是tushare,没权限自己去注册个吧')
class my_strategy(bt.Strategy):
# 设置简单均线周期,以备后面调用
params = (
('maperiod21', 21),
('maperiod55', 55),)
def log(self, txt, dt=None):
# 日记记录输出
dt = dt or self.datas[0].datetime.date(0)
print('%s, %s' % (dt.isoformat(), txt))
def __init__(self):
# 初始化数据参数
# 设置当前收盘价为dataclose
self.dataclose = self.datas[0].close
self.order = None
self.buyprice = None
self.buycomm = None
# 添加简单均线, subplot=False是否单独子图显示
self.sma21 = bt.indicators.SimpleMovingAverage(self.datas[0], period=self.params.maperiod21, plotname='mysma')
self.sma55 = bt.indicators.SimpleMovingAverage(self.datas[0], period=self.params.maperiod55, subplot=False)
def next(self):
# self.log('Close, %.2f' % self.dataclose[0]) # 输出打印收盘价
# self.log('持仓 %.2f' % self.position.size) # 输出持仓
# 检查是否有订单发送当中,如果有则不再发送第二个订单
if self.order:
return
# 检查是否已经有仓位
if not self.position:
# 如果没有则可以执行一下策略了
if self.sma21[0] > self.sma55[0] and self.sma21[-1] < self.sma55[-1]:
# 记录输出买入价格
# self.log('买入信号产生的价格: %.2f' % self.dataclose[0])
# 跟踪已经创建好的订单避免重复第二次交易
self.order = self.buy()
else:
if self.sma21[0] < self.sma55[0] and self.sma21[-1] > self.sma55[-1]:
# self.log('卖入信号产生的价格: %.2f' % self.dataclose[0])
self.order = self.sell()
# 记录交易执行情况,输出打印
def notify_order(self, order):
if order.status in [order.Submitted, order.Accepted]:
# 如果有订单提交或者已经接受的订单,返回退出
return
# 主要是检查有没有成交的订单,如果有则日志记录输出价格,金额,手续费。注意,如果资金不足是不会成交订单的
if order.status in [order.Completed]:
# if order.isbuy():
# self.log(
# '实际买入价格: %.2f, 市值: %.2f, 手续费 %.2f' %
# (order.executed.price,
# order.executed.value,
# order.executed.comm))
#
# self.buyprice = order.executed.price
# self.buycomm = order.executed.comm
# else: # Sell
# self.log('实际卖出价格: %.2f, 市值: %.2f, 手续费 %.2f' %
# (order.executed.price,
# order.executed.value,
# order.executed.comm))
# len(self)是指获取截至当前数据一共有多少根bar
# 以下代码就是指当交易发生时立刻记录下了当天有多少根bar
# 如果要表示当成交后过了5天卖,则可以这样写 if len(self) >= (self.bar_executed + 5):
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):
# if not trade.isclosed: # 如果交易还没有关闭,则退出不输出显示盈利跟手续费
# return
# self.log('策略收益 %.2f, 成本 %.2f' %
# (trade.pnl, trade.pnlcomm))
def stop(self):
# 策略停止输出结果
total_funds = self.broker.getvalue()
# print('MA均线: %2d日,总资金: %.2f' % (self.params.maperiod21, total_funds))
def run_cerebro(): # 策略回测
for widget_backtrader_window in tk_window.centre_frame.winfo_children():
widget_backtrader_window.destroy()
backtrader_window = tk.PanedWindow(tk_window.centre_frame, opaqueresize=False)
backtrader_window.pack(fill=tk.BOTH, expand=1)
# 创建左边frame框架,主要放回测策略文件代码(暂未开发,占位而已)
backtrader_left_frame = tk.Frame(backtrader_window, relief=tk.SUNKEN, bg='#353535', bd=5, borderwidth=4)
backtrader_left_frame.pack(fill=tk.BOTH, expand=1)
top_label = tk.Label(backtrader_left_frame, text='回测策略文件', bd=1)
top_label.pack()
backtrader_window.add(backtrader_left_frame)
# 创建右边图形输出框架,主要放回测分析显示跟用户输入的股票代码跟日期
backtrader_plot_window = tk.PanedWindow(orient='vertical', opaqueresize=False)
backtrader_window.add(backtrader_plot_window)
backtrader_plot_window_top = tk.PanedWindow(opaqueresize=False)
backtrader_plot_window.add(backtrader_plot_window_top)
# ******************************************************************************************************************
backtrader_top_left_frame = tk.Frame(backtrader_plot_window_top, width=tk_window.screenWidth,
height=tk_window.screenHeight, relief=tk.SUNKEN, bg='#353535', bd=5,
borderwidth=4)
backtrader_top_left_frame.pack(fill=tk.BOTH)
# 在主框架下创建股票代码输入子框架
code_frame = tk.Frame(backtrader_top_left_frame, borderwidth=1, bg='#353535')
code_frame.pack()
# 创建标签‘股票代码’
stock_label = tk.Label(code_frame, text='单股回测股票代码', bd=1)
stock_label.pack(side=tk.LEFT)
# 创建股票代码输入框
input_code_var = tk.StringVar()
code_widget = tk.Entry(code_frame, textvariable=input_code_var, borderwidth=1, justify=tk.CENTER)
# input_code_get = input_code_var.set(input_code_var.get()) # 获取输入的新值
code_widget.pack(side=tk.LEFT, padx=4)
# 在主框架下创建股票日期输入框子框架
input_date_frame = tk.Frame(backtrader_top_left_frame, borderwidth=1, bg='#353535')
input_date_frame.pack()
# 创建标签‘开始日期’
date_start_label = tk.Label(input_date_frame, text='开始日期', bd=1)
date_start_label.pack(side=tk.LEFT)
# 创建开始日期代码输入框
input_startdate_var = tk.StringVar()
startdate_widget = tk.Entry(input_date_frame, textvariable=input_startdate_var, borderwidth=1, justify=tk.CENTER)
input_startdate_get = input_startdate_var.set(input_startdate_var.get()) # 获取输入的新值
startdate_widget.pack(side=tk.LEFT, padx=4)
# 创建标签‘结束日期’
date_end_label = tk.Label(input_date_frame, text='结束日期', bd=1)
date_end_label.pack(side=tk.LEFT)
# 创建结束日期代码输入框
input_enddate_var = tk.StringVar()
enddate_widget = tk.Entry(input_date_frame, textvariable=input_enddate_var, borderwidth=1, justify=tk.CENTER)
input_enddate_get = input_enddate_var.set(input_enddate_var.get()) # 获取输入的新值
enddate_widget.pack(side=tk.LEFT, padx=4)
# 先把部件布局好了再backtrader_top_frame用.add()添加到backtrader_plot_window
backtrader_plot_window_top.add(backtrader_top_left_frame, height=tk_window.screenHeight / 10,
width=tk_window.screenHeight / 1.4)
# ******************************************************************************************************************
backtrader_top_right_frame = tk.Frame(backtrader_plot_window_top, width=tk_window.screenWidth,
height=tk_window.screenHeight, relief=tk.SUNKEN, bg='#353535', bd=5,
borderwidth=4)
backtrader_top_right_frame.pack(fill=tk.BOTH)
# 在主框架右边创建股票代码输入子框架
multi_code_frame = tk.Frame(backtrader_top_right_frame, borderwidth=1, bg='#353535')
multi_code_frame.pack()
# 创建标签‘股票代码’
multi_stock_label = tk.Label(multi_code_frame, text='多股回测股票代码', bd=1)
multi_stock_label.pack(side=tk.LEFT)
# 创建股票代码输入框
input_multi_code_var = tk.StringVar()
multi_code_widget = tk.Entry(multi_code_frame, textvariable=input_multi_code_var, borderwidth=1, justify=tk.CENTER)
# input_code_get = input_code_var.set(input_code_var.get()) # 获取输入的新值
multi_code_widget.pack(side=tk.LEFT, padx=4)
# 在主框架下创建股票日期输入框子框架
multi_input_date_frame = tk.Frame(backtrader_top_right_frame, borderwidth=1, bg='#353535')
multi_input_date_frame.pack()
# 创建标签‘开始日期’
multi_date_start_label = tk.Label(multi_input_date_frame, text='开始日期', bd=1)
multi_date_start_label.pack(side=tk.LEFT)
# 创建开始日期代码输入框
multi_input_startdate_var = tk.StringVar()
multi_startdate_widget = tk.Entry(multi_input_date_frame, textvariable=multi_input_startdate_var, borderwidth=1,
justify=tk.CENTER)
input_startdate_get = multi_input_startdate_var.set(multi_input_startdate_var.get()) # 获取输入的新值
multi_startdate_widget.pack(side=tk.LEFT, padx=4)
# 创建标签‘结束日期’
multi_date_end_label = tk.Label(multi_input_date_frame, text='结束日期', bd=1)
multi_date_end_label.pack(side=tk.LEFT)
# 创建结束日期代码输入框
multi_input_enddate_var = tk.StringVar()
multi_enddate_widget = tk.Entry(multi_input_date_frame, textvariable=multi_input_enddate_var, borderwidth=1,
justify=tk.CENTER)
multi_input_enddate_get = multi_input_enddate_var.set(multi_input_enddate_var.get()) # 获取输入的新值
multi_enddate_widget.pack(side=tk.LEFT, padx=4)
# 先把部件布局好了再backtrader_top_frame用.add()添加到backtrader_plot_window
backtrader_plot_window_top.add(backtrader_top_right_frame, height=tk_window.screenHeight / 10,
width=tk_window.screenWidth / 2)
# ******************************************************************************************************************
backtrader_plot_window_bottom = tk.PanedWindow(opaqueresize=False)
backtrader_plot_window.add(backtrader_plot_window_bottom)
# 创建底部窗口框架,用来放图形输出
backtrader_bottom_frame = tk.Frame(backtrader_plot_window_bottom, width=tk_window.screenWidth,
height=tk_window.screenHeight, relief=tk.SUNKEN, bg='#353535', bd=5,
borderwidth=4)
backtrader_bottom_frame.pack(fill=tk.BOTH)
backtrader_plot_window_bottom.add(backtrader_bottom_frame)
# ******************************************************************************************************************
def mplfinance_go(): # 图形输出渲染
# 在backtrader_bottom_frame的原有基础上再创建一个框架,目的方便在更新股票股票回测时防止图形重叠
for widget_backtrader_bottom_frame in backtrader_bottom_frame.winfo_children():
widget_backtrader_bottom_frame.destroy()
# 创建左右两个frame框架方便管理布局大小跟刷新,framed大小跟控件的长高有关
backtrader_bottomleft_frame = tk.Frame(backtrader_bottom_frame, bg='#353535', bd=5, borderwidth=4)
backtrader_bottomleft_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
backtrader_bottomright_frame = tk.Frame(backtrader_bottom_frame, bg='#353535', bd=5, borderwidth=4)
backtrader_bottomright_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=0)
# 以下函数作用是省略输入代码后缀.sz .sh
def code_name_transform(get_stockcode): # 输入的数字股票代码转换成字符串股票代码
str_stockcode = str(get_stockcode)
str_stockcode = str_stockcode.strip() # 删除前后空格字符
if 6 > len(str_stockcode) > 0:
str_stockcode = str_stockcode.zfill(6) + '.SZ' # zfill()函数返回指定长度的字符串,原字符串右对齐,前面填充0
if len(str_stockcode) == 6:
if str_stockcode[0:1] == '0':
str_stockcode = str_stockcode + '.SZ'
if str_stockcode[0:1] == '3':
str_stockcode = str_stockcode + '.SZ'
if str_stockcode[0:1] == '6':
str_stockcode = str_stockcode + '.SH'
return str_stockcode
# 交互数据的获取跟处理
stock_name = input_code_var.get()
code_name = code_name_transform(stock_name)
start_date = input_startdate_var.get()
end_date = input_enddate_var.get()
try:
# adj='qfq'向前复权,freq='D 数据频度:日K线
df = ts.pro_bar(ts_code=code_name, start_date=start_date, end_date=end_date, adj='qfq', freq='D')
df['trade_date'] = pd.to_datetime(df['trade_date'])
# df = df.drop(['change', 'pre_close', 'pct_chg', 'amount'], axis=1)
# 设置用于backtrader的数据
df_back = df.rename(columns={'vol': 'volume'})
df_back.set_index('trade_date', inplace=True) # 设置索引覆盖原来的数据
df_back = df_back.sort_index(ascending=True) # 将时间顺序升序,符合时间序列
df_back['openinterest'] = 0
# 喂养数据到backtrader当中去
back_start_time = datetime.datetime.strptime(start_date, "%Y%m%d") # str转换成时间格式2015-01-01 00:00:00
back_end_time = datetime.datetime.strptime(end_date, '%Y%m%d')
# print(back_start_time)
data_back = bt.feeds.PandasData(dataname=df_back,
fromdate=back_start_time,
todate=back_end_time)
# 设置用于mpf图形的数据
data_mpf = df.loc[:, ['trade_date', 'open', 'close', 'high', 'low', 'vol']] # :取所有行数据,后面取date列,open列等数据
data_mpf = data_mpf.rename(
columns={'trade_date': 'Date', 'open': 'Open', 'close': 'Close', 'high': 'High', 'low': 'Low',
'vol': 'Volume'}) # 更换列名,为后面函数变量做准备
data_mpf.set_index('Date', inplace=True) # 设置date列为索引,覆盖原来索引,这个时候索引还是 object 类型,就是字符串类型。
# 将object类型转化成 DateIndex 类型,pd.DatetimeIndex 是把某一列进行转换,同时把该列的数据设置为索引 index。
data_mpf.index = pd.DatetimeIndex(data_mpf.index)
data_mpf = data_mpf.sort_index(ascending=True) # 将时间顺序升序,符合时间序列
# print(data_mpf)
# 创建策略容器
cerebro = bt.Cerebro()
# 添加自定义的策略my_strategy
cerebro.addstrategy(my_strategy)
# 添加数据
cerebro.adddata(data_back)
# 设置资金
startcash = 100000
cerebro.broker.setcash(startcash)
# 设置每笔交易交易的股票数量
cerebro.addsizer(bt.sizers.FixedSize, stake=100)
# 设置手续费
cerebro.broker.setcommission(commission=0.0005)
# 输出初始资金
d1 = back_start_time.strftime('%Y%m%d')
d2 = back_end_time.strftime('%Y%m%d')
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='SharpeRatio')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='DW')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='TradeAnalyzer')
cerebro.addanalyzer(bt.analyzers.Transactions, _name='Transactions')
# 运行策略
# stdstats=False不显示回测的统计结果
result = cerebro.run(stdstats=True, optreturn=False)
backtrader_analysis = result[0]
# print(backtrader_analysis.analyzers.SharpeRatio.get_analysis())
# print(backtrader_analysis.analyzers.DW.get_analysis())
# print(backtrader_analysis.analyzers.TradeAnalyzer.get_analysis())
# 在下面的占位符后面不能有空格,否则空格后面的输入信息是输不进treeview的单元格
startcash_value = '初始资金:%.2f' % startcash
endcash_value = '期末资金:%.2f' % cerebro.broker.getvalue()
try:
completed_net = '已完成盈亏:%.2f' % backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['pnl']['net']['total']
except Exception as e0:
completed_net = '已完成盈亏:%s' % None
try:
float_profit = '浮动盈亏:%.2f' % (cerebro.broker.getvalue() - startcash -
backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['pnl']['net']['total'])
except Exception as e1:
float_profit = '浮动盈亏:%s' % None
try:
completed_commission = '手续费用:%.2f' % (
backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['pnl']['gross']['total'] -
backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['pnl']['net']['total'])
except Exception as e2:
completed_commission = '手续费用:%s' % None
start_backtrade_date = '回测开始时间:%s' % d1
end_backtrade_date = '回测结束时间:%s' % d2
try:
sharpeRatio_value = '夏普比例:%.2f' % backtrader_analysis.analyzers.SharpeRatio.get_analysis()['sharperatio']
except Exception as e3:
sharpeRatio_value = '夏普比例:%s' % None
try:
drawdown_value = '最大回撤:%.2f' % backtrader_analysis.analyzers.DW.get_analysis()['max']['drawdown']
except Exception as e4:
drawdown_value = '最大回撤:%s' % None
try:
moneydown_value = '最大资金回撤:%.2f' % backtrader_analysis.analyzers.DW.get_analysis()['max']['moneydown']
except Exception as e5:
moneydown_value = '最大资金回撤:%s' % None
try:
max_drawdown_lastday = '最大回撤持续天数:%d' % backtrader_analysis.analyzers.DW.get_analysis()['max']['len']
except Exception as e6:
max_drawdown_lastday = '最大回撤持续天数:%s' % None
try:
total_value = '交易次数:%d' % backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['total']['total']
except Exception as e7:
total_value = '交易次数:%s' % None
try:
uncompleted_trade = '未完成交易数量:%d' % backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['total']
['open']
except Exception as e8:
uncompleted_trade = '未完成交易数量:%s' % None
try:
completed_trade = '已完成交易数量:%d' % backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['total']
['closed']
except Exception as e9:
completed_trade = '已完成交易数量:%s' % None
try:
win_value = '盈利次数:%d' % backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['won']['total']
except Exception as e10:
win_value = '盈利次数:%s' % None
try:
lost_value = '亏损次数:%d' % backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['lost']['total']
except Exception as e11:
lost_value = '亏损次数:%s' % None
try:
win_rate = '胜率:%.2f' % (backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['won']['total'] /
backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['total']['total'])
except Exception as e12:
win_rate = '胜率:%s' % None
try:
lost_rate = '败率:%.2f' % (backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['lost']['total'] /
backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['total']['total'])
except Exception as e13:
lost_rate = '败率:%s' % None
analysis_log = [] # 设置空列表用来接收回测记录
history_trade_buy_date_list = [] # 设置空列表用来接收买点标记的时间日期,下面的空列表都是为标记做准备
history_trade_sell_date_list = []
history_trade_buy_vol_list = []
history_trade_sell_vol_list = []
history_trade_buy_price_list = []
history_trade_sell_price_list = []
trade_signal_buy = pd.DataFrame(columns=['Date', 'buy_price', 'buy_vol']) # 创建买点dataframe
trade_signal_sell = pd.DataFrame(columns=['Date', 'sell_price', 'sell_vol'])
analysis_log.extend([startcash_value, endcash_value, float_profit, completed_net, completed_commission,
start_backtrade_date, end_backtrade_date, sharpeRatio_value, drawdown_value,
moneydown_value, max_drawdown_lastday, total_value, uncompleted_trade, completed_trade,
win_value, lost_value, win_rate, lost_rate])
for key, value in backtrader_analysis.analyzers.Transactions.get_analysis().items():
trade_log = '日期:%s,价格:%.2f,数量:%d,市值:%.2f' % (key.strftime('%Y-%m-%d'), value[0][1], value[0][0],
value[0][4])
analysis_log.extend([trade_log])
history_trade_date = key.strftime('%Y-%m-%d')
history_trade_price = value[0][1]
history_trade_vol = value[0][0]
if history_trade_vol > 0:
history_trade_buy_date_list.append(history_trade_date)
history_trade_buy_price_list.append(history_trade_price)
history_trade_buy_vol_list.append(history_trade_vol)
elif history_trade_vol < 0:
history_trade_sell_date_list.append(history_trade_date)
history_trade_sell_price_list.append(history_trade_price)
history_trade_sell_vol_list.append(history_trade_vol)
trade_signal_buy['Date'] = history_trade_buy_date_list
trade_signal_buy['buy_price'] = history_trade_buy_price_list
trade_signal_buy['buy_vol'] = history_trade_buy_vol_list
trade_signal_buy.set_index('Date', inplace=True)
trade_signal_buy.index = pd.DatetimeIndex(trade_signal_buy.index)
trade_signal_sell['Date'] = history_trade_sell_date_list
trade_signal_sell['sell_price'] = history_trade_sell_price_list
trade_signal_sell['sell_vol'] = history_trade_sell_vol_list
trade_signal_sell.set_index('Date', inplace=True)
trade_signal_sell.index = pd.DatetimeIndex(trade_signal_sell.index)
backtrader_treeview = ttk.Treeview(backtrader_bottomright_frame, columns=['1'], show='headings')
# 在treeview布局钱先布局横竖滚动条,不然会出现布局问题,你可以试着将滚动条代码放在最后试下
VScroll1 = ttk.Scrollbar(backtrader_bottomright_frame, orient='vertical', command=backtrader_treeview.yview)
backtrader_treeview.configure(yscrollcommand=VScroll1.set)
VScroll1.pack(side=tk.RIGHT, fill=tk.Y)
backtrader_treeview.column('1', width=int(tk_window.screenWidth / 4), anchor='w')
backtrader_treeview.heading('1', text='回测记录')
backtrader_treeview.pack(side=tk.LEFT, fill=tk.BOTH, expand=0)
for i in range(len(analysis_log)): # 写入回测记录
backtrader_treeview.insert('', 'end', values=analysis_log[i])
# 合并前面的买卖数据dataframe,为绘图做准备
trade_all = pd.merge(left=data_mpf, right=trade_signal_buy, left_index=True, right_index=True, how='outer')
trade_all = pd.merge(left=trade_all, right=trade_signal_sell, left_index=True, right_index=True, how='outer')
# print(trade_all)
# grid = False不显示分割线
# cerebro.plot(style='candlestick', grid=False, iplot=False)
colors_set = mpf.make_marketcolors(up='red', down='green', edge='i', wick='i', volume='in', inherit=True)
# 设置图形风格,gridaxis:设置网格线位置,gridstyle:设置网格线线型,y_on_right:设置y轴位置是否在右
mpf_style = mpf.make_mpf_style(gridaxis='horizontal', gridstyle='-.', y_on_right=False, facecolor='white',
figcolor='white', marketcolors=colors_set)
# 添加买卖点附图
try: # 设置try语句是预防当只有一个买信号没有卖信号发生报错的情况,比如002978 605388
add_plot = [mpf.make_addplot(trade_all['buy_price'], scatter=True, markersize=130, marker='^', color='r'),
mpf.make_addplot(trade_all['sell_price'], scatter=True, markersize=130, marker='v', color='g')]
daily_fig, axlist = mpf.plot(data_mpf, type='candle', mav=(21, 55), volume=True, show_nontrading=False,
style=mpf_style, addplot=add_plot, returnfig=True)
canvas_stock_daily_basic = FigureCanvasTkAgg(daily_fig, master=backtrader_bottomleft_frame)
canvas_stock_daily_basic.draw()
toolbar_stock_daily_basic = NavigationToolbar2Tk(canvas_stock_daily_basic, backtrader_bottomleft_frame)
toolbar_stock_daily_basic.update() # 显示图形导航工具条
canvas_stock_daily_basic._tkcanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
plt.cla() # 清除axes,即当前 figure 中的活动的axes,但其他axes保持不变。
except Exception as e_plot1:
try:
add_plot = [mpf.make_addplot(trade_all['buy_price'], scatter=True, markersize=130, marker='^', color='r')]
daily_fig, axlist = mpf.plot(data_mpf, type='candle', mav=(21, 55), volume=True, show_nontrading=False,
style=mpf_style, addplot=add_plot, returnfig=True)
canvas_stock_daily_basic = FigureCanvasTkAgg(daily_fig, master=backtrader_bottomleft_frame)
canvas_stock_daily_basic.draw()
toolbar_stock_daily_basic = NavigationToolbar2Tk(canvas_stock_daily_basic, backtrader_bottomleft_frame)
toolbar_stock_daily_basic.update() # 显示图形导航工具条
canvas_stock_daily_basic._tkcanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
plt.cla()
except Exception as e_plot2:
daily_fig, axlist = mpf.plot(data_mpf, type='candle', mav=(21, 55), volume=True, show_nontrading=False,
style=mpf_style, returnfig=True)
canvas_stock_daily_basic = FigureCanvasTkAgg(daily_fig, master=backtrader_bottomleft_frame)
canvas_stock_daily_basic.draw()
toolbar_stock_daily_basic = NavigationToolbar2Tk(canvas_stock_daily_basic, backtrader_bottomleft_frame)
toolbar_stock_daily_basic.update() # 显示图形导航工具条
canvas_stock_daily_basic._tkcanvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
plt.cla()
except Exception as e_cerebro:
tk.messagebox.showwarning(title='错误',
message='%s 数据不足!请查看股票策略指标的参数跟回测日期的数据是否相符以支持回测' % code_name)
print('%s 数据不足!请查看股票策略指标的参数跟回测日期的数据是否相符以支持回测' % code_name)
# ******************************************************************************************************************
def backtrader_go():
plt.close('all') # 先关闭下plt,不关闭的话会在你点完mpl回测后再点backtrader回测报错,可以试着去掉看下有什么BUG
# 以下函数作用是省略输入代码后缀.sz .sh
def code_name_transform(get_stockcode): # 输入的数字股票代码转换成字符串股票代码
str_stockcode = str(get_stockcode)
str_stockcode = str_stockcode.strip() # 删除前后空格字符
if 6 > len(str_stockcode) > 0:
str_stockcode = str_stockcode.zfill(6) + '.SZ' # zfill()函数返回指定长度的字符串,原字符串右对齐,前面填充0
if len(str_stockcode) == 6:
if str_stockcode[0:1] == '0':
str_stockcode = str_stockcode + '.SZ'
if str_stockcode[0:1] == '3':
str_stockcode = str_stockcode + '.SZ'
if str_stockcode[0:1] == '6':
str_stockcode = str_stockcode + '.SH'
return str_stockcode
# 交互数据的获取跟处理
stock_name = input_code_var.get()
code_name = code_name_transform(stock_name)
start_date = input_startdate_var.get()
end_date = input_enddate_var.get()
try:
# adj='qfq'向前复权,freq='D 数据频度:日K线
df = ts.pro_bar(ts_code=code_name, start_date=start_date, end_date=end_date, adj='qfq', freq='D')
df['trade_date'] = pd.to_datetime(df['trade_date'])
# df = df.drop(['change', 'pre_close', 'pct_chg', 'amount'], axis=1)
# 设置用于backtrader的数据
df_back = df.rename(columns={'vol': 'volume'})
df_back.set_index('trade_date', inplace=True) # 设置索引覆盖原来的数据
df_back = df_back.sort_index(ascending=True) # 将时间顺序升序,符合时间序列
df_back['openinterest'] = 0
# 喂养数据到backtrader当中去
back_start_time = datetime.datetime.strptime(start_date, "%Y%m%d") # str转换成时间格式2015-01-01 00:00:00
back_end_time = datetime.datetime.strptime(end_date, '%Y%m%d')
# print(back_start_time)
data_back = bt.feeds.PandasData(dataname=df_back,
fromdate=back_start_time,
todate=back_end_time)
# 创建策略容器
cerebro_single = bt.Cerebro()
# 添加自定义的策略my_strategy
cerebro_single.addstrategy(my_strategy)
# 添加数据
cerebro_single.adddata(data_back)
# 设置资金
startcash_single = 100000
cerebro_single.broker.setcash(startcash_single)
# 设置每笔交易交易的股票数量
cerebro_single.addsizer(bt.sizers.FixedSize, stake=100)
# 设置手续费
cerebro_single.broker.setcommission(commission=0.0005)
# 运行策略,stdstats=False不显示回测的统计结果
cerebro_single.run(stdstats=True, optreturn=False)
# grid = False不显示分割线
cerebro_single.plot(style='candlestick', grid=False, iplot=False)
except Exception as e_cerebro:
tk.messagebox.showwarning(title='错误',
message='%s 数据不足!请查看股票策略指标的参数跟回测日期的数据是否相符以支持回测' % code_name)
print('%s 数据不足!请查看股票策略指标的参数跟回测日期的数据是否相符以支持回测' % code_name)
# ******************************************************************************************************************
def multibacktrader_go():
# 在backtrader_bottom_frame的原有基础上再创建一个框架,目的方便在更新股票股票回测时防止图形重叠
for widget_backtrader_bottom_frame in backtrader_bottom_frame.winfo_children():
widget_backtrader_bottom_frame.destroy()
multi_stock_list = []
# 以下函数作用是省略输入代码后缀.sz .sh
def multi_code_name_transform(get_stockcode): # 输入的数字股票代码转换成字符串股票代码
str_stockcode = str(get_stockcode).split(',') # 分隔符是小写,不是大写,逗号
for s in str_stockcode:
s = s.strip() # 删除前后空格字符
if 6 > len(s) > 0:
s = s.zfill(6) + '.SZ' # zfill()函数返回指定长度的字符串,原字符串右对齐,前面填充0
if len(s) == 6:
if s[0:1] == '0':
s = s + '.SZ'
if s[0:1] == '3':
s = s + '.SZ'
if s[0:1] == '6':
s = s + '.SH'
multi_stock_list.append(s)
return multi_stock_list
# 交互数据的获取跟处理
stock_name = input_multi_code_var.get()
df_basic_all = pro.stock_basic(exchange='', list_status='L') # 获取所有上市公司的信息为全部上市公司回测做准备
if not stock_name: # 如果输入的股票代码为空值
multi_code_name = df_basic_all['ts_code']
else:
multi_code_name = multi_code_name_transform(stock_name)
# print(multi_code_name)
start_date = multi_input_startdate_var.get()
end_date = multi_input_enddate_var.get()
multi_df = pd.DataFrame(columns=['股票代码', '股票名称', '初始资金', '期末资金', '浮动盈亏', '已完成盈亏', '手续费',
'夏普', '最大回撤', '资金回撤', '已回撤天数', '交易次数', '未完成交易', '已完成交易',
'盈亏次数', '亏损次数', '胜率', '败率'])
# 先设置表的列名有哪些
multi_area = ('股票代码', '股票名称', '初始资金', '期末资金', '浮动盈亏', '已完成盈亏', '手续费', '夏普', '最大回撤',
'资金回撤', '已回撤天数', '交易次数', '未完成交易', '已完成交易', '盈亏次数', '亏损次数', '胜率', '败率')
multi_stock_treeview = ttk.Treeview(backtrader_bottom_frame, columns=multi_area, show='headings')
# 在treeview布局钱先布局横竖滚动条,不然会出现布局问题,你可以试着将滚动条代码放在最后试下
VScroll1 = ttk.Scrollbar(backtrader_bottom_frame, orient='vertical', command=multi_stock_treeview.yview)
multi_stock_treeview.configure(yscrollcommand=VScroll1.set)
VScroll1.pack(side=tk.RIGHT, fill=tk.Y)
for i in range(len(multi_area)): # 命名列表名
multi_stock_treeview.column(multi_area[i], width=int(tk_window.screenWidth / 21), anchor='center')
multi_stock_treeview.heading(multi_area[i], text=multi_area[i])
multi_stock_treeview.pack(fill=tk.BOTH, expand=1)
for multi_c in multi_code_name: # 循环获取输入的股票代码
try:
# adj='qfq'向前复权,freq='D 数据频度:日K线
df = ts.pro_bar(ts_code=multi_c, start_date=start_date, end_date=end_date, adj='qfq', freq='D')
multi_stock_basic = pro.stock_basic(ts_code=multi_c, list_status='L')
df['trade_date'] = pd.to_datetime(df['trade_date'])
# df = df.drop(['change', 'pre_close', 'pct_chg', 'amount'], axis=1)
# 设置用于backtrader的数据
df_back = df.rename(columns={'vol': 'volume'})
df_back.set_index('trade_date', inplace=True) # 设置索引覆盖原来的数据
df_back = df_back.sort_index(ascending=True) # 将时间顺序升序,符合时间序列
df_back['openinterest'] = 0
# 喂养数据到backtrader当中去
back_start_time = datetime.datetime.strptime(start_date, "%Y%m%d") # str转换成时间格式2015-01-01 00:00:00
back_end_time = datetime.datetime.strptime(end_date, '%Y%m%d')
# print(back_start_time)
data_back = bt.feeds.PandasData(dataname=df_back,
fromdate=back_start_time,
todate=back_end_time)
# 创建策略容器
cerebro = bt.Cerebro()
# 添加自定义的策略my_strategy
cerebro.addstrategy(my_strategy)
# 添加数据
cerebro.adddata(data_back)
# 设置资金
startcash = 100000
cerebro.broker.setcash(startcash)
# 设置每笔交易交易的股票数量
cerebro.addsizer(bt.sizers.FixedSize, stake=100)
# 设置手续费
cerebro.broker.setcommission(commission=0.0005)
# 输出初始资金
d1 = back_start_time.strftime('%Y%m%d')
d2 = back_end_time.strftime('%Y%m%d')
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='SharpeRatio')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='DW')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='TradeAnalyzer')
cerebro.addanalyzer(bt.analyzers.Transactions, _name='Transactions')
# 运行策略
# stdstats=False不显示回测的统计结果
result = cerebro.run(stdstats=True, optreturn=False)
backtrader_analysis = result[0]
multi_df['股票代码'] = multi_stock_basic['symbol']
multi_df['股票名称'] = multi_stock_basic['name']
multi_df['初始资金'] = startcash
try:
multi_df['期末资金'] = round(cerebro.broker.getvalue(), 2)
except Exception as e1:
multi_df['期末资金'] = None
try:
multi_df['浮动盈亏'] = round((cerebro.broker.getvalue() - startcash -
backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['pnl']['net']
['total']), 2)
except Exception as e2:
multi_df['浮动盈亏'] = None
try:
multi_df['已完成盈亏'] = round(backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['pnl']['net']
['total'], 2)
except Exception as e3:
multi_df['已完成盈亏'] = None
try:
multi_df['手续费'] = round((backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['pnl']['gross']
['total'] - backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['pnl']
['net']['total']), 2)
except Exception as e4:
multi_df['手续费'] = None
try:
multi_df['夏普'] = round(backtrader_analysis.analyzers.SharpeRatio.get_analysis()['sharperatio'], 2)
except Exception as e5:
multi_df['夏普'] = None
try:
multi_df['最大回撤'] = round(backtrader_analysis.analyzers.DW.get_analysis()['max']['drawdown'], 2)
except Exception as e6:
multi_df['最大回撤'] = None
try:
multi_df['资金回撤'] = round(backtrader_analysis.analyzers.DW.get_analysis()['max']['moneydown'], 2)
except Exception as e7:
multi_df['资金回撤'] = None
try:
multi_df['已回撤天数'] = backtrader_analysis.analyzers.DW.get_analysis()['max']['len']
except Exception as e8:
multi_df['已回撤天数'] = None
try:
multi_df['交易次数'] = backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['total']['total']
except Exception as e9:
multi_df['交易次数'] = None
try:
multi_df['未完成交易'] = backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['total']['open']
except Exception as e10:
multi_df['未完成交易'] = None
try:
multi_df['已完成交易'] = backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['total']['closed']
except Exception as e11:
multi_df['已完成交易'] = None
try:
multi_df['盈亏次数'] = backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['won']['total']
except Exception as e12:
multi_df['盈亏次数'] = None
try:
multi_df['亏损次数'] = backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['lost']['total']
except Exception as e13:
multi_df['亏损次数'] = None
try:
multi_df['胜率'] = round((backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['won']['total'] /
backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['total']['total']), 2)
except Exception as e14:
multi_df['胜率'] = None
try:
multi_df['败率'] = round((backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['lost']['total'] /
backtrader_analysis.analyzers.TradeAnalyzer.get_analysis()['total']['total']), 2)
except Exception as e15:
multi_df['败率'] = None
for i in range(len(multi_df.index)): # 导入插入股票数据
# 插入的值数组格式用.tolist()转化成list格式,否则显示会多出‘跟[这种字符串
multi_stock_treeview.insert('', 'end', values=multi_df.values[i].tolist())
print(multi_df)
except Exception as e_cerebro:
print('%s 数据不足!请查看股票策略指标的参数跟回测日期的数据是否相符以支持回测' % multi_c)
continue
def stock_treeview_sort(tv, col, reverse): # Treeview、列名、排列方式
try:
# tv.set指定item,如果不设定column和value,则返回他们的字典,如果设定了column,则返回该column的value,如果value也设定了,则作相应更改。
# get_children()函数,其返回的是treeview中的记录号
# 参照网上的treeview排序方法函数,由于股票的价格排序数据类型是浮点数字,在排序钱将价格类型有str转换成float,否则排序会不正确
stockdata_list = [(float(tv.set(k, col)), k) for k in tv.get_children('')]
except Exception:
stockdata_list = [(tv.set(k, col), k) for k in tv.get_children('')]
# print(tv.get_children(''))
# print(stockdata_list)
stockdata_list.sort(reverse=reverse) # 排序方式
# rearrange items in sorted positions
# 根据排序后索引移动,enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标
for index, (val, k) in enumerate(stockdata_list):
tv.move(k, '', index)
# print(k)
tv.heading(col, command=lambda col=col: stock_treeview_sort(tv, col, not reverse)) # 重写标题,使之成为再点倒序的标题
for col in multi_area:
multi_stock_treeview.column(col, width=70, anchor='center')
multi_stock_treeview.heading(col, text=col,
command=lambda col=col: stock_treeview_sort(multi_stock_treeview, col, False))
# ******************************************************************************************************************
# 在主框架下创建回测按钮子框架
backtrade_left_button_frame = tk.Frame(backtrader_top_left_frame, borderwidth=1, bg='#353535')
backtrade_left_button_frame.pack()
backtrade_right_button_frame = tk.Frame(backtrader_top_right_frame, borderwidth=1, bg='#353535')
backtrade_right_button_frame.pack()
# 创建查询按钮并设置功能
mplfinance_button = tk.Button(backtrade_left_button_frame, text='Mplfinance', height=1, command=mplfinance_go)
mplfinance_button.pack(side=tk.LEFT, padx=4)
backtrader_button = tk.Button(backtrade_left_button_frame, text='BackTrader', height=1, command=backtrader_go)
backtrader_button.pack(side=tk.RIGHT)
multi_backtrader_button = tk.Button(backtrade_right_button_frame, text='MultiBackTrade', height=1,
command=multibacktrader_go)
multi_backtrader_button.pack(side=tk.LEFT, padx=4)