# tushare ID:409200
# 【PY从0到1】 bt完整框架


# 本节介绍
# 优化了开关逻辑
# 取消了输出信息中无效的部分
# 添加策略的时候增加了策略参数变量直接修改params
# 完整的策略框架搭建完成


#导入库
import datetime  as dt
import pandas as pd
import tushare as ts
import backtrader as bt
import seaborn as sns
sns.set()

# 本案例用到的函数源码
#----------------------

def data_obtain(key, code, start, end):
    '''
    获取相应股票的数据
    基础函数不做过多注释
    时间格式举例:20180101
    '''
    pro = ts.pro_api(key)
    df = pro.daily(ts_code=code, 
                      start_date=start, 
                      end_date=end, 
                      fields='trade_date,open,high,low,close,vol')
    df.rename(columns={'trade_date':'date', 
                       'vol':'volume'}, inplace=True) 
    df['date'] = pd.to_datetime(df['date'])
    df.set_index('date', inplace=True)
    code = code[:6]
    df.to_csv('data/stock/' + code + '.csv')

    
def pools_get4fn(fnam,tim0str,tim9str,fgSort=True):
    '''
    数据读取函数,及标准化函数
    '''
    df = pd.read_csv(fnam, index_col=0, parse_dates=True)                       # 读取数据
    df.sort_index(ascending=fgSort,inplace=True)                                # 正序排列
    tim0 = None if tim0str == '' else dt.datetime.strptime(tim0str,'%Y-%m-%d')  # 改变时间格式
    tim9 = None if tim9str == '' else dt.datetime.strptime(tim9str,'%Y-%m-%d')  # 改变时间格式
    df['openinterest'] = 0                                                      # 目前没有作用,等到以后的课程解释
    data = bt.feeds.PandasData(dataname=df,fromdate=tim0,todate=tim9)           # 转化为bt内部格式
    return data

#----------------------

# 策略加载
#----------------------
    
class TySta001(bt.Strategy):
    '''
    这是一个储存策略框架的类
    '''
    params = (
        ('maperiod', 20),
        ('fgprint', True),
        )                                                # 定义策略使用的默认参数
    
    def log(self, txt, dt=None, fgprint=True):
        '''
        记录函数,打印一些交易记录
        '''
        if self.params.fgprint and fgprint:              # 打印开关
            dt = dt or self.datas[0].datetime.date(0)    # dt为空就运行or后面的,后面的为获取当天日期
            print('\t%s, %s' % (dt.isoformat(), txt))    # 打印当天日期和txt
    
    def __init__(self):
        '''
        初始化参数
        运行频率:与数据周期一致
        '''
        self.dataclose = self.datas[0].close         # 当天获取收盘价
        self.order = None                            # 订单交易情况清零
        self.buyprice = None                         # 买入价格清零
        self.buycomm = None                          # 交易佣金额清零
        self.sma = bt.indicators.SimpleMovingAverage(self.datas[0], 
                                                     period=self.params.maperiod) # 加载SMA指标
        self.macd = bt.indicators.MACDHisto(self.datas[0], subplot=True)          # 加载MACD指标
        # self.em = bt.indicators.ExponentialMovingAverage(self.datas[0], 
        #                                                   subplot=True)           # 加载滑动平均    
        # self.rsi = bt.indicators.RSI(self.datas[0], subplot=True)                 # 加载RSI指标
        # self.atr = bt.indicators.ATR(self.datas[0], plot=True)                    # 加载ATR指标,注意默认plot=False
        
    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.bar_executed = len(self)                                # 完成订单持续的周期
                self.buyprice = order.executed.price                         # 记录买入价格
                self.buycomm = order.executed.comm                           # 记录佣金
            elif order.issell():                                             # 为卖单
                self.log('卖单设置成功,成交价为:%.2f,小计:%.2f,佣金:%.2f'
                         % (order.executed.price, order.executed.value, order.executed.comm))
                   
        elif order.status in [order.Canceled, order.Margin, order.Rejected]: # 订单状态为取消、保证金和拒绝
            self.log('订单状态异常:取消/保证金/拒绝。')
        
        self.order = None                                                    # 订单检测完成,将订单状态清零
        
    def notify_trade(self, trade):
        '''
        检查订单是否关闭
        若订单关闭,则打印利润情况
        运行频率:与数据周期一致
        '''
        if not trade.isclosed:
            return
        else:                                                                # 订单关闭时统计盈利情况
            self.log('交易利润,毛利:%.2f,净利:%.2f' % (trade.pnl, trade.pnlcomm))
        
    def next(self):
        '''
        核心策略
        移动平均线策略
        运行频率:与数据周期一致
        '''
        self.log(txt='收盘价为:%.2f' % self.dataclose[0] + '',
                 fgprint=False)                                              # 调用log函数
        if not self.position:                                                # 没有持仓
            if self.dataclose[0] > self.sma[0]:                              # 收盘价大于均线
                self.order = self.buy()                                      # 设置买单
                self.log(txt='设置买单BUY!:%.2f, 代码为:%s' %
                         (self.dataclose[0], self.datas[0]._name))           # 调用log
        
        else:                                                                # 有持仓
            if self.dataclose[0] < self.sma[0]:                              # 收盘价小于均线
                self.order = self.sell()                                     # 设置卖单
                self.log(txt='设置卖单SELL!:%.2f, 代码为:%s' % 
                          (self.dataclose[0], self.datas[0]._name))          # 调用log

    def stop(self):
        '''
        在策略执行完毕后输出参数周期以及最终的
        运行频率:只有在最后的回测期输出
        '''
        self.log('MA均线周期 =%2d, 最终资产总值:%.2f' % 
                 (self.params.maperiod, self.broker.getvalue()), 
                 fgprint=True)                                               # 打印结束状态
            
