公司的基本面因素一直具备滞后性,令基本面的量化出现巨大困难。而从上市公司的基本面因素来看,一般只有每个季度的公布期才会有财务指标的更新,而这种财务指标的滞后性对股票表现是否有影响呢?如何去规避基本面滞后产生的风险呢?下面我们将重点介绍量化交易在公司基本面分析上的应用,即平时常说的 基本面量化(Quantamental)。
反映公司经营优劣的指标
首先我们简单介绍下可能运用在量化策略上的基本面指标,相信大部分投资者都对上市公司的基本面有一定的了解,上市公司的基本面情况总是同公司业绩相关,而衡量业绩的主要基本面指标有每股收益、净资产收益率、主营业务收入等等。
而上市公司财务指标又常常存在相关的性质,比如每股收益和主营业务收入和产品毛利率相关,所以当我们把一堆财务指标放在一起统计可能就会产生相关性问题,从而降低了模型对市场走势的解释程度。因此,如何选出合适的独立性指标就成为我们进行财务指标量化模型设计的基础。
那么怎样的财务指标会较真实的反映上市公司的经营优劣呢?
-
具有延续性的财务指标,比如近三年净利润增速,这一个指标把3年的净利润增速平均起来,这种增长性具备一定的长期特征;
-
与现金流相关的指标,由于涉及真实的资金往来,现金流能够比较真实反映上市公司的经营状况。
每股现金流量/每股业绩
每股现金流量比每股盈余更能显示从事资本性支出及支付股利的能力。每股现金流量通常比每股盈余要高,这是因为公司正常经营活动所产生的净现金流量还会包括一些从利润中扣除出去但又不影响现金流出的费用调整项目,如折旧费等。但每股现金流量也有可能低于每股盈余。一家公司的每股现金流量越高,说明这家公司的每股普通股在一个会计年度内所赚得的现金流量越多;反之,则表示每股普通股所赚得的现金流量越少。
而每股现金流量常常与上市公司的业绩、总股本相关,所以用每股现金流量/每股业绩来衡量上市公司的现金流动情况,比单纯用每股盈余更为合理。
净资产收益率
净资产收益率又称股东权益收益率,是净利润与平均股东权益的百分比,是公司税后利润除以净资产得到的百分比率,该指标反映股东权益的收益水平,用以衡量公司运用自有资本的效率。指标值越高,说明投资带来的收益越高。
净资产收益率通过净资金去计量每年上市公司收益的百分比,净资产收益率比每股净利润,资产收益率等更合理的衡量归于于股东的上市公司权益的增值速度。
销售毛利率
销售毛利率,表示每一元销售收入扣除销售成本后,有多少钱可以用于各项期间费用和形成盈利。 销售毛利率是企业销售净利率的最初基础,没有足够大的毛利率便不能盈利。
在分析企业主营业务的盈利空间和变化趋势时,销售毛利率是一个重要指标。该指标的优点在于可以对企业某一主要产品或主要业务的盈利状况进行分析,这对于判断企业核心竞争力的变化趋势及其企业成长性极有帮助。
基本面量化的具体实现
-
确定三个财务因子为销售毛利率、净资产收益率、每股现金流量/每股业绩
-
通过features数据接口获取全市场3000多家上市公司的财务数据
-
单独筛选每个财务因子前500的上市公司
-
最终确定三个因子都能排在前500的股票篮子
-
买入该股票篮子,等权重买入
-
一个月换仓一次,买入新确定的股票篮子
回测结果:
从策略结果来看,年化收益26.9%,应该超过了大部分公募基金,虽然回撤很大,但细心地伙伴可以看出是发生在15年股灾期间和16年熔断期间,如果配合择时模型,想必效果会更好。尤其是值得注意的是,该策略在17年还取得了稳定正收益。本例子只作为如何使用财务数据进行基本面量化的样例策略,便于大家能够快速上手开发策略。
策略案例
数据准备函数
def prepare(context): # 确定起始时间 start_date = context.start_date # 确定结束时间 end_date = context.end_date instruments = context.instruments fields = ['fs_gross_profit_margin_0', 'fs_roe_0', 'fs_free_cash_flow_0', 'fs_net_profit_0'] raw_data = D.features(instruments, start_date, end_date, fields) raw_data['cash_flow/profit'] = raw_data['fs_free_cash_flow_0'] / raw_data['fs_net_profit_0'] context.daily_buy_stock = pd.DataFrame(raw_data.groupby('date').apply(seek_stock)) def seek_stock(df): ahead_f1 = set(df.sort_values('fs_roe_0',ascending=False)['instrument'][:500]) ahead_f2 = set(df.sort_values('fs_gross_profit_margin_0',ascending=False)['instrument'][:500]) ahead_f3 = set(df.sort_values('cash_flow/profit',ascending=False)['instrument'][:500]) return list(ahead_f1 & ahead_f2 & ahead_f3)
策略逻辑主体函数
# 回测参数设置,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][0]) # 通过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(market='CN_STOCK_A'), start_date='2013-01-01', end_date='2017-05-01', 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', m_deps='quantamental' )