前言:

思考了段时间,分析股票回测最终还是选择了backtrader,大体写了个框架,目前的效果图如下(后期还会改):

这次新添加了两个py文件,分别是stock_backtrader.py跟function.py,其中stock_backtrader.py就是主要负责回测这一块的代码,而function.py则是负责类似新添的时钟小功能,现在目前只是一个大体的框架思路,后期可能还会有很大的改动,先写下记录下。

首先是main.py代码:

import tk_window
import graphic
import function
import stock_backtrader
然后是tk_window.py
import tkinter as tk
import graphic
import function
import stock_backtrader
root = tk.Tk() # 创建主窗口
s = graphic.Show() # Show实例化
screenWidth = root.winfo_screenwidth() # 获取屏幕宽的分辨率
screenHeight = root.winfo_screenheight()
x, y = int(screenWidth / 4), int(screenHeight / 4) # 初始运行窗口屏幕坐标(x, y),设置成在左上角显示
width = int(screenWidth / 2) # 初始化窗口是显示器分辨率的二分之一
height = int(screenHeight / 2)
root.geometry('{}x{}+{}+{}'.format(width, height, x, y)) # 窗口的大小跟初始运行位置
root.title('Wilbur量化分析软件')
# root.resizable(0, 0) # 固定窗口宽跟高,不能调整大小,无法最大窗口化
root.iconbitmap('ZHY.ico') # 窗口左上角图标设置,需要自己放张图标为icon格式的图片文件在项目文件目录下
# 构建上方菜单栏目框架
top_frame = tk.Frame(root, width=screenWidth, height=screenHeight, relief=tk.SUNKEN, bg='#353535', bd=5, borderwidth=4)
top_frame.pack(fill=tk.BOTH, side=tk.TOP, expand=0)
# 构建底部状态栏目框架
bottom_frame = tk.Frame(root, width=screenWidth, height=screenHeight, relief=tk.SUNKEN, bg='#353535', bd=5, borderwidth=4)
bottom_frame.pack(fill=tk.BOTH, side=tk.BOTTOM, expand=0)
# 构建左边功能栏目框架
left_frame = tk.Frame(root, width=screenWidth, height=screenHeight, relief=tk.SUNKEN, bg='#353535', bd=5, borderwidth=4)
left_frame.pack(fill=tk.BOTH, side=tk.LEFT, expand=0)
# 构建右边信息栏目框架
# right_frame = tk.Frame(root, width=screenWidth, height=screenHeight,
# relief=tk.SUNKEN, bg='#353535', bd=5, borderwidth=4)
# right_frame.pack(fill=tk.BOTH, side=tk.RIGHT, expand=0)
# 构建中间显示栏目框架
centre_frame = tk.Frame(root, width=screenWidth, height=screenHeight, relief=tk.SUNKEN, bg='#353535', bd=5, borderwidth=4)
centre_frame.pack(fill=tk.BOTH, expand=1)
# 构建各个框架的标签或按钮
top_label = tk.Label(top_frame, text='菜单栏目', bd=1)
top_label.pack()
bottom_label = tk.Label(bottom_frame, text='状态栏目', bd=1)
bottom_label.pack(side=tk.LEFT)
# 在状态栏目添加系统时钟功能
function.time_clock()
left_button1 = tk.Button(left_frame, text='全景', bd=1, command=s.stockindex_function)
left_button1.pack()
left_button2 = tk.Button(left_frame, text='查询', bd=1, command=s.stock_query_function)
left_button2.pack()
left_button3 = tk.Button(left_frame, text='股票', bd=1, command=s.show_stocklist_function)
left_button3.pack()
left_button4 = tk.Button(left_frame, text='回测', bd=1, command=stock_backtrader.run_cerebro)
left_button4.pack()
left_button5 = tk.Button(left_frame, text='优化', bd=1, command=stock_backtrader.run_optimization)
left_button5.pack()
centre_label = tk.Label(centre_frame, text='显示栏目', bd=1)
centre_label.pack()
root.mainloop()
其次是graphic.py
代码发布在我的另外一篇文章,代码太多,超过了5000字字限制了。
还有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
import matplotlib.pyplot as plt
import matplotlib as mpl # 用于设置曲线参数
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk) # 使用后端TkAgg
mpl.use('TkAgg')
ts.set_token('数据用的是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]) # 输出打印收盘价
# 检查是否有订单发送当中,如果有则不再发送第二个订单
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(): # 策略回测
# 数据的获取跟处理
stock_code = '000004.SZ'
stock_start_date = '20150101'
stock_end_date = '20200828'
# adj='qfq'向前复权,freq='D 数据频度:日K线
df = ts.pro_bar(ts_code=stock_code, start_date=stock_start_date, end_date=stock_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)
df = df.rename(columns={'vol': 'volume'})
df.set_index('trade_date', inplace=True) # 设置索引覆盖原来的数据
df = df.sort_index(ascending=True) # 将时间顺序升序,符合时间序列
df['openinterest'] = 0
# 喂养数据到backtrader当中去
back_start_time = datetime.datetime(2015, 1, 1)
back_end_time = datetime.datetime(2020, 8, 28)
data = bt.feeds.PandasData(dataname=df,
fromdate=back_start_time,
todate=back_end_time
)
# 创建策略容器
cerebro = bt.Cerebro()
# 添加自定义的策略my_strategy
cerebro.addstrategy(my_strategy)
# 添加数据
cerebro.adddata(data)
# 设置资金
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')
print('初始资金: %.2f' % startcash)
print('回测开始时间: %s' % d1)
print('回测结束时间: %s' % d2)
# 运行策略
# stdstats=False不显示回测的统计结果
cerebro.run(stdstats=True)
net_profit = cerebro.broker.getvalue() - startcash
print('净收益: %.2f' % net_profit)
# grid = False不显示分割线
# cerebro.plot(style='candlestick', grid=False, iplot=False)
plofit_show = plt.figure('Figure5')
df.close.plot()
# df.close[0]指的是测试开始日期收盘价,df.close[-1]指的是数据结束日期收盘价
plt.annotate('期间累计涨幅: %.2f' % ((df.close[-1]/df.close[0]-1)*100), xy=(df.index[-150], df.close.mean()),
xytext=(df.index[-500], df.close.min()), bbox=dict(boxstyle='round,pad=0.5',
fc='yellow', alpha=0.5),
arrowprops=dict(facecolor='green', shrink=0.05), fontsize=12)
print(df.close[0])
print(df.close[-1])
print(df)
for widget_profit_show_frame in tk_window.centre_frame.winfo_children():
widget_profit_show_frame.destroy()
canvas_stock_daily_basic = FigureCanvasTkAgg(plofit_show, master=tk_window.centre_frame)
canvas_stock_daily_basic.draw()
toolbar_stock_daily_basic = NavigationToolbar2Tk(canvas_stock_daily_basic, tk_window.centre_frame)
toolbar_stock_daily_basic.update() # 显示图形导航工具条
canvas_stock_daily_basic._tkcanvas.pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1)
# 策略参数优化函数
class my_optimization(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]) # 输出打印收盘价
# 检查是否有订单发送当中,如果有则不再发送第二个订单
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_optimization():
stock_code = '000001.SZ'
stock_start_date = '20150101'
stock_end_date = '20200828'
df = ts.pro_bar(ts_code=stock_code, start_date=stock_start_date, end_date=stock_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)
df = df.rename(columns={'vol': 'volume'})
df.set_index('trade_date', inplace=True) # 设置索引覆盖原来的数据
df = df.sort_index(ascending=True) # 将时间顺序升序,符合时间序列
df['openinterest'] = 0
# 喂养数据到backtrader当中去
back_start_time = datetime.datetime(2015, 1, 1)
back_end_time = datetime.datetime(2020, 8, 28)
data = bt.feeds.PandasData(dataname=df,
fromdate=back_start_time,
todate=back_end_time
)
# 创建策略容器
cerebro = bt.Cerebro()
# 添加自定义的策略my_strategy
cerebro.optstrategy(my_optimization, maperiod21=range(3, 54))
# 添加数据
cerebro.adddata(data)
# 设置资金
startcash = 100000
cerebro.broker.setcash(startcash)
# 设置每笔交易交易的股票数量
cerebro.addsizer(bt.sizers.FixedSize, stake=100)
# 设置手续费
cerebro.broker.setcommission(commission=0.01)
print('期初总资金: %.2f' %
cerebro.broker.getvalue())
cerebro.run(maxcpus=1)
最后function.py
import tkinter as tk
import time
import tk_window
def time_clock():
tk_clock_var = tk.StringVar()
tk_clock = tk.Label(tk_window.bottom_frame, textvariable=tk_clock_var, bg='#353535', fg='white')
tk_clock.pack(side=tk.RIGHT)
def tk_clock_trickit():
tk_clock_var.set(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())))
tk_window.bottom_frame.update()
tk_clock.after(0, tk_clock_trickit)
tk_clock.after(0, tk_clock_trickit)