backtest2104

code

import datetime

import numpy as np, pandas as pd
import matplotlib.pyplot as plt # 个性配置文件: C:\Users\Administrator\.matplotlib\matplotlibrc
from matplotlib.ticker import FuncFormatter, MaxNLocator # 刻度的格式, 刻度的定位
import mplfinance as mpf; _=mpf
from mplfinance.original_flavor import candlestick_ohlc
# from cycler import cycler# 用于定制线条颜色

import util.ttr as ttr
from dqsbt.util2 import util as aid # setcash/comm, adddata, addanalyzer
from qsutil.gspace import g
from qsutil.u import ucheck
from qsutil.db.dboperation import resample_d_to_w

from candleplotter import  CandlePlotter
from study.tlog import Tlog
from qsutil import pkl
from qsutil.u import uplot
from qsutil.u import uload
# df_dict = uload.load_data(g.codes_3kj)

def load_1h():
    pfile =  'd:\\secData\\5min\\hs300_1h.pkl'
    # pkl.pkl_dump((dfh,dfw),pfile)    
    dfh, dfw = pkl.pkl_load(pfile)
    return dfh, dfw

def load_nmoney():
    pfile =  'd:\\secData\\nmoney.pkl'
    # pkl.pkl_dump((dfh,dfw),pfile)    
    nmoney= pkl.pkl_load(pfile)
    return nmoney

def load_eod():
    df_dict=aid.load_data(g.k1l)
    return df_dict[g.k1]

# def get_index_eod(code='399296.SZ', start='2018-01-01', end='2019-12-31',):
def get_index_eod(code='399296.SZ', start='1933', end='2033',):
    return uload.get_index_df(code, start, end)

#%% basestrategy

