【宽客学院】选股+择时策略组合

择时
选股
宽客学院
标签: #<Tag:0x00007f5bf535f108> #<Tag:0x00007f5bf535efc8> #<Tag:0x00007f5bf535ee88>

(iQuant) #1
作者:bigquant
阅读时间:10分钟
本文由BigQuant宽客学院推出,难度标签:☆☆☆☆

导语:本文讨论交易中两个非常重要的命题:选股+择时,并将其两者结合起来开发策略。


选股就是要选一只好股票,而择时就是选一个好的买卖时机。如果投资者选了一只很差劲的股票,无论怎样择时操作,都无法获得超额的利润。但如果只选股而不进行择时,又可能面临系统性风险爆发的困境。如何将选股和择时策略有机地结合起来?

本文列出两个策略,第一个策略为纯选股策略,没有择时,即任何时候都有股票仓位。第二个策略为选股+择时,当大盘处于死叉(短期均线下穿长期均线)的时候就保持空仓。对比发现,有择时的策略资金曲线更为平滑。

克隆策略
In [2]:
def prepare(context):
    # 获取股票市净率数据,返回DataFrame数据格式
    market_cap_data = D.history_data(context.instruments,context.start_date,context.end_date,
              fields=['pb_lf','amount'])
    # 获取每日按市净率排序 (从低到高)的前三十只股票
    context.daily_buy_stock = market_cap_data.groupby('date').apply(lambda df:df[(df['amount'] > 0)&((df['pb_lf'] > 0))].sort_values('pb_lf')[:30])

纯选股策略

In [4]:
# 回测参数设置,initialize函数只运行一次
def initialize(context):
    # 手续费设置
    context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5)) 
    # 调仓规则(每月的第一天调仓)
    context.schedule_function(rebalance, date_rule=date_rules.month_start(days_offset=0)) 
    
# handle_data函数会每天运行一次
def handle_data(context,data):
    pass

# 换仓函数
def rebalance(context, data):
    # 当前的日期
    date = data.current_dt.strftime('%Y-%m-%d')
    # 根据日期获取调仓需要买入的股票的列表
    stock_to_buy = list(context.daily_buy_stock.ix[date].instrument)
    # 通过positions对象,使用列表生成式的方法获取目前持仓的股票列表
    stock_hold_now = [equity.symbol for equity in context.portfolio.positions]
    # 继续持有的股票:调仓时,如果买入的股票已经存在于目前的持仓里,那么应继续持有
    no_need_to_sell = [i for i in stock_hold_now if i in stock_to_buy]
    # 需要卖出的股票
    stock_to_sell = [i for i in stock_hold_now if i not in no_need_to_sell]
  
    # 卖出
    for stock in stock_to_sell:
        # 如果该股票停牌,则没法成交。因此需要用can_trade方法检查下该股票的状态
        # 如果返回真值,则可以正常下单,否则会出错
        # 因为stock是字符串格式,我们用symbol方法将其转化成平台可以接受的形式:Equity格式

        if data.can_trade(context.symbol(stock)):
            # order_target_percent是平台的一个下单接口,表明下单使得该股票的权重为0,
            #   即卖出全部股票,可参考回测文档
            context.order_target_percent(context.symbol(stock), 0)
    
    # 如果当天没有买入的股票,就返回
    if len(stock_to_buy) == 0:
        return

    # 等权重买入 
    weight =  1 / len(stock_to_buy)
    
    # 买入
    for stock in stock_to_buy:
        if data.can_trade(context.symbol(stock)):
            # 下单使得某只股票的持仓权重达到weight,因为
            # weight大于0,因此是等权重买入
            context.order_target_percent(context.symbol(stock), weight)
            
