附录:因子检查策略代码¶
用于因子策略回测效果检查
'''
根据研究结论编写的策略,由于研究中多空组合的收益中,空头贡献了重要的部分(约50%),由于回测中不能做空,所以策略的收益相对研究中较低。
by wunderkindye
'''
'''
根据聚宽高频因子挖掘大赛比赛专用模板修改
初始资金:2000000
建议回测时间范围
每日调仓 一年 回测日期:20180720-20190720
每周调仓 三年 回测日期:20160720-20190720
每月调仓 五年 回测日期:20140720-20190720
股票池:中证500
每日持仓:数量固定为股票池的20只,持仓均为等权重持有
换仓时间:默认14:50
交易成本:不考虑滑点,印花税1‰,佣金2.5‱、最低5元
'''
# 导入函数库
from jqdata import *
import numpy as np
import pandas as pd
import jqfactor
################################################# 以下内容根据研究因子内容定义 ########################################################
# 定义因子
def calc_factor(context):
'''
用户自定义因子,要求返回一个 Series,index为股票code,value为因子值
我们会买入「因子值最小」的20只,如果您想使用买入「因子值最大」的20只股票,只需将您的结果「乘以-1.0」即可,详见函数下方 return 部分
'''
# 获取股票池,g.stock_pool为因子挖掘的对象股票池,用户不可对此股票池进行二次筛选
stocks = g.stock_pool
# 获取当前时间
now = context.current_dt
# 获取数据
close = get_price(stocks,end_date=context.previous_date,count=21,fields=['close'])['close']
money = get_price(stocks,end_date=context.previous_date,count=21,fields=['money'])['money']
cap = get_fundamentals(query(
valuation.circulating_market_cap
).filter(
valuation.code.in_(stocks)
), date=context.previous_date)
far = close.iloc[-1,:]/close.iloc[0,:] - 1
#far = sum(close.iloc[:,:])/(21*close.iloc[-1,:])-1 研报中的优化思路:均线价格代替期末期初价格差,然而实证检验效果不佳
money = (sum(money.iloc[:,:]))
cap = pd.Series(cap['circulating_market_cap'].values, index=far.index)
result = (far.iloc[:]/(money.iloc[:]/(cap.iloc[:]*100000000)))
return result
# 开盘前运行函数
def before_market_open(context):
'''
盘后运行函数,可选实现
'''
pass
## 收盘后运行函数
def after_market_close(context):
'''
盘后运行函数,可选实现
'''
pass
################################################# 以下内容除设置运行周期,其他地方不用修改 ########################################################
# 初始化函数,设定基准等等
def initialize(context):
# 设定500等权作为基准
g.benchmark = '000982.XSHG'
set_benchmark(g.benchmark)
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
### 股票相关设定 ###
# 股票类每笔交易时的手续费
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.00025, close_commission=0.00025, min_commission=5),type='stock')
# 滑点
set_slippage(FixedSlippage(0.0))
# 初始化因子设置
factor_analysis_initialize(context)
# 定义股票池
set_stockpool(context)
# 运行函数(reference_security为运行时间的参考标的;传入的标的只做种类区分,因此传入'000300.XSHG'或'510300.XSHG'是一样的)
run_daily(set_stockpool, time='before_open', reference_security='000300.XSHG')
run_daily(before_market_open, time='before_open', reference_security='000300.XSHG')
#设置策略交易时间间隔
#run_daily(trade, time='14:50', reference_security='000300.XSHG')
run_weekly(trade,1, time='14:50', reference_security='000300.XSHG')
#run_monthly(trade,1, time='14:50', reference_security='000300.XSHG')
run_daily(after_market_close, time='after_close', reference_security='000300.XSHG')
# 定义股票池
def set_stockpool(context):
# 获取股票池
stocks = get_index_stocks(g.benchmark,context.previous_date)
paused_series = get_price(stocks,end_date=context.current_dt,count=1,fields='paused')['paused'].iloc[0]
# g.stock_pool 为因子挖掘的对象股票池,用户不可对此股票池进行二次筛选
g.stock_pool = paused_series[paused_series==False].index.tolist()
# 定义需要用到的全局变量
def factor_analysis_initialize(context):
# g.weight_method 为加权方式, "avg"按平均加权
g.weight_method = "avg"
weight_method_model = {"avg": "平均加权"}
# 持仓股票数量
g.buy_num = 20
# g.sell为卖出股票权重列表
g.sell = pd.Series(dtype=float)
# g.buy为买入股票权重列表
g.buy = pd.Series(dtype=float)
#g.ind为行业分类
g.ind = 'jq_l1'
# g.d 为获取昨天的时间点
g.d = context.previous_date
# 对因子进行分析计算出每日买入或卖出的股票
def fac(context):
# 获取因子值
far = calc_factor(context)
# 买入股票池
try:
buy = far.sort_values(ascending=True).index.tolist()[:g.buy_num]
except:
buy = far.order(ascending=True).index.tolist()[:g.buy_num]
# 买卖股票权重
if g.weight_method == "avg":
buy_weight = pd.Series(1. / len(buy), index=buy)
else:
raise ValueError('invalid weight_method %s', weight_method)
return buy_weight
#股票交易
def trade(context):
# 计算买入卖出的股票和权重
try:
factor_analysis_initialize(context)
g.buy = fac(context)
except ValueError:
if "Bin edges must be unique" in str(e):
log.error("计算因子值过程出错!")
else:
raise
for s in context.portfolio.positions.keys():
if s not in g.buy.index:
order_target_value(s, 0)
long_cash = context.portfolio.total_value
for s in g.buy.index:
order_target_value(s, g.buy.loc[s] * 0.98 * long_cash)
# 买入股票
def buy(context):
# 计算买入卖出的股票和权重
try:
factor_analysis_initialize(context)
g.buy = fac(context)
except ValueError:
if "Bin edges must be unique" in str(e):
log.error("计算因子值过程出错!")
else:
raise
long_cash = context.portfolio.total_value
for s in g.buy.index:
order_target_value(s, g.buy.loc[s] * 0.98 * long_cash)
# 卖出股票
def sell(context):
for s in context.portfolio.positions.keys():
order_target_value(s, 0)
请先从下面内容开始¶
因子分析基础模板¶
#导入需要的数据库
from jqfactor import *
from jqdata import *
import pandas as pd
import warnings
warnings.filterwarnings('ignore')
#获取日期列表
def get_tradeday_list(start,end,frequency=None,count=None):
if count != None:
df = get_price('000001.XSHG',end_date=end,count=count)
else:
df = get_price('000001.XSHG',start_date=start,end_date=end)
if frequency == None or frequency =='day':
return df.index
else:
df['year-month'] = [str(i)[0:7] for i in df.index]
if frequency == 'month':
return df.drop_duplicates('year-month').index
elif frequency == 'quarter':
df['month'] = [str(i)[5:7] for i in df.index]
df = df[(df['month']=='01') | (df['month']=='04') | (df['month']=='07') | (df['month']=='10') ]
return df.drop_duplicates('year-month').index
elif frequency =='halfyear':
df['month'] = [str(i)[5:7] for i in df.index]
df = df[(df['month']=='01') | (df['month']=='06')]
return df.drop_duplicates('year-month').index
===初始化====¶
# 设置起止时间
start='2018-07-20'
end='2019-07-20'
# 设置调仓周期
periods=(5,10,20)
# 设置分层数量
quantiles=5
#获取日期列表
date_list = get_tradeday_list(start=start,end=end,count=None)#获取回测日期间的所有交易日
===原始计算因子数据===¶
- 进行因子值函数定义
- 循环日期获取因子值
定义要计算的动量因子¶
研报中提出,A股市场的反转现象是由于散户比例高,导致市场的非理性与羊群效应,因此股票的价格变化往往存在过度变化的情况,后续会反向调整。
继而,研报中提出的因子优化的思路之一,是通过中性化(提取横截面回归残差),去除动量/反转因子中含有的流动性因子(流动性因子作为散户数量的代理变量,散户越多,该股票被交易的频率越高,从而有较高的流动性。
原来的模板中也在注释中给出了中性化处理的代码,但经检验,中性化处理对因子IC,IR值的改善贡献不大。仔细思考,中性化处理(提取截面回归残差)其实是基于线性回归模型,即假设因变量(21天前后股票价格差)和自变量(流动性/散户交易占比)呈线性关系。但其实这样的假设并不太合理,直觉上,散户参与交易不应固定地使股票21天后的价格比21天前的价格更高或更低,而应是放大21天前后的价格差。本来上涨的股票因为散户的参与涨的更多,本来下跌的股票下跌更多。
对应的,在本研究中替代原来的中性化处理,用“21天前后价格差/21天平均换手率之和”作为改良后的因子。
def factor_cal(pool,date):
close = get_price(pool,end_date=date,count=21,fields=['close'])['close']#获取收盘价
money = get_price(pool,end_date=date,count=21,fields=['money'])['money']#获取交易额
cap = get_fundamentals(query(
valuation.circulating_market_cap
).filter(
valuation.code.in_(pool)
), date=date) #获取股票流通市值,用于计算换手率用
far = close.iloc[-1,:]/close.iloc[0,:] - 1 #21天前后价差
#far = sum(close.iloc[:,:])/(21*close.iloc[-1,:])-1 用于研报中的另一种改良思路,用平均价格代替时间点价格,但检验发现效果不佳
money = (sum(money.iloc[:,:])/21.) #计算平均换手率
cap = pd.Series(cap['circulating_market_cap'].values, index=far.index)
result = (far.iloc[:]/(money.iloc[:]/(cap.iloc[:]*100000000))) #计算因子值:股价差/平均换手率
return result
#定义一个空的dataframe记录因子值
factor_df = pd.DataFrame()
#循环计算给定日期范围的因子值
mark = 1
print (len(date_list))
for d in date_list:
try: #获取流通市值的函数在零星几天会失败,因此这里用try处理
pool = get_index_stocks('000905.XSHG',date=d)
far = factor_cal(pool,d)
if mark == 1:
factor_df = far
mark = 0
else:
factor_df = pd.concat([factor_df,far],axis=1)
except:
date_list=date_list.drop(d)
continue
#将columns更改为可以日期标签
factor_df.columns = date_list
factor_df.head(3)
===进行因子数据优化===¶
- 标准化
#数据清洗、包括去极值、标准化、中性化等,并加入y值
for date in date_list:
#对数据进行处理、标准化、去极值、中性化
#factor_df = winsorize_med(factor_df, scale=3, inclusive=True, inf2nan=True, axis=0) #中位数去极值处理
se = standardlize(factor_df[date], inf2nan=True) #对每列做标准化处理
factor_df[date] = se
#进行转置,调整为分析可用的格式
factor_df = factor_df.T
factor_df.head()
因子效果检查¶
#使用获取的因子值进行单因子分析
far = analyze_factor(factor=-factor_df, start_date=date_list[0], end_date=date_list[-1], weight_method='avg', industry='jq_l1', quantiles=quantiles, periods=periods,max_loss=0.3)
IC分析
# 打印信息比率(IC)相关表
far.plot_information_table(group_adjust=False, method='rank')
分组收益
# 画各分位数平均收益图
far.plot_quantile_returns_bar(by_group=False, demeaned=0, group_adjust=False)
#调用因子分析方法,进行因子信息全览,主要关注做多最大分位做空最小分位的收益
far.create_full_tear_sheet(demeaned=False, group_adjust=False, by_group=False, turnover_periods=None, avgretplot=(5, 15), std_bar=False)
因子效果检查综述¶
IC分析显示,改良后的因子IC值与原因子相近,但标准差有所降低,即信息系数的稳定性得到提升。IR最低值为0.389,表现较稳健。
分组收益显示,在三种持有周期下,收益的组间单调性表现优秀,相比原来的因子有明显提升。
做多最大分位做空最小分位的多空组合显示收益较稳健,持有10天的多空组合年化收益接近15%,回测周期中上半年内的因子效率有待提升。