class BaseStrategy:
    '''EOD日线数据'''
    def __init__(self, df=None, 
                 code=None, 
                 start='2020-1-1', 
                 end='2020-12-31', 
                 n_sma=5, 
                 moveup_level=4, #跟踪止损线 上移门槛 4: 4%
                 n_tsl=3, n_nbar=8,
                 n_hhv=3, n_llv=3, n_median=3, upslope=3, 
                 n_lr1=21, n_lr2=8, 
                 
                 freq='Daily', 
                 figsize=(12,6),  # (12,8)
                 comm=1/100/100., 
                 **kwargs):

        self.dfd=df # daily df
    
        self.p=dict()
        self.p.update(dict(
            code=code,
            dname=g.cn_dict[code], 
            start=start, end=end,
            n_hhv=n_hhv,
            n_llv=n_llv,
            n_median=n_median,
            upslope=upslope,
            
            n_sma=n_sma, n_lr1=n_lr1, n_lr2=n_lr2, 
            n_nbar=n_nbar, 
            moveup_level=moveup_level, # 表示 pnl > 4% 的时候, 就要上移tsl
            n_tsl=n_tsl, # 3表示设置sl的时候, 引用最近4期的low的最小值
            freq=freq,
            figsize=figsize,
            comm=comm,
            ))
        self.mparams = 'n_hhv n_llv n_median upslope'.split()
        self.p.update(kwargs)

        # super().__init__(**kwargs)
        
    def load(self):
        if self.dfd is None:
        # self.dfh, self.dfw = load_1h()
            # self.dfd = load_eod()
            self.dfd = get_index_eod(g.ticker)
        
    def ind(self):
        start = g.strptime(self.p['start']) - datetime.timedelta(days=450)
        end = self.p['end']
        
        df = self.dfd.loc[start:end, ].copy()
        
        df['weekday'] = df.index.isocalendar().day
        df['roc1'] = ttr.roc1(df.close)[0] #用作策略当日收益率的引用值
        df['close1'] = df.close.shift(1) #用于计算策略卖出日的收益
        df['tr'], _, __ = ttr.tr_(df)
        df['atr20'] = ttr.atr(df,20).shift(1)
        _ = ttr.ema(df.close, 2)
        df['ema'] = ttr.ema(_, 2).shift(1)
        df['trend'] = ttr.sma(df.close, self.p['n_sma']*4)
        
        high, low = df.high, df.low
        df['outa'] = ttr.hhv(df.high, 40) - (ttr.hhv(high,4)-ttr.llv(low,4))

        df['hhv'] = ttr.hhv(df.high, 10).shift(1)
        df['llv'] = ttr.llv(df.low, 10).shift(1)
        _ = (df.hhv + df.llv)/2  - df.atr20/2
        df['lline'] = ttr.ema(_, 2) # lifeline 生命线
        
        df['nmoney'] = load_nmoney()
        df['nmoney'] = df.nmoney.fillna(0)
        df['nmoney_cum'] = df.nmoney.cumsum()
        df['nmoney_ma'] = ttr.sma(df.nmoney_cum, self.p['n_sma'])
        df['moneyjc'] = ttr.upCross(df.nmoney_cum, df.nmoney_ma)
        df['moneysc'] = ttr.downCross(df.nmoney_cum, df.nmoney_ma)

        self.df = df.loc[self.p['start']: end].copy()
        self.plotter = CandlePlotter(self.df, self.p)
        
            
        
    def play(self, cook=False, 
             which='trueemajc', label='emajc', color='r'):
        '''
        
        '''
        self._ind()

        self._candles()
        # self._plot_trend(plot_mm=True)
        self._plot_trend(plot_mm=False)
        
        self._get_buysignal()
        self._plot_buysignal(which, label, color)
        print('\nSignal info: ', which, label, color)
        # self._plot_buysignal('trueema2', 'ema2', 'k')
        # self._plot_buysignal('truebuy', 'cdft', 'k')
        # self._plot_buysignal('truebuy_zsl', 'zsl', 'red')
        # self._plot_buysignal('truebuy_mjjc', 'mjjc', 'green')
         
        if cook:
            # self.cook_signal_zsxy(label)
            self.cook_signal(label)
            self.get_perf()
            self.print_transac()
            # self.print_pnl_desc()
            self.plot_tsl()        
        self.custom_fig() # self._subplots_adj()
        
        '''
        # self._plot_box_btn_cross()        
        
        '''
        


    def cook_signal(self, sname='emajc',
            ini_sl='ema',
            sl_ref='ema',
            sprice_ref='last_sl', 
        ):
        
        ''' 中轴线上拐头 进场, 低点破前低点出场
        也就是: 
            止损位跟踪收盘时/收盘后的k线低点
            卖出价 = sl = 锁定前k线的low, 
                判断: if row.low<sl
                
        ini_sl:     买入位置止损位/初始止损 low mml
        sprice_ref: 卖出价格锁定位置 'curr_close' last_sl
        sl_ref:     跟踪止损参考线,   'trend' 'mml' low
        '''
        self.tlog = Tlog(self.p)
        
        pos = self.tlog.pos;  df = self.df
        signal = getattr(self, sname); signal=signal.fillna(0)
        
        print(self.tlog.log_title)
        for i, (dt, row) in enumerate(df.iterrows()):
            if not pos:
                if signal[dt]: # signal comes into my room
                    # hqpat = sname
                    # sl = getattr(row, ini_sl)/1.0 # 设置初始止损
                    sl = df.ema[i+1]
                    pos = self.tlog.buy(i, dt, row, df, sl) # set pos = 1
                else:
                    pass
            elif pos:
                # 取tlog里的数据(昨天的)
                # sl = self.tlog.sl 
                sl = row.ema
                # 计算新的指标
                pnl = (row.close/self.tlog.bprice - 1) * 100; _=pnl
                
                # if row.close<row.mmh and row.open>row.mmh: 
                if False: # skit it
                    pos = self.tlog.sell(i, dt, row, df, sprice_ref) # set pos = 0
                
                # 收盘价向下击穿止损的情况
                # elif row.close<sl: # df里的sl: 当前日的止损位; 效果很不好
                # elif row.low<sl: # df里的sl: 当前日的止损位; 效果很不好
                elif row.close<row.ema: # df里的sl: 当前日的止损位; 效果很不好
                # elif self.zzxxgt[dt]: # df里的sl: 当前日的止损位; 效果很不好
                    # pos = self.tlog.sell(i, dt, row, df, 'curr_close') 
                    pos = self.tlog.sell(i, dt, row, df, 'row_ema') 

                # 日常盘整: 登记收盘时间戳下的头寸和止损
                else: 
                    self.tlog.register(i, dt, row, df, 'row_ema')
        
        self._get_equity()



    def _find_event(self,   *cond_args, label='dingfx', name='顶分形',):
        event = cond_args[0]
        for cond in cond_args[1:]:        
            event = event & cond
        # event = all(cond_args)   # a & b & c & d     & e
        if  True  in event.value_counts().index:
            print(f'\n "{name}"信号次数: ', event.value_counts()[True])
            self.df[label] = event




    def find_event(self,  label='dingfx', name='顶分形'):
        df=self.df
        h,l = df.high, df.low
        
        a = h<h.shift(1)
        b = l<l.shift(1)
        c = h.shift(1)>h.shift(2)
        d = l.shift(1)>l.shift(2)
        e = h.shift(1) > df.hhv.shift(1)
        args = (a,b,c,d,e)
        self._find_event(*args, label=label, name=name)


    def find_event_difx(self,  ):
        df=self.df
        h,l = df.high, df.low
        
        a = h > h.shift(1)
        b = l > l.shift(1)
        c = h.shift(1) < h.shift(2)
        d = l.shift(1) < l.shift(2)
        e = l.shift(1) < df.llv.shift(1)
        args = (a,b,c,d,e)
        self._find_event(*args, label='difx', name='底分形')



    def find_event_(self,  event_label='dfx', event_name='顶分形'):
        df=self.df
        h,l = df.high, df.low
        
        a = h<h.shift(1)
        b = l<l.shift(1)
        c = h.shift(1)>h.shift(2)
        d = l.shift(1)>l.shift(2)
        e = h.shift(1) > df.hhv.shift(1)
        
        event = a & b & c & d     & e
        if  True  in event.value_counts().index:
            print(f'\n "{event_name}"信号次数: ', event.value_counts()[True])
            self.df[event_label] = event




    def get_bsig_group(self):
        '''仅提取买入信号分组, 需要在get_signal()方法之后才可以调用  '''
        try:
            out = self.bsig.groupby('sig').get_group(True)
            return out
        except Exception as e:
            print('抛出异常: ',  e)
            print('你应该先调用self.get_signal(0)方法来提取交易信号')
            
    def get_ssig_group(self):
        return self.ssig.groupby('sig').get_group(True)

    def get_signal(self, only_evevt=True):
        ''' 
        提取特征事件之交易信号
        输出: 
            - self.df里添加了两列: 金叉事件'jc', 死叉事件'sc' 
            - 可选地, self里添加两个属性: bsig 和 ssig , df类型
        
        周k线的操作信号,
        行模式获取买入信号, 比较高效, 但是缺乏细腻
        周线数据属于低频数据, 其信噪比非常高, 
        均线稳定, k线与均线之间的关系也很明确
        应该能寻找出比较可靠的交易信号.
        上沿: 红线
        下沿: 蓝线
        趋势线: 绿线
        '''
        df=self.df
        # print('\nbandwidth 简述:\n', self.df_ha.bw1.describe())
        
        # 1. 均线和收盘线都在趋势线之下, 发生金叉:   ==>超跌反弹
        # 2. 均线和收盘线都在趋势线之上,  发生金叉:   ==>主升浪
        # 3. 均线在趋势线之下但是收盘线在趋势线之上,  发生金叉: ==>密集金叉 ==> 牛熊转势特征
        # 前期为疯熊的话, 大概率为 空头陷阱
        # 前期为疯牛的话, 大概率为 多头陷阱

        # hdark: high dark 高位的阴线(heikin ashi蜡烛线的黑线/阴线)
        # 前期一直是上涨, ha线一直是白色的大实体, 越过了wh(前一个周线的高点), 
        # 最近两三根ha线变成了小实体+上下影线, 现在是黑色实体, 
        # 但是收盘价依然在wh之上, 
        # ==>下跌的前兆
        