m = M.trade.v2( 
    instruments=D.instruments(),
    start_date='2013-01-01' , 
    end_date='2017-08-10' ,
    # 必须传入initialize,只在第一天运行
    initialize=initialize,
    #  必须传入handle_data,每个交易日都会运行
    handle_data=handle_data,
    prepare=prepare,
    # 买入以开盘价成交
    order_price_field_buy='open',
    # 卖出也以开盘价成交
    order_price_field_sell='open',
    # 策略本金
    capital_base=1000000,
    # 比较基准:沪深300
    benchmark='000300.INDX',
)
[2018-04-28 16:53:51.313446] INFO: bigquant: backtest.v7 开始运行..
[2018-04-28 16:54:08.368343] INFO: algo: set price type:backward_adjusted
[2018-04-28 16:55:24.922540] INFO: Performance: Simulated 1119 trading days out of 1119.
[2018-04-28 16:55:24.924219] INFO: Performance: first open: 2013-01-04 01:30:00+00:00
[2018-04-28 16:55:24.925901] INFO: Performance: last close: 2017-08-10 07:00:00+00:00
  • 收益率205.96%
  • 年化收益率28.64%
  • 基准收益率47.28%
  • 阿尔法0.2
  • 贝塔0.93
  • 夏普比率0.91
  • 胜率0.631
  • 盈亏比3.939
  • 收益波动率26.48%
  • 信息比率1.64
  • 最大回撤40.3%
[2018-04-28 16:55:29.813414] INFO: bigquant: backtest.v7 运行完成[98.499965s].

市场状态判断

In [5]:
def prepare(context):
    # 获取股票市净率数据,返回DataFrame数据格式
    market_cap_data = D.history_data(context.instruments,context.start_date,context.end_date,
              fields=['pb_lf','amount'])
    # 获取每日按市净率排序 (从低到高)的前三十只股票
    context.daily_buy_stock = market_cap_data.groupby('date').apply(lambda df:df[(df['amount'] > 0)&((df['pb_lf'] > 0))].sort_values('pb_lf')[:30])

    bm_price = D.history_data(['000300.SHA'], start_date=context.start_date , end_date=context.end_date, fields=['close'])
    bm_price['sma'] = bm_price['close'].rolling(5).mean()
    bm_price['lma'] = bm_price['close'].rolling(32).mean()
    bm_price['gold_cross_status'] = bm_price['sma'] > bm_price['lma']
    bm_price['pos_percent'] = np.where(bm_price['gold_cross_status'],1,0)
    context.pos_df = bm_price[['date', 'pos_percent']].set_index('date')

选股+择时策略

In [8]:
# 回测参数设置,initialize函数只运行一次
def initialize(context):
    # 手续费设置
    context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5)) 
    # 调仓规则(每月的第一天调仓)
    context.schedule_function(rebalance, date_rule=date_rules.month_start(days_offset=0)) 

# handle_data函数会每天运行一次
def handle_data(context,data):
    date = data.current_dt.strftime('%Y-%m-%d')
    stock_hold_now = [equity.symbol for equity in context.portfolio.positions]
    # 满足空仓条件
    if context.pos_df.ix[date].pos_percent == 0:
        # 全部卖出
        for stock in stock_hold_now:
            if data.can_trade(context.symbol(stock)):
                context.order_target_percent(context.symbol(stock), 0)

