前言:
就只更新了一个多股策略运行线程关闭功能按钮,由于添加了线程管理代码,多股回测的结果实时显示在tkinter,回测一个显示一个。只更改了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 os
import threading
import time
import sys
import inspect
import ctypes
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('' , 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图形的数据
# :取所有行数据,后面取date列,open列等数据
data_mpf = df.loc[:, ['trade_date', 'open', 'close', 'high', 'low', 'vol']]
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], width=8, 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, anchor='center')
multi_stock_treeview.heading(col, text=col,
command=lambda col=col: stock_treeview_sort(multi_stock_treeview, col, False))
# ******************************************************************************************************************
# 线程管理,对运行中的策略进行结束功能
def thread_start(func, *args):
global t
t = threading.Thread(target=func, args=args)
t.setDaemon(True)
t.start()
def _async_raise(tid, exctype):
tid = ctypes.c_long(tid)
if not inspect.isclass(exctype):
exctype = type(exctype)
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
if res == 0:
raise ValueError("invalid thread id")
elif res != 1:
ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
raise SystemError("PyThreadState_SetAsyncExc failed")
def stop_thread(thread):
_async_raise(thread.ident, SystemExit)
# ******************************************************************************************************************
# 在主框架下创建回测按钮子框架
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=lambda: thread_start(multibacktrader_go))
multi_backtrader_button.pack(side=tk.LEFT, padx=4)
multi_pause_button = tk.Button(backtrade_right_button_frame, text='Stop', height=1,
command=lambda: stop_thread(t))
multi_pause_button.pack(side=tk.LEFT, padx=4)