只是搬运工【点天灯】

策略分享
标签: #<Tag:0x00007fcf607d6900>

(hugo) #1
克隆策略

所谓点天灯,是老时候赌场里的一种说法,其实应该叫“点灯”,是一种赌博的技巧,意思是如果发现赌台上有人手气非常不好,就反着他押,他押大你就押小,他押闲你就押庄,赌的不是自己的运气而是他人的霉气,这个手气不好的人,就是你的“灯”。有些人天生运气差,逢赌必输,还会专门被人请去“点灯”,小输搏大利。(出自《盗墓笔记》)

首先定义一下这个股票中的“天灯”,我想找出一支股票,它与其他股票两两相对之后得出的correlation是所有负相关中绝对值最大的,同时在股票池中它也负影响了最多支股票,并且在每支被影响股票的千百个correlation中,它们相对应的负相关系数绝对值也是最大的。

一句话来说,这支也可以被称作是市场的晴雨表,它涨大盘跌,它跌大盘强势。我这里做了一份深证2012年度的一份研究结果,阐述了一下怎么找到那支特殊的股票,然后我写了一个算法,回测了从2012年到2017年的结果

算法大概:以一年为一个窗口,找出前一年的那支股票(衰神),在今年以它作为参考指标,它的MACD线一旦下降,开仓买进对应的会被影响的多支股票(资产组合),它的MACD线一旦上升或者它与对应的股票之间的correlation大于了-0.5,就清仓对应的股票。(也加入一点权重的东西) 然后到了下一年,清仓所有股票,然后又开始以上一年作为sample,找出那支天灯衰神股票,重复上面的步骤。

结果大概是,2012到2014年末,这个策略都是很失败的,一直都是below the zero line,虽然对应的benchmark沪深300不是那么强势,甚至还有点低迷。 分水岭在2015年,伴随着大盘走强,策略开始收益,一直稳步上升,最终有39%的回测收益。做到这里我在怀疑是不是因为2015大盘涨势不错所以策略才起效果, 因此我专门针对2015-2017做了回测,结果证明,尽管是使用前一年中的那支(衰神)股票作为参考风向标,但是在测试中,策略收效很不错(39%),而且干翻了benchmark沪深300。

怎么说呢,我觉得所谓的“点天灯”现象可能存在,但是可能出现在大盘涨势很强或者下跌很严重的时候,因为只有股票市场走势比较大的时候,相关系数才能更有影响力,在2012至2014年的窗口中,我们可以看到沪深300的走势幅度并不大,如果对比2015年后的情况的话。

In [1]:
import re
import collections
import talib

以下是回测部分

自定义的函数,方便回测

In [2]:
#前期工作
#--------------------------get_date_by_days-----------------------------------
def get_near_date(date,direction):
    td = D.trading_days(market='CN', start_date=None, end_date=None)
    td['date'] = td['date'].apply(lambda x:x.strftime('%Y-%m-%d'))
    while True:
        if direction == 0:
            pre_date = datetime.datetime.strptime(date,'%Y-%m-%d') - datetime.timedelta(1)
        elif direction == 1:
            pre_date = datetime.datetime.strptime(date,'%Y-%m-%d') + datetime.timedelta(1)
        pre_date = pre_date.strftime('%Y-%m-%d')
        date = pre_date
        if pre_date in list(td['date']):
            break
    return date
#查找date在td中的位置
def get_date_index(date,direction):
    td = D.trading_days(market='CN', start_date=None, end_date=None)
    td['date'] = td['date'].apply(lambda x:x.strftime('%Y-%m-%d'))
    try:
        ix = td[td['date']== date].index[0]
    except IndexError as e:
        date = get_near_date(date,direction)
        ix = td[td['date']== date].index[0]
    return ix

def get_date_range(ix,direction,days):
    td = D.trading_days(market='CN', start_date=None, end_date=None)
    if direction == 0: 
        start_ix = ix-days+1#用get_date_index取得的index位置获取
        assert len(td.ix[start_ix:ix]) == days #assert断言函数,如果不为真则报错
        start_date = td.ix[start_ix:ix]['date'].min()
        end_date = td.ix[start_ix:ix]['date'].max()
    elif direction ==1:
        end_ix = ix+days-1
        assert len(td.ix[ix:end_ix]) == days
        start_date = td.ix[ix:end_ix]['date'].min()
        end_date = td.ix[ix:end_ix]['date'].max()
    return (start_date, end_date)

def get_history_data_by_days(instruments,date,days,direction,fields):
    ix = get_date_index(date, direction)
    start_date = get_date_range(ix,direction,days)[0]
    end_date = get_date_range(ix,direction,days)[1]
    return D.history_data(instruments=instruments,start_date=start_date,end_date=end_date,fields=fields)