# =============================================================================
#         a = df.high > df.hhv 
#         a1 = a.shift(3)
#         b = df.high < df.hhv 
#         b1 = b.shift(1)
#         b2 = b.shift(2)
#         cxgyhd3t = a1 & b & b1 & b2
#         if  True  in cxgyhd3t.value_counts().index:
#             print('\n cxgyhd3t"创新高以后第三天"信号次数: ', cxgyhd3t.value_counts()[True])
#             self.df['cxgyhd3t'] = cxgyhd3t
# =============================================================================
            
            
        self.df['jc'] = ttr.upCross(df.close, df.lline)
        self.df['sc'] = ttr.downCross(df.close, df.lline)
        print('\n jc"收盘价金叉生命线"信号次数: ', self.df.jc.value_counts()[True])
        print('\n sc"收盘价死叉生命线"信号次数: ', self.df.sc.value_counts()[True])
        
        # cutw: crossUp trend and white block
        # 金叉趋势线并且ha线已经由黑体变成了白体
        # 前期是一波下跌, 向下击穿了wh, 
        # ha线一直是黑色实体, 
        # 在向下试探支撑的过程中, 可能会有四条路线:
        # 1. hwhite
        #       trbt: take a rest at just below wh/top 在上沿下方歇歇脚
        #       
        # 2. 在wt附近或者wh和wl的中点线附近形成中枢横盘, 
        #       此时ha线表现为小实体+长上下影线 
        #       ==>持观望态度(有股时持股, 应该无操作)
        # 3. twhite   
        #       继续下行跌穿wt来到wl附近或者在wl的上沿处得到支撑
        #       最近两三根ha线变成了小实体+上下影线, 现在是白色色实体, 
        #       但是收盘价依然在wl之上, 
        #       ==>下涨的前兆
        # 4.    继续下行击穿wl/下沿
        #       ==> 上涨趋势已经彻底改变了. 到了不可操作区. 
        #       ==> 继续深跌之后可能有反弹行情. 也只能半仓快进快出了
        

        if not only_evevt:        
            bsignal = self._get_signal('jc')
            ssignal = self._get_signal('sc')
            self.bsig, self.ssig = bsignal, ssignal


    
    def get_signal_nmoney(self, only_evevt=True):
        ''' 提取'北向资金'的开仓/平仓信号, 其类型为df, cols='sig stype' 
        '''
        df=self.df
        print('\n moneyjc"北向资金流入金叉"信号次数: ', df.moneyjc.value_counts()[True])
        print('\n moneysc"北向资金流入死叉"信号次数: ', df.moneysc.value_counts()[True])
            
        if not only_evevt:        
            bsignal = self._get_signal('moneyjc')
            ssignal = self._get_signal('moneysc')
            self.bsig, self.ssig = bsignal, ssignal


    def _add_signal_to_df(self, signal, stype='bwhite'):        
        if stype in self.df:
            signal['sig'] = getattr(self.df, stype) | getattr(signal, 'sig') 
            str_arr = np.where(getattr(self.df, stype), stype, '')
            signal['stype'] += str_arr
        return signal
        
    def _get_signal(self, stype:str):
        ''' '''
        signal = pd.DataFrame(index=self.df.index, columns='sig stype'.split())
        signal.stype = '' #bsignal.stype.astype(str) # change signal_type datatype as str
        signal = self._add_signal_to_df(signal, stype)
        return signal

    
    def simplest_bt(self, print_log=True, bprice_ref='close', sprice_ref='close'):
        pos=0
        bp=0; sp=0; sellbar=0; allow_enter_bar=10; _=allow_enter_bar
        trans, srec, pos_l, sroc1 = [],[], [], []
        zips = zip(self.bsig.index, self.df.close, self.bsig.values,self.ssig.values)
        
        print('\n开始简单回测 --------')
        print('sn  dt                  close   bsig     btype    ssig     stype     pnl')
        print('--'*30)
        for i, (dt, c0, (bs, bt), (ss, st)) in enumerate(zips):
            if not pos:
                if bs: # and i>=sellbar+allow_enter_bar :
                    pos=1; pos_l.append((dt, pos))
                    bp=c0  if bprice_ref=='close' else self.df['lline'].iloc[i]
                    t = (dt, 'buy', bp, 0)
                    trans.append(t)
                    # _=-self.p['comm']
                    _ = c0/bp - 1 - self.p['comm']
                    sroc1.append((dt, _))
                    if print_log:
                        print('{:>3d} {} {:.2f} {:8s} {:8s} {:8s} {:8s}'.format(
                        i, dt, c0, str(bs),bt,str(ss),st))
            else:
                if ss:
                    sellbar = i; _=sellbar
                    pos=0; pos_l.append((dt, pos))
                    sp=c0 if sprice_ref=='close' else self.df['lline'].iloc[i]
                    pnl = sp/bp -1
                    t = (dt, 'sell', sp, pnl)
                    trans.append(t)
                    srec_ = (dt, bp, sp, pnl)
                    srec.append(srec_) 
                    _ = sp/self.df.close[i-1] - 1 - self.p['comm']
                    sroc1.append((dt, _))
                    if print_log:
                        print('{:>3d} {} {:.2f} {:8s} {:8s} {:8s} {:8s} {:.2%}'.format(
                        i, dt, c0, str(bs),bt,str(ss),st,pnl))
        
        self.pos_l, self.sroc1, self.srec, self.trans = \
            pos_l, sroc1, srec, trans
        self.get_equity()

    def get_equity(self):        
        pos = pd.DataFrame(self.pos_l, columns='dt pos'.split()).set_index('dt')
        self.df['pos'] = pos
        self.df.pos=self.df.pos.ffill()
        
        # 计算策略的每日收益率: sroc1
        # 考虑到了佣金, 并剔除了进出场时的两个bar的原始roc1
        sroc1_bs = pd.DataFrame(self.sroc1, columns='dt sroc1'.split()).set_index('dt')
        self.df['sroc1_bs'] = sroc1_bs
        sroc1_ = self.df.pos * self.df.roc1
        self.df['sroc1'] = np.where(
            self.df.sroc1_bs.fillna(False), self.df.sroc1_bs, sroc1_)
        self.df['sroc1'] = self.df.sroc1.fillna(0)
        
        self.df['equity'] = self.df.sroc1.add(1).cumprod() * self.df.close[0] 

        if hasattr(self.plotter, 'ax'):
            self.plotter._plot_equity()