# 换仓函数
def rebalance(context, data):
    # 当前的日期
    date = data.current_dt.strftime('%Y-%m-%d')
    # 根据日期获取调仓需要买入的股票的列表
    stock_to_buy = list(context.daily_buy_stock.ix[date].instrument)
    # 通过positions对象,使用列表生成式的方法获取目前持仓的股票列表
    stock_hold_now = [equity.symbol for equity in context.portfolio.positions]
    # 继续持有的股票:调仓时,如果买入的股票已经存在于目前的持仓里,那么应继续持有
    no_need_to_sell = [i for i in stock_hold_now if i in stock_to_buy]
    # 需要卖出的股票
    stock_to_sell = [i for i in stock_hold_now if i not in no_need_to_sell]
  
    # 卖出
    for stock in stock_to_sell:
        # 如果该股票停牌,则没法成交。因此需要用can_trade方法检查下该股票的状态
        # 如果返回真值,则可以正常下单,否则会出错
        # 因为stock是字符串格式,我们用symbol方法将其转化成平台可以接受的形式:Equity格式

        if data.can_trade(context.symbol(stock)):
            # order_target_percent是平台的一个下单接口,表明下单使得该股票的权重为0,
            #   即卖出全部股票,可参考回测文档
            context.order_target_percent(context.symbol(stock), 0)
    
    # 如果当天没有买入的股票,就返回
    if len(stock_to_buy) == 0:
        return

    # 等权重买入 
    weight =  1 / len(stock_to_buy)
    
    # 买入
    for stock in stock_to_buy:
        if data.can_trade(context.symbol(stock)):
            # 下单使得某只股票的持仓权重达到weight,因为
            # weight大于0,因此是等权重买入
            context.order_target_percent(context.symbol(stock), weight)
            
m1 = M.trade.v2( 
    instruments=D.instruments(),
    start_date='2013-01-01' , 
    end_date='2017-08-10' ,
    # 必须传入initialize,只在第一天运行
    initialize=initialize,
    #  必须传入handle_data,每个交易日都会运行
    handle_data=handle_data,
    prepare=prepare,
    # 买入以开盘价成交
    order_price_field_buy='open',
    # 卖出也以开盘价成交
    order_price_field_sell='open',
    # 策略本金
    capital_base=1000000,
    # 比较基准:沪深300
    benchmark='000300.INDX',
)
[2018-04-28 17:04:09.050224] INFO: bigquant: backtest.v7 开始运行..
[2018-04-28 17:04:30.189086] INFO: algo: set price type:backward_adjusted
[2018-04-28 17:05:14.737753] INFO: Performance: Simulated 1119 trading days out of 1119.
[2018-04-28 17:05:14.739184] INFO: Performance: first open: 2013-01-04 01:30:00+00:00
[2018-04-28 17:05:14.740304] INFO: Performance: last close: 2017-08-10 07:00:00+00:00
  • 收益率253.08%
  • 年化收益率32.86%
  • 基准收益率47.28%
  • 阿尔法0.27
  • 贝塔0.35
  • 夏普比率1.65
  • 胜率0.704
  • 盈亏比2.213
  • 收益波动率17.22%
  • 信息比率1.08
  • 最大回撤14.71%
[2018-04-28 17:05:19.582779] INFO: bigquant: backtest.v7 运行完成[70.532544s].

结论

可以看出,加入择时的策略比之前的策略有较好的改善和提高,改进效果比较明显。

选股 选股+择时 改进效果
年化收益 28.64% 32.86% 提高14.73%
夏普比率 0.91 1.65 提高81.32%
收益波动率 26.48% 17.22% 减少35%
最大回撤 40.3% 14.71% 减少63.5%

小结:本文分别给出单纯选股策略和选股+择时策略案例,并进行比对,我们会发现,选股+择时策略资金曲线会更加平滑,表现会更加优良。以此为大家提供一个参考。


   本文由BigQuant宽客学院推出,版权归BigQuant所有,转载请注明出处。


【宽客学院】策略风险分析
(kingmo888) #2

简单的均线交叉择时达不到题主这样的效果的,
而题主实现了这样的效果必然是哪里有些问题。


(iQuant) #3

欢迎交流。均线择时主要是使得仓位很多时候为0,所以规避了大跌。仓位你可以从图上看出,持仓占比是绿色的线条。15年以来,还是很多有一些时间段是保持空仓的。


(a20180322) #4

大盘择时和总体仓位管理很重要,可以加入可视化视图吗,确保资金曲线跟得上牛市的同时熊市少回撤


(iQuant) #5

可以的哈。
可视化编写策略功能很强大的。
一个用户的帖子里面就可以大盘择时,不过这个功能他注释了,你要使用的话,取消注释就行。