前言:
先放效果先吧:
一:这次更新主要是增加了策略文件这一功能,主要目的是为了方便切换策略测试。增添了弹出菜单新建策略、编辑、运行该策略、重命名、删除、刷新功能。
二:其中在点击回测是会在你项目的路径增添策略文件这一新的文件夹,之后的新建策略也会是在这一文件夹内,新建的文件格式是txt,在新建文件后最好能重命名一下,如果不重命名则最多可以新建两个文件相同的文件,新建文件后要手动点击菜单的刷新才会有显示,或者重新运行程序。重命名是最好加上.txt后缀。
三:每次点击Mplfinance或者其他功能回测按钮时,请先选中策略选中运行该策略功能在进行回测,否则会弹窗提示。
四:文件txt策略内容的缩进如图所示,全部的缩进是原来格式一个’shift‘ + ‘Tab’,前面不能有空格。最好在pycharm编辑策略跟缩进再放到txt文件内。
其他代码不变,修改的只是stock_backtrader.py文件,其他代码请到V0.42版本复制
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 os
import sys
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, bg='#353535', bd=5, borderwidth=4)
backtrader_left_frame.pack(fill=tk.BOTH, expand=1)
# ******************************************************************************************************************
# path = sys.path[0] # 启动的py文件所在的路径
File_Path = os.getcwd() + '\\策略文件' # 获取到当前文件的目录,并检查是否有‘策略文件’文件夹,如果不存在则自动新建‘策略文件’文件夹
if not os.path.exists(File_Path):
os.makedirs(File_Path)
pathList = os.path.split(File_Path) # 分别获取得到绝对路径跟文件夹名称,得到的是个列表
pathlist_name = pathList[-1] # 获取文件夹名称,如果是[0]则是获取绝对路径
strategy_list_tree = ttk.Treeview(backtrader_left_frame, show='tree')
father_treeview = strategy_list_tree.insert("", "end", text=pathlist_name, open=True) # 写入父节名称
for filepath in os.listdir(File_Path):
strategy_list_tree.insert(father_treeview, "end", text=filepath) # 写入子节名称
strategy_list_tree.pack(fill=tk.BOTH, expand=1)
backtrader_window.add(backtrader_left_frame, width=tk_window.screenHeight / 5.5)
def create_newfile():
if not os.path.exists(File_Path + '\\' + '新建策略.txt'):
txt_file = open(File_Path + '\\' + '新建策略.txt', 'ab+')
txt_file.close()
else:
txt_file = open(File_Path + '\\' + '新建策略1.txt', 'ab+')
txt_file.close()
def rename_newfile():
rename_input_frame = tk.Toplevel()
rename_input_frame.title('重命名')
rename_input_frame.geometry('{}x{}+{}+{}'.format(180, 35, int(tk_window.screenWidth / 4),
int(tk_window.screenHeight / 4)))
rename_input_var = tk.StringVar()
rename_input_widget = tk.Entry(rename_input_frame, textvariable=rename_input_var, justify=tk.CENTER)
rename_input_widget.pack(side=tk.LEFT)
def rename_now():
for item in strategy_list_tree.selection():
item_text = strategy_list_tree.item(item, "text") # 获取选中树形条目的名称
os.rename(File_Path + '\\' + item_text, File_Path + '\\' + rename_input_var.get())
rename_button = tk.Button(rename_input_frame, text='rename', height=1, command=rename_now)
rename_button.pack(side=tk.RIGHT)
def edit_file():
for item in strategy_list_tree.selection():
item_text = strategy_list_tree.item(item, "text") # 获取选中树形条目的名称
select_filepath = File_Path + '\\' + item_text # 得到选中项目的绝对路径
os.startfile(select_filepath) # 打开文件,如果不是txt格式的文件会弹出窗口让你选择打开方式
# 设置获取策略文件txt内容的函数,首先通过read获取内容,然后通过exec(use_strategy())将本函数返回的str格式文本内容转换成可执行的代码
# 在每个策略运行前的代码先写class my_strategy(bt.Strategy):,然后再写exec(use_strategy())
# 在运行策略是先选中你建立的txt策略文件,然后右键鼠标在弹出的菜单中选中使用该策略选项,之后再运行回测
def use_strategy():
for item in strategy_list_tree.selection():
item_text = strategy_list_tree.item(item, "text") # 获取选中树形条目的名称
select_filepath = File_Path + '\\' + item_text # 得到选中项目的绝对路径
txt_file = open(select_filepath, 'r')
txt_data = txt_file.read()
txt_file.close()
return txt_data
def delect_file():
for item in strategy_list_tree.selection():
item_text = strategy_list_tree.item(item, "text") # 获取选中树形条目的名称
select_filepath = File_Path + '\\' + item_text # 得到选中项目的绝对路径
delect_confirm = tk.messagebox.askokcancel('提示', '要执行此操作吗?文件直接删除不放回收站!')
if delect_confirm: # 如果返回True,则执行删除,
os.remove(select_filepath) # 打开文件,如果不是txt格式的文件会弹出窗口让你选择打开方式
# 创建弹出菜单,为后面功能开发做准备
strategy_menu = tk.Menu(backtrader_left_frame, tearoff=False) # tearoff=True显示分割线
strategy_menu.add_command(label='新建策略', command=create_newfile) # 弹出菜单内容
strategy_menu.add_command(label='编辑', command=edit_file) # 弹出菜单内容
strategy_menu.add_command(label='运行该策略', command=use_strategy) # 弹出菜单内容
strategy_menu.add_command(label='重命名', command=rename_newfile) # 弹出菜单内容
strategy_menu.add_command(label='删除', command=delect_file)
strategy_menu.add_separator()
strategy_menu.add_command(label='刷新', command=run_cerebro)
def pop(event):
strategy_menu.post(event.x_root, event.y_root) # #设置弹出的位置
strategy_list_tree.bind('<Button-3>', pop) # 设置右键弹出菜单
# ******************************************************************************************************************
# 创建右边图形输出框架,主要放回测分析显示跟用户输入的股票代码跟日期
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:
class my_strategy(bt.Strategy):
exec(use_strategy())
except Exception as e_class:
tk.messagebox.showwarning(title='错误', message='请先选择运行策略再进行回测')
print('请先选择运行策略再进行回测')
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()
class my_strategy(bt.Strategy):
exec(use_strategy())
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') # 获取所有上市公司的信息为全部上市公司回测做准备
class my_strategy(bt.Strategy):
exec(use_strategy())
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)
HScroll1 = ttk.Scrollbar(backtrader_bottom_frame, orient='horizontal', command=multi_stock_treeview.xview)
multi_stock_treeview.configure(xscrollcommand=HScroll1.set)
HScroll1.pack(side=tk.BOTTOM, fill=tk.X)
for i in range(len(multi_area)): # 命名列表名
multi_stock_treeview.column(multi_area[i], 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)