#---------------------
     
# 颜色形状配置库
#---------------------
# K线和量能bar
tq10_corUp,tq10_corDown=['#7F7F7F','#17BECF']           
tq09_corUp,tq09_corDown=['#B61000','#0061B3']
tq08_corUp,tq08_corDown=['#FB3320','#020AF0']
tq07_corUp,tq07_corDown=['#B0F76D','#E1440F']
tq06_corUp,tq06_corDown=['#FF3333','#47D8D8']
tq05_corUp,tq05_corDown=['#FB0200','#007E00']
tq04_corUp,tq04_corDown=['#18DEF5','#E38323']
tq03_corUp,tq03_corDown=['black','blue']
tq02_corUp,tq02_corDown=['red','blue']
tq01_corUp,tq01_corDown=['red','lime']

tq_ksty01=dict(volup=tq01_corUp,voldown=tq01_corDown,barup=tq01_corUp,bardown=tq01_corDown)
tq_ksty02=dict(volup=tq02_corUp,voldown=tq02_corDown,barup=tq02_corUp,bardown=tq02_corDown)
tq_ksty03=dict(volup=tq03_corUp,voldown=tq03_corDown,barup=tq03_corUp,bardown=tq03_corDown)
tq_ksty04=dict(volup=tq04_corUp,voldown=tq04_corDown,barup=tq04_corUp,bardown=tq04_corDown)
tq_ksty05=dict(volup=tq05_corUp,voldown=tq05_corDown,barup=tq05_corUp,bardown=tq05_corDown)
tq_ksty06=dict(volup=tq06_corUp,voldown=tq06_corDown,barup=tq06_corUp,bardown=tq06_corDown)
tq_ksty07=dict(volup=tq07_corUp,voldown=tq07_corDown,barup=tq07_corUp,bardown=tq07_corDown)
tq_ksty08=dict(volup=tq08_corUp,voldown=tq08_corDown,barup=tq08_corUp,bardown=tq08_corDown)
tq_ksty09=dict(volup=tq09_corUp,voldown=tq09_corDown,barup=tq09_corUp,bardown=tq09_corDown)
tq_ksty10=dict(volup=tq10_corUp,voldown=tq10_corDown,barup=tq10_corUp,bardown=tq10_corDown)

# 买卖符号
class MyBuySell(bt.observers.BuySell):
    '''
    从三个里面选一个
    屏蔽其他两个
    '''
    # plotlines = dict(buy=dict(marker='$\u21E7$', markersize=12.0),
    #                   sell=dict(marker='$\u21E9$', markersize=12.0))
                     
    # plotlines = dict(buy=dict(marker='$++$', markersize=12.0),
    #                   sell=dict(marker='$--$', markersize=12.0))
                     
    plotlines = dict(buy=dict(marker='$✔$', markersize=12.0),
                      sell=dict(marker='$✘$', markersize=12.0))
 
#---------------------
 
# 开始回测
    
# 加载cerebro 大脑
cerebro = bt.Cerebro()                # 加载大脑
print('\n#1,回测大脑加载完成!')

# 设置初始资金
dmoney0 = 100000.0                    # 初始资金设定
cerebro.broker.setcash(dmoney0)       # 加载入大脑
dcash0 = cerebro.broker.startingcash  # 获取初始资金
print('\n#2,初始资金设置完成,载入大脑!')