# =============================================================================
#             self.df['equity'].plot(
#                 ax=self.plotter.ax, use_index=False, 
#                 color='m', marker='o', markersize=4,
#                 label='equity({:.2f}) vs bm({:.2f})'.format(
#                     self.df.equity[-1]/self.df.equity[0],
#                     self.df.close[-1]/self.df.close[0],
#                     ),
#                 legend=True,
#             )
# =============================================================================
        
        
        
        self.srec_df = pd.DataFrame(self.srec, columns='dt bp sp pnl'.split()).set_index('dt')
        
        self.trans_df = pd.DataFrame(self.trans, columns='dt operation price pnl'.split()).set_index('dt')
        self.nav = self.trans_df.pnl.add(1).cumprod()
        print('nav:{:.2f}, benchmark nav:{:.2f}'.format(self.nav[-1], self.df.close[-1]/self.df.close[0] ))
        
        # fig,ax=plt.subplots()    
        # self.df.roc1.add(1).cumprod().plot(ax=ax)
        # self.nav.plot(ax=ax, marker='o')        
        # (self.nav/1.001).plot(ax=ax, marker='^')        

    def _get_equity(self):
        df = self.df_ha.copy()
        pos_l = pd.DataFrame(self.tlog.pos_l, columns='dt pos'.split()).set_index('dt').shift(1)
        df['pos'] = pos_l # index will be aligned  to df.index
        df['pos'] = df.pos.fillna(0)
        # df['equity'] = ((df.roc1*df.pos) + 1).cumprod()*df.close[0] 
        
        sroc1 = pd.DataFrame(self.tlog.sroc1_l, columns='dt sroc1'.split()).set_index('dt')
        df['sroc1'] = sroc1
        df['sroc1'] = df.sroc1.fillna(0)
        df['equity'] = ((df.sroc1*df.pos) + 1).cumprod()*df.close[0] 
        self.df = df

    def show_event(self, bsig=None, ssig=None, annotate=False):
        # show week start bar tickmark,  show week end bar tickmark
        # uplot.vlines (self.plotter.ax, self.df_ha.iswendbar, self.df_ha.wh ,'red')
        # uplot.vlines (self.plotter.ax, self.df.cxgyhd3t, self.df.hhv ,'red')
        uplot.vlines (self.plotter.ax, getattr(self.df, bsig), self.df.llv, 'green', position='below')
        uplot.vlines (self.plotter.ax, getattr(self.df, ssig), self.df.hhv, 'red', position='above')
        
        # uplot.annotate_event(self.plotter.ax, self.df_ha.iswstartbar, self.df_ha.wh, self.df_ha.bw1, 'wbw', arrow_color='red')
        if annotate:
            uplot.annotate_event(self.plotter.ax, getattr(self.df, bsig), self.df.llv, 
                    self.df.close, 'bs', text_position='below', 
                    text_format='.0f', arrow_color='red')

            uplot.annotate_event(self.plotter.ax, getattr(self.df, ssig), self.df.hhv, 
                    self.df.close, 'ss', text_position='above',
                    arrow_color='purple', text_format='.0f')
    

    def make_subplot(self, length=100, plot_signal=False, plot_equity=False, savefig=False):
        for i in range(0, len(self.df), length):
            # print(i)
            self.subplot=CandlePlotter(self.plotter.df.iloc[i:i+length,], self.plotter.p)
            self.subplot._candles()
            self.subplot._plot_trend()
            if plot_signal:
                self.subplot._plot_signal(bsig='jc', ssig='sc')
            if plot_equity:
                self.subplot._plot_equity()
                pass
            if savefig:
                date_ = self.subplot.df.index[0].date().isoformat()
                fname = f'{g.k1[:6]}_{date_}.png'  
                self.subplot.fig.savefig(fname)
    
    def weekday_effect(self):        
        fig,ax=plt.subplots()
        gb = self.df.roc1.groupby(self.df.weekday)
        for x in range(1, 5+1):
            lbl = f'wk{x}'
            self.df[lbl] = gb.get_group(x).add(1).cumprod()
            self.df[lbl] = self.df[lbl].fillna(method='ffill')
            self.df[lbl].plot(ax=ax, label=lbl+f'({self.df[lbl][-1]:.2f})', legend=True, use_index=False)
        self.df.roc1.add(1).cumprod().plot(ax=ax, use_index=False, label='benchmark', legend=True)
        