#----------------------------END-------------------------------------

#获取目标股和股票清单
def get_data(start_date,end_date):
    import re
    import collections
    instruments=D.instruments(start_date=start_date,end_date=end_date,market='CN_STOCK_A')
    df=pd.DataFrame(list(map(lambda x:re.split('[\.]',x),instruments)),columns=['code','filter'])
    df['cn_a']=pd.Series(instruments,index=df.index)
    all_order_list=df[df['filter']=='SZA']['cn_a'].values.tolist()#取得深证的股票
    #计算correlation
    text=D.history_data(instruments=all_order_list,start_date=start_date,end_date=end_date,fields='close')
    testall=pd.pivot_table(text,values='close',index=['date'],columns=['instrument'])
    testall=testall.dropna(axis=1,how='any')
    ndf=testall.corr()
    #这种处理将A:A配对的这种相关系数的处理成0值
    ndf=ndf-np.diag(np.diag(ndf))#np.diag(np.diag)是取对角线
    ndf=ndf.fillna(0)
    #构建负相关系数表格(cov-:负相关系数,stock-:来自哪只股票的影响)
    vv=[]
    for i in ndf.columns:
        min_id=ndf.loc[i,:].idxmin()
        vv.append([i,ndf.loc[i,min_id],min_id])
        f_cor=pd.DataFrame(vv,columns=['instruments','cov-','stock-'])
        f_cor=f_cor.set_index('instruments')
    #找出目标股
    a=f_cor['stock-'].tolist()
    targetstock=max(a,key=a.count)#目标股票
    df=f_cor[(f_cor['stock-']==targetstock)&(f_cor['cov-']<-0.5)]#受影响股票与目标股票的相关系数小于-0.5
    return targetstock,df.index.tolist()


#生成daliy_stock_list
def filter_df(start_date,end_date,target,stock_list,window):
    stock_df=D.history_data(s1,start_date,end_date,'close',price_type='original')
    stock_df=stock_df.pivot_table(values='close',index=['date'],columns=['instrument'])
    target_df=D.history_data(target,start_date,end_date,'close',price_type='original')
    target_df=target_df.set_index('date')
    cor_df=stock_df.rolling(window).corr(target_df['close'])
    vv=[]
    for r in cor_df.index:
        for c in cor_df.columns:
            vv.append([r,c,cor_df.loc[r,c]])
    return pd.DataFrame(vv,columns=['date','instrument','cor'])


#get_data_by_days获取的cons
def count_cons(data,targetstock,stock_list):
    cons=len(stock_list)
    start_date=data.current_dt.strftime('%Y-%m-%d')
    for i in stock_list:
        test=get_history_data_by_days(i,start_date,365,0,'close')['close'].values
        test1=get_history_data_by_days(targetstock,start_date,365,0,'close')['close'].values
        s1=pd.Series(test)
        s2=pd.Series(test1)
        covnum=s1.corr(s2)
        if  covnum >= -0.5:
            cons=cons-1
    return cons


#计算当日复核的股票数量
def count_n(data,targetstock,stock_list):
    cons=len(stock_list)
    for i in stock_list:
        test=data.history(symbol(i),'close',200, '1d')
        test1=data.history(symbol(targetstock),'close',200, '1d')
        s1=pd.Series(test)
        s2=pd.Series(test1)
        covnum=s1.corr(s2)
        if  covnum >= -0.5:
            cons=cons-1
    return cons

第一版

调用data.history 最开始第一版回测结果,收益率为-9%,差点放弃,但是因为回测是报错说没有数据,所以怀疑是数据造成的,看到社区有相同错误,说是data.history数据只有252天的,且不稳定
所以又做了第二版
慎用data.history!

In [3]:
#--------------------------------回测--------------------------------------------

start_date = '2015-01-01'
end_date = '2017-11-21'
benchmark = '000300.INDX'
instruments =D.instruments(start_date=start_date,end_date=end_date)
# 起始资金
capital_base = 100000
def initialize(context):
    set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
    #macd参数
    context.SHORTPERIOD = 12
    context.LONGPERIOD = 26
    context.SMOOTHPERIOD = 9
    
    context.OBSERVATION = 100
    schedule_function(func=rebalance, date_rule=date_rules.month_end())
    
    context.target1,context.s1=get_data('2014-01-01','2014-12-31')
    print('target1:'+context.target1)

