[量化学堂-策略开发]大师系列之价值投资选股策略

大师系列
价值投资
标签: #<Tag:0x00007f5bf8c1fb50> #<Tag:0x00007f5bf8c1fa10>

(iQuant) #1

国外证券市场比较悠久,曾出现过本杰明·格雷厄姆、彼得·林奇、詹姆斯·奥肖内西、查尔斯·布兰德斯等多位投资大师,这些投资大师有一个共同点,他们在证券市场上保持了常年的稳定持续盈利,他们的投资法则及选股标准在一些著作中有详细的描述。值得欣慰的是,申万宏源证券研究所发布了<申万宏源-申万大师系列价值投资篇>系列第一季共20篇研究报告。学习这些报告主要有两个目的:

一是我们自身想去认真的学习经典,复制这些策略本身就是自我学习过程,我们深信向这些被市场证明长期优秀,被后世尊为经典的投资大师学习,必然值得,必有所得;

二是复制和验证大师策略的过程, 会自然的驱使我们更多的从投资逻辑和投资思维上思考收益之源,而不再是不停的数据挖掘和数理分析。大师系列的尝试,于我们是一个求道,而非求术的旅程。

本贴主要是帮助用户怎样开发大师系列的策略,让大家更了解我们的平台,同时帮助大家在我们的平台上开发更丰富的策略。因此我们介绍一种简单的价值投资法来选取股票,规则如下:

策略逻辑:当股票处于价值洼地时,具备投资价值

策略内容:每月月初买入市盈率小于15倍、市净率小于1.5倍的30只股票,持有至下个月月初再调仓

资金管理:等权重买入

风险控制:无单只股票仓位上限控制、无止盈止损

第一步:获取数据, 整理换仓时的买入股票列表

BigQuant平台具有丰富的金融数据,包括行情数据和财报数据,并且具有便捷、简单的API调用接口。

def prepare(context):
    start_date = context.start_date # 开始日期
    end_date = context.end_date # 结束日期
    context.instruments = D.instruments(context.start_date, context.end_date, market='CN_STOCK_A')
    # 获取市盈率、市净率、成交额数据。history_data是我们平台获取数据的一个重要API。fields参数为列表形式,传入的列表即为我们想要获取的数据。
    history_data = D.history_data(instruments, context.start_date, context.end_date, ['pb_lf', 'pe_ttm','amount'])
    context.daily_buy_stock = history_data.groupby('date').apply(seek_symbol)  #  按交易日groupby,获取每个交易日选出的股票列表
    
def seek_symbol(df):
    selected = df[(df['pb_lf'] < 1.5)
        & (df['pe_ttm'] < 15) 
        & (df['amount'] > 0) 
        & (df['pb_lf'] > 0)
        & (df['pe_ttm'] > 0)]
                                    
    # 按pe_ttm和pb_lf 升序排列
    selected = selected.sort_values(['pe_ttm', 'pb_lf'])
    return list(selected.instrument)[:30] # 记得转化成list

第二步:回测主体函数
我们平台策略回测有丰富的文档介绍,请参考:帮助文档

def initialize(context):
    # 设置交易费用,买入是万三,卖出是千分之1.3,如果不足5元按5元算
    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)) 
   
def handle_data(context,data):
    pass

# 换仓
def rebalance(context, data):
    # 日期
    date = data.current_dt.strftime('%Y-%m-%d')
    
    # 买入股票列表
    stock_to_buy = context.daily_buy_stock.ix[date]
    # 目前持仓列表    
    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:
        if data.can_trade(context.symbol(stock)):
            context.order_target_percent(context.symbol(stock), 0)
            
    # 如果当天没有买入就返回
    if len(stock_to_buy) == 0:
        return
    
    # 等权重
    weight = 1 / len(stock_to_buy)
    # 执行买入
    for  cp in stock_to_buy:
        if data.can_trade(context.symbol(cp)):
            context.order_target_percent(context.symbol(cp), weight)

第三步:回测接口