# 用函数抓取数据
# data_obtain('',
#             '000001.SZ', 
#             '20170101',
#             '20200101')

# 读取数据
rs0 = 'data/stock/'                       # 数据存储地址
xcod = '000001'                           # 需要处理的股票代码
fdat = rs0+xcod+'.csv'                    # 数据完整路径
print('\t 数据载入完成,数据路径为:',fdat)

# 数据切割并标准化
t0str,t9str = '2018-01-01','2018-04-15'   # 数据切割的时间
data = pools_get4fn(fdat,t0str,t9str)     # 数据切割、标准化
print('\t 数据切割完毕,成功标准化。')

# 平均K线图
# data.addfilter(bt.filters.HeikinAshi)   # 添加一个过滤器

# 将数据加入大脑
cerebro.adddata(data, name=xcod)          # 数据载入大脑
print('\t 数据载入大脑。')

# 加载策略
cerebro.addstrategy(TySta001, maperiod=21, fgprint=True) # 添加策略,params中的参数可以直接在此修改
print('\t 策略添加完成。')

# 佣金设置
cerebro.broker.setcommission(commission=0.001)          # 添加佣金
print('\t 交易费用设置完成。')

# 交易数量设置
cerebro.addsizer(bt.sizers.FixedSize, stake=800)        # 使用固定的交易手数
print('\t 交易数目设置成功。')

# 加载策略分析指标
# SQN
cerebro.addanalyzer(bt.analyzers.SQN, _name='SqnAnz')
# TimeReturn
tframes = dict(days=bt.TimeFrame.Days,
               weeks=bt.TimeFrame.Weeks,
               months=bt.TimeFrame.Months,
               years=bt.TimeFrame.Years)                                     # 时间周期字典
cerebro.addanalyzer(bt.analyzers.TimeReturn, 
                    timeframe=tframes['years'], 
                    _name='TimeAnz')                                         # timeframe时间周期选择
# sharpe
cerebro.addanalyzer(bt.analyzers.SharpeRatio, 
                    _name='SharpeRatio', legacyannual=True)
# AnnualReturun
cerebro.addanalyzer(bt.analyzers.AnnualReturn, _name='AnnualReturn')         # 投资回报率
# TradeAnalyzer
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='TradeAnalyzer')
# DrawDown
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='DW')
# VWR
cerebro.addanalyzer(bt.analyzers.VWR, _name='VWR')

# 买卖符号设定
bt.observers.BuySell = MyBuySell          # 加载预设符号

# 进行回测
print('\n#3,大脑正在分析数据。')
results = cerebro.run()                   # 运行策略

# 分析结果
dval9 = cerebro.broker.getvalue()         # 获得最后的资金数
dget = dval9 - dcash0
kret = dget / dcash0 * 100    # 计算投资回报率
print('\n#4,大脑处理完成。\n   分析结果如下:')
print('\t起始资金 : %.2f' % dcash0 + '')
print('\t资产总值 : %.2f' % dval9 + '')
print('\t利润总额 :%.2f' % dget + '')
print('\t投资回报率(ROI):%.2f %%' % kret)

# 评估策略质量
print('\n#5,大脑正在评估策略质量。')
strat = results[0]                                      # 获取第一个策略的测试结果
anzs = strat.analyzers                                  # 获取实例
dsharp = anzs.SharpeRatio.get_analysis()['sharperatio'] # 获得夏普比率结果
trade_info = anzs.TradeAnalyzer.get_analysis()          # 交易统计信息
dw = anzs.DW.get_analysis()                             # 获得回撤数据
max_drowdown_len = dw['max']['len']                     # 最大周期
max_drowdown = dw['max']['drawdown']                    # 最大回撤
max_drowdown_money = dw['max']['moneydown']             # 最大回撤金额
print('\t夏普指数为:', dsharp)
print('\t最大回撤周期为:', max_drowdown)
print('\t最大回测金额为:', max_drowdown_money)
# 加载所有加载的评估指标
print('完整的评估指标如下:')
for alyzer in strat.analyzers:                          
    alyzer.print()

# 可视化
# style为K线风格参数,支持line candle bar ohlc。**tq_ksty为K线与成交量颜色设定(01~10)
cerebro.plot(style='candle', **tq_ksty08) 
# volume成交量参数开关,voloverlay子图开关
cerebro.plot(volume=True, voloverlay=True) 
# numfigs切分K线图
cerebro.plot(numfigs=1)
print('\n#6,绘制处理结果。')

# 回测完成
print('\nBackTrader回测完毕。')

 

AI量化的成长之路