def rebalance(context,data):
    u = data.current_dt
    if  (u.day, u.month, u.year)==(31,12,2015) or (u.day, u.month, u.year)==(30,12,2016):
        start_date=datetime.datetime(year=u.year - 1, month = u.month, day = u.day).strftime('%Y-%m-%d')
        end_date=datetime.datetime(year=u.year, month = u.month, day = u.day-1).strftime('%Y-%m-%d')
        context.target2,context.s2=get_data(start_date,end_date)
        print('target2:'+context.target2)

def before_trading_start(context,data):
    u = data.current_dt
    if  (u.year)==(2016) or (u.year)==(2017) :
        targetstock = context.target2
        stock = context.s2
    else:
        targetstock = context.target1
        stock = context.s1
    context.cons=count_n(data,targetstock,stock)
    
def handle_data(context, data):
    u = data.current_dt
    if  (u.year)==(2016) or (u.year)==(2017):
        targetstock = context.target2
        stock = context.s2
    else:
        targetstock = context.target1
        stock = context.s1
        
    for i in stock:
        test=data.history(symbol(i),'close', 200, '1d')
        test1=data.history(symbol(targetstock),'close',200, '1d')
        s1=pd.Series(test) 
        s2=pd.Series(test1)
        covnum=s1.corr(s2)
        prices1 = data.history(symbol(targetstock), 'close', context.OBSERVATION, '1d')
        macd1, signal1, hist1 = talib.MACD(prices1.fillna(0).values, context.SHORTPERIOD,context.LONGPERIOD, context.SMOOTHPERIOD)            
        if  covnum <= -0.5 and context.cons != 0 and macd1[-1] - signal1[-1] < 0 and macd1[-2] - signal1[-2] > 0:
            context.order_target_percent(symbol(i), 1/context.cons)
        if  (macd1[-1] - signal1[-1] > 0 and macd1[-2] - signal1[-2] < 0) or covnum > -0.5:
        # 获取该股票的仓位
            curPosition = context.portfolio.positions[symbol(i)].amount
        # 清仓
            if curPosition > 0:
                context.order_target_value(symbol(i), 0)
    if len(prices1.dropna())<1:
            print('目标股无数据'+str(u)+':'+targetstock)
m = M.trade.v2(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    before_trading_start=before_trading_start,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)
[2017-12-24 22:15:41.002732] INFO: bigquant: backtest.v7 开始运行..
[2017-12-24 22:15:41.006850] INFO: bigquant: 命中缓存
  • 收益率-9.12%
  • 年化收益率-3.36%
  • 基准收益率19.36%
  • 阿尔法-0.08
  • 贝塔0.07
  • 夏普比率-0.59
  • 收益波动率13.29%
  • 信息比率-0.35
  • 最大回撤21.66%
[2017-12-24 22:15:42.665221] INFO: bigquant: backtest.v7 运行完成[1.662461s].
In [4]:
#放弃data.history使用dataframe这种模式

#--------------------------------准备数据----------------------------------------
#-------------------------------daliy_to_buy------------------------------------
#2015年的stock_list
target1,s1=get_data('2014-01-01','2014-12-31')#使用2014年的数据选择目标股
stock_list1=filter_df('2013-01-01','2015-12-31',target1,s1,365)
stock_list1=stock_list1.set_index('date').dropna()
stock_list1=stock_list1['2015-01-01':]

#2016年的stock_list
target2,s2=get_data('2015-01-01','2015-12-31')#使用2015年的数据选择目标股
stock_list2=filter_df('2014-01-01','2016-12-31',target2,s2,365)
stock_list2=stock_list2.set_index('date').dropna()
stock_list2=stock_list2['2016-01-01':]

#2017年的stock_list
target3,s3=get_data('2016-01-01','2016-12-31')#使用2016年的数据选择目标股
stock_list3=filter_df('2015-01-01','2017-12-20',target3,s3,365)
stock_list3=stock_list3.set_index('date').dropna()
stock_list3=stock_list3['2017-01-01':]

stock_con=pd.concat([stock_list1,stock_list2,stock_list3])#合并
stock_con['date']=stock_con.index
#--------------------------------indicator数据----------------------------------
#2015年的indicator
prices1=D.history_data(target1,'2014-01-01','2015-12-31','close',price_type='forward_adjusted')
macd1, signal1, hist1 = talib.MACD(np.array(prices1.close.values.astype('double')),12,26, 9)
indicator1=pd.DataFrame()                 
indicator1['macd']=macd1
indicator1['signal']=signal1
indicator1['macd1']=indicator1.macd.shift(1)
indicator1['signal1']=indicator1.signal.shift(1)
indicator1.index=prices1.date
indicator1=indicator1['2015-01-01':]
#2016年的indicator
prices2=D.history_data(target2,'2015-01-01','2016-12-31','close',price_type='forward_adjusted')
macd2, signal2, hist2 = talib.MACD(np.array(prices2.close.values.astype('double')),12,26, 9)
indicator2=pd.DataFrame()                 
indicator2['macd']=macd2
indicator2['signal']=signal2
indicator2['macd1']=indicator2.macd.shift(1)
indicator2['signal1']=indicator2.signal.shift(1)
indicator2.index=prices2.date
indicator2=indicator2['2016-01-01':]