# 使用该回测接口,需要传入多个策略参数
m = M.trade.v3( 
    instruments=None,
    start_date='2013-01-01', 
    end_date='2018-02-02',
    prepare=prepare,
    # 必须传入initialize,只在第一天运行
    initialize=initialize,
    # 必须传入handle_data,每个交易日都会运行
    handle_data=handle_data,
    # 买入以开盘价成交
    order_price_field_buy='open',
    # 卖出也以开盘价成交
    order_price_field_sell='open',
    # 策略本金
    capital_base=1000000,
    # 比较基准:沪深300
    benchmark='000300.INDX',
) 

好嘞,策略就完全写好了。我们运行完曲线如下:

整体来看,该策略是正收益系统策略,长期坚持该策略收益是不错的。

是不是发现我们平台很方便开发策略?之前朋友问我,为什么Python运行速度不是最快但会成为量化的主流语言。其实对于量化研究人员来说,虽然速度是一方面考虑,但更多的是为了验证策略思想,Python语言的优势就是在此,有一个思想就可以很快的将思想验证,然而C++虽然速度快,但要验证一个简单的思想却要编写大量的代码。好比为什么飞机速度快,但市里面上班开汽车就足够了(不考虑其他因素),因为汽车足够灵活。所以,还在犹豫选择什么语言从事量化投资的小伙伴们,Python就是你比较好的选择。本文到此就要结束了,策略的完整代码分享在文末,小伙伴们赶紧 克隆策略吧。

克隆策略

1. 获取数据, 整理换仓时的买入股票列表

In [22]:
def prepare(context):
    start_date = context.start_date # 开始日期
    end_date = context.end_date # 结束日期
    context.instruments = D.instruments(context.start_date, context.end_date, market='CN_STOCK_A')
    # 获取市盈率、市净率、成交额数据
    history_data = D.history_data(instruments, context.start_date, context.end_date, ['pb_lf', 'pe_ttm','amount'])
    context.daily_buy_stock = history_data.groupby('date').apply(seek_symbol)
    
def seek_symbol(df):
    selected = df[(df['pb_lf'] < 1.5)
        & (df['pe_ttm'] < 15) 
        & (df['amount'] > 0) 
        & (df['pb_lf'] > 0)
        & (df['pe_ttm'] > 0)]
                                    
    # 按pe_ttm和pb_lf 升序排列
    selected = selected.sort_values(['pe_ttm', 'pb_lf'])
    return list(selected.instrument)[:30] # 记得转化成list

2. 回测主体函数

In [23]:
def initialize(context):
    # 设置交易费用,买入是万三,卖出是千分之1.3,如果不足5元按5元算
    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)) 
   
def handle_data(context,data):
    pass

# 换仓
def rebalance(context, data):
    # 日期
    date = data.current_dt.strftime('%Y-%m-%d')
    
    # 买入股票列表
    stock_to_buy = context.daily_buy_stock.ix[date]
    # 目前持仓列表    
    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:
        if data.can_trade(context.symbol(stock)):
            context.order_target_percent(context.symbol(stock), 0)
            
    # 如果当天没有买入就返回
    if len(stock_to_buy) == 0:
        return
    
    # 等权重
    weight = 1 / len(stock_to_buy)
    # 执行买入
    for  cp in stock_to_buy:
        if data.can_trade(context.symbol(cp)):
            context.order_target_percent(context.symbol(cp), weight)

3. 回测接口

In [26]:
# 使用该回测接口,需要传入多个策略参数
m = M.trade.v3( 
    instruments=None,
    start_date='2013-01-01', 
    end_date='2018-02-02',
    prepare=prepare,
    # 必须传入initialize,只在第一天运行
    initialize=initialize,
    # 必须传入handle_data,每个交易日都会运行
    handle_data=handle_data,
    # 买入以开盘价成交
    order_price_field_buy='open',
    # 卖出也以开盘价成交
    order_price_field_sell='open',
    # 策略本金
    capital_base=1000000,
    # 比较基准:沪深300
    benchmark='000300.INDX',
)
[2018-02-02 17:16:54.226281] INFO: bigquant: backtest.v7 开始运行..
[2018-02-02 17:17:28.116572] INFO: algo: set price type:backward_adjusted
[2018-02-02 17:18:56.302777] INFO: Performance: Simulated 1239 trading days out of 1239.
[2018-02-02 17:18:56.305832] INFO: Performance: first open: 2013-01-04 01:30:00+00:00
[2018-02-02 17:18:56.310324] INFO: Performance: last close: 2018-02-02 07:00:00+00:00
  • 收益率225.53%
  • 年化收益率27.13%
  • 基准收益率69.29%
  • 阿尔法0.16
  • 贝塔0.91
  • 夏普比率0.91
  • 胜率0.686
  • 盈亏比2.469
  • 收益波动率24.96%
  • 信息比率1.36
  • 最大回撤35.59%
