@@ -287,9 +287,9 @@ def __init__(self):
287
287
288
288
# 通过参数形式来设置
289
289
cerebro .plot (iplot = False ,
290
- style = 'candel' , # 设置主图行情数据的样式为蜡烛图
290
+ style = 'candel' , # 设置主图行情数据的样式为蜡烛图
291
291
lcolors = colors , # 重新设置主题颜色
292
- plotdist = 0.1 , # 设置图形之间的间距
292
+ plotdist = 0.1 , # 设置图形之间的间距
293
293
barup = '#ff9896' , bardown = '#98df8a' , # 设置蜡烛图上涨和下跌的颜色
294
294
volup = '#ff9896' , voldown = '#98df8a' , # 设置成交量在行情上涨和下跌情况下的颜色
295
295
....)
@@ -474,6 +474,227 @@ class TestStrategy(bt.Strategy):
474
474
volup = '#ff9896' ,
475
475
voldown = '#98df8a' ,
476
476
loc = '#5f5a41' ,
477
- # # 蜡烛之间会比较拥挤,可以通过设置 numfigs=2,分 2 部分绘制
477
+ # 蜡烛之间会比较拥挤,可以通过设置 numfigs=2,分 2 部分绘制
478
478
# 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\n return' , 'Cumulative\n returns' , 'Annual\n volatility' ,
666
+ 'Sharpe\n ratio' , 'Calmar\n ratio' , 'Stability' , 'Max\n drawdown' ,
667
+ 'Omega\n ratio' , 'Sortino\n ratio' , 'Skew' , 'Kurtosis' , 'Tail\n ratio' ,
668
+ 'Daily value\n at 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