#2017年的indicator
prices3=D.history_data(target3,'2016-01-01','2017-12-20','close',price_type='forward_adjusted')
macd3, signal3, hist3 = talib.MACD(np.array(prices3.close.values.astype('double')),12,26, 9)
indicator3=pd.DataFrame()                 
indicator3['macd']=macd3
indicator3['signal']=signal3
indicator3['macd1']=indicator3.macd.shift(1)
indicator3['signal1']=indicator3.signal.shift(1)
indicator3.index=prices3.date
indicator3=indicator3['2017-01-01':]
indicator_con=pd.concat([indicator1,indicator2,indicator3])#合并
indicator_con['date']=indicator_con.index
#--------------------------------回测--------------------------------------------

start_date = '2015-01-01'
end_date = '2017-11-21'
benchmark = '000300.INDX'
instruments =D.instruments(start_date=start_date,end_date=end_date)
# 起始资金
capital_base = 100000
def initialize(context):
    set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
    #macd参数
    context.SHORTPERIOD = 12
    context.LONGPERIOD = 26
    context.SMOOTHPERIOD = 9
    
    context.OBSERVATION = 100
    schedule_function(func=rebalance, date_rule=date_rules.month_end())
    context.indicator=indicator_con
    context.target1,context.s1=get_data('2014-01-01','2014-12-31')
    print('target1:'+context.target1)

def rebalance(context,data):
    u = data.current_dt
    if  (u.day, u.month, u.year)==(31,12,2015) or (u.day, u.month, u.year)==(30,12,2016):
        start_date=datetime.datetime(year=u.year - 1, month = u.month, day = u.day).strftime('%Y-%m-%d')
        end_date=datetime.datetime(year=u.year, month = u.month, day = u.day-1).strftime('%Y-%m-%d')
        context.target2,context.s2=get_data(start_date,end_date)
        print('target2:'+context.target2)

    
def handle_data(context, data):
    u = data.current_dt
    
    cor_df=stock_con[stock_con.date==u.strftime('%Y-%m-%d')]
    cor_df=cor_df.set_index('instrument')
    cons=len(cor_df)#买入个数
    macd1=context.indicator.loc[u.strftime('%Y-%m-%d'),'macd']
    signal1=context.indicator.loc[u.strftime('%Y-%m-%d'),'signal']
    macd2=context.indicator.loc[u.strftime('%Y-%m-%d'),'macd1']
    signal2=context.indicator.loc[u.strftime('%Y-%m-%d'),'signal1']
    
    if  (u.year)==(2016) or (u.year)==(2017):
        targetstock = context.target2
    else:
        targetstock = context.target1
    
    for i in cor_df.index:
        covnum=cor_df.loc[i,'cor']
        if  covnum <= -0.5 and cons != 0 and macd1 - signal1< 0 and macd2 - signal2 > 0:
            context.order_target_percent(symbol(i), 1/cons)
            if  (macd1 - signal1 > 0 and macd2 - signal2 < 0) or covnum > -0.5:
            # 获取该股票的仓位
                curPosition = context.portfolio.positions[symbol(i)].amount
            # 清仓
                if curPosition > 0:
                    context.order_target_value(symbol(i), 0)
m = M.trade.v2(
    instruments=instruments,
    start_date=start_date,
    end_date=end_date,
    initialize=initialize,
    handle_data=handle_data,
    # 买入订单以开盘价成交
    order_price_field_buy='open',
    # 卖出订单以开盘价成交
    order_price_field_sell='open',
    capital_base=capital_base,
    benchmark=benchmark,
)
[2017-12-24 22:21:08.794785] INFO: bigquant: backtest.v7 开始运行..
[2017-12-24 22:21:08.797343] INFO: bigquant: 命中缓存
  • 收益率39.12%
  • 年化收益率12.55%
  • 基准收益率19.36%
  • 阿尔法0.06
  • 贝塔0.9
  • 夏普比率0.25
  • 收益波动率32.18%
  • 信息比率0.28
  • 最大回撤51.57%
[2017-12-24 22:21:12.182078] INFO: bigquant: backtest.v7 运行完成[3.387272s].