[2018-02-02 17:19:06.138883] INFO: bigquant: backtest.v7 运行完成[131.912562s].

已经生成了策略,如何利用策略推荐股票操作
[量化学堂-策略开发]事件驱动策略(基于业绩快报)
社区干货与精选整理(持续更新中...)
向导式策略生成器新建AI策略
请教:如何选出高市值的标的
(DANNIKMAO) #2

非常奇怪,年化20%多,回撤35%,信息比率居然能达到8.39,确定算法正确??


(小Q) #3

经过全面和详细的检查,发现回测结果指标确实有一点问题,已经完全debug了。谢谢您的 宝贵意见!


(iQuant) #4

@DANNIKMAO 我们进行了指标的检验,发现修复后指标计算是正确的。
指标计算参考《回测结果指标详解》
检验过程如下:

克隆策略

1.获取数据

In [1]:
start_date = '2013-02-01' # 开始日期
end_date = '2017-05-07' # 结束日期
instruments = D.instruments()
# 获取市盈率、市净率、成交额数据
history_data = D.history_data(instruments, start_date=start_date,
               end_date= end_date, fields=[ 'pb_lf', 'pe_ttm','amount'])

2.整理换仓时买入股票列表

In [2]:
# 该函数的目的是通过history_data这个大的原始数据,获取每日满足价值投资股票列表
def seek_symbol(df):
    selected = df[(df['pb_lf'] < 1.5)
        & (df['pe_ttm'] < 15) 
        & (df['amount'] > 0) 
        & (df['pb_lf'] > 0)
        & (df['pe_ttm'] > 0 ) ]
                                    
    # 按pe_ttm和pb_lf 升序排列
    selected = selected.sort_values(['pe_ttm','pb_lf'])
    return list(selected.instrument)[:30] # 记得转化成list

daily_buy_stock = history_data.groupby('date').apply(seek_symbol)

3. 回测主体

In [3]:
def initialize(context):
    # 设置交易费用,买入是万三,卖出是千分之1.3,如果不足5元按5元算
    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)) 
    # 上面schedule_function函数的这句代码,其实可以写到一行,分两行是为了便于展示
   
def handle_data(context,data):
    pass

# 换仓
def rebalance(context, data):
    # 日期
    date = data.current_dt.strftime('%Y-%m-%d')
   
    # 买入股票列表
    stock_to_buy = daily_buy_stock.ix[date]
    # 目前持仓列表    
    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:
        if data.can_trade(context.symbol(stock)):
            context.order_target_percent(context.symbol(stock), 0)
    
    if len(stock_to_buy) == 0:
        return
    # 等权重
    weight = 1 / len(stock_to_buy)
    # 执行买入
    for  cp in stock_to_buy:
        if data.can_trade(context.symbol(cp)):
            context.order_target_percent(context.symbol(cp), weight)

4.回测接口

In [4]:
# 使用第四版的回测接口,需要传入多个策略参数
m=M.backtest.v5( 
    instruments=instruments,
    start_date=start_date, 
    end_date=end_date,
    # 必须传入initialize,只在第一天运行
    initialize=initialize,
    # 必须传入handle_data,每个交易日都会运行
    handle_data=handle_data,
    # 买入以开盘价成交
    order_price_field_buy='open',
    # 卖出也以开盘价成交
    order_price_field_sell='open',
    # 策略本金
    capital_base=1000000,
    # 比较基准:沪深300
    benchmark='000300.INDX',
)
[2017-05-09 17:49:18.984825] INFO: bigquant: backtest.v5 start ..
[2017-05-09 17:52:30.058895] INFO: Performance: Simulated 1032 trading days out of 1032.
[2017-05-09 17:52:30.061169] INFO: Performance: first open: 2013-02-01 14:30:00+00:00
[2017-05-09 17:52:30.062580] INFO: Performance: last close: 2017-05-05 19:00:00+00:00
  • 收益率134.04%
  • 年化收益率23.08%
  • 基准收益率25.89%
  • 阿尔法0.17
  • 贝塔0.92
  • 夏普比率0.7
  • 收益波动率26.85%
  • 信息比率1.42
  • 最大回撤35.61%