#%% utest
        
def utest(n_sma=5):
    # from study.tlog import Tlog
    # from study.trade_chart_hilo import CandlePlotter

    class LifelineStrat(BaseStrategy):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
    
    g.df = get_index_eod(g.ticker)
    start,end= '2020-1-1', '2020-12-31'; 
    self = LifelineStrat(df=g.df, start=start, end=end, n_sma=n_sma)
    self.load()
    self.ind()
    self.plotter._candles()
    self.plotter._plot_trend()     
    
    self.get_signal(0)
    self.show_event(bsig='jc', ssig='sc')    
    # self.simplest_bt(print_log=False, )
    # self.simplest_bt(print_log=False,  bprice_ref='lline')
    # self.simplest_bt(print_log=False, bprice_ref='lline', sprice_ref='lline')
    # self.simplest_bt(print_log=0, bprice_ref='lline', sprice_ref='lline')
    self.simplest_bt(print_log=1, bprice_ref='lline', sprice_ref='lline')


    print(f"策略参数n_sma={self.p['n_sma']}")
    print('策略绩效-------')
    ttr.perf_measure(self.df.equity, with_print=True)
    print('BnH绩效-------')
    ttr.perf_measure(self.df.close, with_print=True)
 
    return self

# Here is the main entry:
# self = utest(5)    
# for n in range(3,15, 1): self= utest(n)       
# self= utest(cook=False)       
        



posted @ 2021-12-13 22:59  duanqs  阅读(78)  评论(0编辑  收藏  举报