Skip to content

Commit 4ef4be2

Browse files
committed
2021-10-21
1 parent d7db5fe commit 4ef4be2

File tree

1 file changed

+225
-4
lines changed

1 file changed

+225
-4
lines changed

Lesson7.py

+225-4
Original file line numberDiff line numberDiff line change
@@ -287,9 +287,9 @@ def __init__(self):
287287

288288
# 通过参数形式来设置
289289
cerebro.plot(iplot=False,
290-
style='candel', # 设置主图行情数据的样式为蜡烛图
290+
style='candel', # 设置主图行情数据的样式为蜡烛图
291291
lcolors=colors , # 重新设置主题颜色
292-
plotdist=0.1, # 设置图形之间的间距
292+
plotdist=0.1, # 设置图形之间的间距
293293
barup = '#ff9896', bardown='#98df8a', # 设置蜡烛图上涨和下跌的颜色
294294
volup='#ff9896', voldown='#98df8a', # 设置成交量在行情上涨和下跌情况下的颜色
295295
....)
@@ -474,6 +474,227 @@ class TestStrategy(bt.Strategy):
474474
volup='#ff9896',
475475
voldown='#98df8a',
476476
loc='#5f5a41',
477-
# # 蜡烛之间会比较拥挤,可以通过设置 numfigs=2,分 2 部分绘制
477+
# 蜡烛之间会比较拥挤,可以通过设置 numfigs=2,分 2 部分绘制
478478
# numfigs=2,
479-
grid=False) # 删除水平网格
479+
grid=False) # 删除水平网格
480+
481+
# =============================================================================
482+
#%%
483+
# 第3章 基于收益序列进行可视化
484+
'''
485+
Backtrader 自带的绘图工具方便好用,不过平时在汇报策略回测结果时,可能更关注的是策略的累计收益曲线和业绩评价指标等结果,
486+
而这些回测统计信息只需基于回测返回的 TimeReturn 收益序列做简单计算即可得到。
487+
下面是基于 Backtrader 回测返回的分析器 TimeReturn、pyfolio、matplotlib 得到的可视化图形。
488+
'''
489+
import numpy as np
490+
import pandas as pd
491+
import backtrader as bt
492+
import warnings
493+
warnings.filterwarnings('ignore')
494+
495+
import tushare as ts
496+
import json
497+
with open(r'Data/tushare_token.json','r') as load_json:
498+
token_json = json.load(load_json)
499+
token = token_json['token']
500+
ts.set_token(token)
501+
pro = ts.pro_api(token)
502+
503+
# 使用Tushare获取数据,要严格保持OHLC的格式
504+
df = ts.pro_bar(ts_code='600276.SH', adj='qfq',start_date='20160101', end_date='20211015')
505+
df = df[['trade_date', 'open', 'high', 'low', 'close','vol']]
506+
df.columns = ['trade_date', 'open', 'high', 'low', 'close','volume']
507+
df.trade_date = pd.to_datetime(df.trade_date)
508+
# 索引必须是日期
509+
df.index = df.trade_date
510+
# 日期必须要升序
511+
df.sort_index(inplace=True)
512+
513+
# Create a Stratey
514+
class TestStrategy(bt.Strategy):
515+
# 设定参数,便于修改
516+
params = (
517+
('maperiod', 60),
518+
)
519+
520+
def log(self, txt, dt=None):
521+
'''
522+
日志函数:打印结果
523+
datas[0]:传入的数据,包含日期、OHLC等数据
524+
datas[0].datetime.date(0):调用传入数据中的日期列
525+
'''
526+
dt = dt or self.datas[0].datetime.date(0)
527+
# print('%s, %s' % (dt.isoformat(), txt))
528+
529+
def __init__(self):
530+
# dataclose变量:跟踪当前收盘价
531+
self.dataclose = self.datas[0].close
532+
533+
# order变量:跟踪订单状态
534+
self.order = None
535+
# buyprice变量:买入价格
536+
self.buyprice = None
537+
# buycomm变量:买入时佣金费用
538+
self.buycomm = None
539+
540+
# 指标:简单移动平均 MovingAverageSimple【15天】
541+
self.sma = bt.indicators.SimpleMovingAverage(self.datas[0],period=self.params.maperiod)
542+
543+
# 添加画图专用指标
544+
bt.indicators.ExponentialMovingAverage(self.datas[0], period=25)
545+
546+
self.my_indicator_kdj = bt.indicators.StochasticFull(self.datas[0],period=9)
547+
548+
def notify_order(self, order):
549+
'''订单状态通知(order.status):提示成交状态'''
550+
if order.status in [order.Submitted, order.Accepted]:
551+
# 如果订单只是提交状态,那么啥也不提示
552+
return
553+
554+
# 检查订单是否执行完毕
555+
# 注意:如果剩余现金不足,则会被拒绝!
556+
if order.status in [order.Completed]:
557+
if order.isbuy():
558+
# 买入信号记录:买入价、买入费用、买入佣金费用
559+
self.log(
560+
'BUY EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
561+
(order.executed.price,
562+
order.executed.value,
563+
order.executed.comm))
564+
self.buyprice = order.executed.price
565+
self.buycomm = order.executed.comm
566+
elif order.issell():
567+
# 卖出信号记录:卖出价、卖出费用、卖出佣金费用
568+
self.log(
569+
'SELL EXECUTED, Price: %.2f, Cost: %.2f, Comm %.2f' %
570+
(order.executed.price,
571+
order.executed.value,
572+
order.executed.comm))
573+
574+
# 记录订单执行的价格柱的编号(即长度)
575+
self.bar_executed = len(self)
576+
577+
# 如果订单被取消/保证金不足/被拒绝
578+
elif order.status in [order.Canceled, order.Margin, order.Rejected]:
579+
self.log('Order Canceled/Margin/Rejected')
580+
581+
# 如果没有查询到订单,则订单为None
582+
self.order = None
583+
584+
def notify_trade(self, trade):
585+
'''交易状态通知:查看交易毛/净利润'''
586+
if not trade.isclosed:
587+
return
588+
# 交易毛利润:trade.pnl、交易净利润:trade.pnlcomm(扣除佣金)
589+
self.log('OPERATION PROFIT, GROSS %.2f, NET %.2f' %
590+
(trade.pnl, trade.pnlcomm))
591+
592+
# 交易策略主函数
593+
def next(self):
594+
# 记录收盘价
595+
self.log('Close, %.2f' % self.dataclose[0])
596+
597+
# 检查一下是否有订单被挂起,如果有的话就先不下单
598+
if self.order:
599+
return
600+
601+
# 检查一下目前是否持有头寸,如果没有就建仓
602+
if not self.position:
603+
604+
# 如果 K角度>0 且 K>D 且 K<50:
605+
if (self.my_indicator_kdj.percK[0] > self.my_indicator_kdj.percK[-1]) & (self.my_indicator_kdj.percK[0] > self.my_indicator_kdj.percD[0]) & (self.my_indicator_kdj.percK[0]<20):
606+
# 买买买!先记录一下买入价格(收盘价)
607+
self.log('BUY CREATE, %.2f' % self.dataclose[0])
608+
# 更新订单状态:buy():开仓买入,买入价是下一个数据,即【开盘价】
609+
self.order = self.buy()
610+
else:
611+
# 如果已经建仓,并持有头寸,则执行卖出指令
612+
# 如果 K角度<0 且 K<D 且 K>70:
613+
if (self.my_indicator_kdj.percK[0] < self.my_indicator_kdj.percK[-1]) & (self.my_indicator_kdj.percK[0] < self.my_indicator_kdj.percD[0]):
614+
# 卖!卖!卖!
615+
self.log('SELL CREATE, %.2f' % self.dataclose[0])
616+
# 更新订单状态:sell():平仓卖出,卖出价是下一个数据,即【开盘价】
617+
self.order = self.sell()
618+
619+
# 创建实例
620+
cerebro = bt.Cerebro()
621+
# 添加策略
622+
cerebro.addstrategy(TestStrategy)
623+
# 添加数据源
624+
data = bt.feeds.PandasData(dataname=df)
625+
# 输入数据源
626+
cerebro.adddata(data)
627+
# 设置初始现金:10万
628+
cerebro.broker.setcash(1000000.0)
629+
# 设定每次买入的股票数量:10股
630+
cerebro.addsizer(bt.sizers.FixedSize, stake=1000)
631+
# 设置佣金费率:双边0.1%
632+
cerebro.broker.setcommission(commission=0.001)
633+
634+
635+
# 回测时需要添加 TimeReturn 分析器
636+
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='_TimeReturn')
637+
result = cerebro.run()
638+
639+
# 提取收益序列
640+
pnl = pd.Series(result[0].analyzers._TimeReturn.get_analysis())
641+
# 计算累计收益
642+
cumulative = (pnl + 1).cumprod()
643+
# 计算回撤序列
644+
max_return = cumulative.cummax()
645+
drawdown = (cumulative - max_return) / max_return
646+
647+
# 计算收益评价指标
648+
import pyfolio as pf
649+
# 按年统计收益指标
650+
perf_stats_year = (pnl).groupby(pnl.index.to_period('y')).apply(lambda data: pf.timeseries.perf_stats(data)).unstack()
651+
# 统计所有时间段的收益指标
652+
perf_stats_all = pf.timeseries.perf_stats((pnl)).to_frame(name='all')
653+
perf_stats = pd.concat([perf_stats_year, perf_stats_all.T], axis=0)
654+
perf_stats_ = round(perf_stats,4).reset_index()
655+
656+
657+
# 绘制图形
658+
import matplotlib.pyplot as plt
659+
plt.rcParams['axes.unicode_minus'] = False # 用来正常显示负号
660+
import matplotlib.ticker as ticker # 导入设置坐标轴的模块
661+
# plt.style.use('seaborn')
662+
plt.style.use('dark_background')
663+
664+
fig, (ax0, ax1) = plt.subplots(2,1, gridspec_kw = {'height_ratios':[1.5, 4]}, figsize=(20,8))
665+
cols_names = ['date', 'Annual\nreturn', 'Cumulative\nreturns', 'Annual\nvolatility',
666+
'Sharpe\nratio', 'Calmar\nratio', 'Stability', 'Max\ndrawdown',
667+
'Omega\nratio', 'Sortino\nratio', 'Skew', 'Kurtosis', 'Tail\nratio',
668+
'Daily value\nat risk']
669+
670+
# 绘制表格
671+
ax0.set_axis_off() # 除去坐标轴
672+
table = ax0.table(cellText = perf_stats_.values,
673+
bbox=(0,0,1,1), # 设置表格位置, (x0, y0, width, height)
674+
rowLoc = 'right', # 行标题居中
675+
cellLoc='right' ,
676+
colLabels = cols_names, # 设置列标题
677+
colLoc = 'right', # 列标题居中
678+
edges = 'open' # 不显示表格边框
679+
)
680+
table.set_fontsize(13)
681+
682+
# 绘制累计收益曲线
683+
ax2 = ax1.twinx()
684+
ax1.yaxis.set_ticks_position('right') # 将回撤曲线的 y 轴移至右侧
685+
ax2.yaxis.set_ticks_position('left') # 将累计收益曲线的 y 轴移至左侧
686+
# 绘制回撤曲线
687+
drawdown.plot.area(ax=ax1, label='drawdown (right)', rot=0, alpha=0.3, fontsize=13, grid=False)
688+
# 绘制累计收益曲线
689+
(cumulative).plot(ax=ax2, color='#F1C40F' , lw=3.0, label='cumret (left)', rot=0, fontsize=13, grid=False)
690+
# 不然 x 轴留有空白
691+
ax2.set_xbound(lower=cumulative.index.min(), upper=cumulative.index.max())
692+
# 主轴定位器:每 5 个月显示一个日期:根据具体天数来做排版
693+
ax2.xaxis.set_major_locator(ticker.MultipleLocator(100))
694+
# 同时绘制双轴的图例
695+
h1,l1 = ax1.get_legend_handles_labels()
696+
h2,l2 = ax2.get_legend_handles_labels()
697+
plt.legend(h1+h2,l1+l2, fontsize=12, loc='upper left', ncol=1)
698+
699+
fig.tight_layout() # 规整排版
700+
plt.show()

0 commit comments

Comments
 (0)