[2017-05-09 17:52:38.484237] INFO: bigquant: backtest.v5 end [199.499353s].
In [ ]:
## 
In [14]:
beta = results['beta'].tail(1)[0] # 系统自动计算的beta
print('beta:' ,beta)
risk_free = results['treasury_period_return'].tail(1)[0] # 无风险收益率
cum_algo_return = results['algorithm_period_return'].tail(1)[0] # 总收益
print('总收益:',cum_algo_return)
cum_benchmark_return = results['benchmark_period_return'].tail(1)[0] # 基准总收益
print('基准总收益:',cum_benchmark_return)
num_trading_days = results['trading_days'].tail(1)[0] # 天数
annualized_algo_return = (cum_algo_return + 1.0) ** (252.0 / num_trading_days) - 1.0
print('策略年化收益:', annualized_algo_return)
annualized_benchmark_return = (cum_benchmark_return + 1.0) **  (252.0 / num_trading_days) - 1.0
alpha_correct =annualized_algo_return - (risk_free + beta * (annualized_benchmark_return - risk_free) )  
print('alpha: ', alpha_correct)
beta: 0.92017861797
总收益: 1.34038272453
基准总收益: 0.258910141263
策略年化收益: 0.230763772066
alpha:  0.174153251028

sharp计算

In [11]:
 
sharp_ratio = ((cum_algo_return +1)** (252.0 / num_trading_days) -1 - risk_free) / algo_volatility  
print('sharp_ratio: ', sharp_ratio)
sharp_ratio:  0.701069020707

信息比率计算

In [12]:
diff = algo_daily_return - benchmark_daily_return
std = np.std(diff)
annualized_std = std*(252**0.5)
ir = (annualized_algo_return - annualized_benchmark_return) / annualized_std
print('信息比率' ,ir)
手动计算的信息比率 1.42944640716

(eqsxin) #5

时间拉长就看到了这个策略的力量!


(chenjianjia) #6

《回测结果指标详解》这个连接打不开


(chenjianjia) #7

两个克隆版本,一个有prepare函数,一个没有,有什么区别吗?回测或者实盘模拟需不需要prepare函数?


(小Q) #8

实盘模拟需要prepare函数的。

没有prepare的版本是最开始的版本,只能用户回测。最近更新的版本有prepare函数,不仅能用来回测还能用来实盘模拟。

建议使用带prepare的版本。


(小Q) #9

验证时间:2018年2月8日 20:35 ,可以打开该篇文章哈。


(jam) #10

还是打不开


(upndown) #11

我用了这种方式计算各个回测指标:

import empyrical
# 统计策略指标
def get_stats(results):
    return_ratio  = empyrical.cum_returns_final(results.returns)
    annual_return_ratio  = empyrical.annual_return(results.returns)
    sharp_ratio = empyrical.sharpe_ratio(results.returns,0.035/252)
    return_volatility = empyrical.annual_volatility(results.returns)
    max_drawdown  = empyrical.max_drawdown(results.returns)
    benchmark_returns = (results.benchmark_period_return+1)/(results.benchmark_period_return+1).shift(1)-1
    alpha, beta =empyrical.alpha_beta_aligned(results.returns, benchmark_returns)
    
    return {
      'return_ratio': return_ratio,
      'annual_return_ratio': annual_return_ratio,
      'beta': beta,
      'alpha': alpha,
      'sharp_ratio': sharp_ratio,
      'return_volatility': return_volatility,
      'max_drawdown': max_drawdown,
    }
get_stats(m.raw_perf.read_df())