聚宽:https://www.joinquant.com/post/1474
多因子策略-APT模型
# 多因子策略-APT模型(Multi-factor)
# 2006-01-01 到 2016-05-29, ¥200000, 每天
import math
import datetime
import numpy as np
import pandas as pd
from jqdata import *
# 导入多元线性规划需要的工具包
import statsmodels.api as sm
from statsmodels import regression
'''
================================================================================
总体回测前
================================================================================
'''
#总体回测前要做的事情
def initialize(context):
set_params() # 1设置策参数
set_variables() # 2设置中间变量
set_backtest() # 3设置回测条件
#1
#设置策略参数
def set_params():
g.tc = 63 # 设置调仓天数
g.lag= 4 # 用过去多少期的财报回归
g.N = 20 # 需要前多少天的数据
g.num_stocks= 20 # 默认值选表现最好的20个股票
# 自己选取的一些因子
g.factors=['eps','adjusted_profit','inc_return','operation_profit_to_total_revenue',
'net_profit_margin','gross_profit_margin','ga_expense_to_total_revenue',
'goods_sale_and_service_to_revenue','inc_total_revenue_year_on_year']
#2
#设置中间变量
def set_variables():
g.t = 0 # 记录回测运行的天数
g.if_trade = False # 当天是否交易
a=get_all_trade_days()
g.ATD=['']*len(a) # 记录这个脚本已经运行的天数
for i in range(0,len(a)):
g.ATD[i]=a[i].isoformat()
#3
#设置回测条件
def set_backtest():
set_option('use_real_price',True) # 用真实价格交易
log.set_level('order','error') # 设置报错等级
'''
================================================================================
每天开盘前
================================================================================
'''
#每天开盘前要做的事情
def before_trading_start(context):
if g.t%g.tc==0:
#每g.tc天,交易一次
g.if_trade=True
# 设置手续费与手续费
set_slip_fee(context)
# 设置可行股票池:获得当前开盘的沪深300股票池并剔除当前或者计算样本期间停牌的股票
g.feasible_stocks = set_feasible_stocks(get_index_stocks('000300.XSHG'),g.N,context)
g.t+=1
#4
# 设置可行股票池:
# 过滤掉当日停牌的股票,且筛选出前days天未停牌股票
# 输入:stock_list为list类型,样本天数days为int类型,context(见API)
# 输出:list
def set_feasible_stocks(stock_list,days,context):
# 得到是否停牌信息的dataframe,停牌的1,未停牌得0
suspened_info_df = get_price(list(stock_list), start_date=context.current_dt, end_date=context.current_dt, frequency='daily', fields='paused')['paused'].T
# 过滤停牌股票 返回dataframe
unsuspened_index = suspened_info_df.iloc[:,0]<1
# 得到当日未停牌股票的代码list:
unsuspened_stocks = suspened_info_df[unsuspened_index].index
# 进一步,筛选出前days天未曾停牌的股票list:
feasible_stocks=[]
current_data=get_current_data()
for stock in unsuspened_stocks:
if sum(attribute_history(stock, days, unit='1d',fields=('paused'),skip_paused=False))[0]==0:
feasible_stocks.append(stock)
return feasible_stocks
#5
# 根据不同的时间段设置滑点与手续费
def set_slip_fee(context):
# 将滑点设置为0
set_slippage(FixedSlippage(0))
# 根据不同的时间段设置手续费
dt=context.current_dt
if dt>datetime.datetime(2013,1, 1):
set_commission(PerTrade(buy_cost=0.0003, sell_cost=0.0013, min_cost=5))
elif dt>datetime.datetime(2011,1, 1):
set_commission(PerTrade(buy_cost=0.001, sell_cost=0.002, min_cost=5))
elif dt>datetime.datetime(2009,1, 1):
set_commission(PerTrade(buy_cost=0.002, sell_cost=0.003, min_cost=5))
else:
set_commission(PerTrade(buy_cost=0.003, sell_cost=0.004, min_cost=5))
'''
================================================================================
每天交易时
================================================================================
'''
def handle_data(context, data):
if g.if_trade==True:
# 按照当前总资产决定分配到每个股票上面的资金
g.everyStock=context.portfolio.portfolio_value/g.num_stocks
# 依本策略,得到待买入的股票列表
toBuy = to_buy(context)
#执行卖出操作
order_sell(context,toBuy)
#执行买入操作
order_buy(toBuy)
g.if_trade = False
#6
# 依本策略,得到待买入的股票列表
# 输入:context(见聚宽API文档)
# 输出:待买入的股票列表-list类型
def to_buy(context):
# 获得今天的日期的字符串
todayStr=str(context.current_dt)[0:10]
# 获得当前的因子
current_factors=getOriginalFactors(g.factors,todayStr)
factors_table=getDS(g.feasible_stocks,todayStr,g.lag)
weights=linreg(factors_table[g.factors],factors_table[['return']])
betas=weights[1:]
# 假设上一期的回归参数对于下一期仍然适用,对股票下一期的收益进行估计
points=current_factors.dot(betas)+weights[0]
points.sort(ascending=False)
NoB=0
for i in range(0,g.num_stocks):
if points[i]>0:
NoB+=1
to_buy=array(points.index[0:NoB])
return to_buy
#7
# 执行卖出操作
# 输入:context,toBuy-list类型
# 输出:none
def order_sell(context,toBuy):
#如果现有持仓股票不在股票池,清空
list_position=context.portfolio.positions.keys()
#有持仓,但是不在要买的名单里
for i in range(0,len(g.feasible_stocks)):
if indexOf(g.feasible_stocks[i],toBuy)==-1 and indexOf(g.feasible_stocks[i],list_position)>-1:
order_target_value(g.feasible_stocks[i], 0)
return
#8
# 执行买入操作
# 输入:context,toBuy-list类型
# 输出:none
def order_buy(toBuy):
# 如果股票在待持仓列表,按所分配的份额持仓
for i in range(0,len(g.feasible_stocks)):
if indexOf(g.feasible_stocks[i],toBuy)>-1:
order_target_value(g.feasible_stocks[i], g.everyStock)
#9
# 查找一个元素在数组里面的位置,如果不存在,则返回-1
# 输入:a为list类型
# 输出:int类型
def indexOf(e,a):
for i in range(0,len(a)):
if e<=a[i]:
return i
return -1
#10
# 取数据的函数
# 输入:因子-list类型,日期d-str类型(XXXX-XX-XX)
# 输出df-dataframe类型
def getOriginalFactors(factors,d):
# 用于查询基本面数据的查询语句
q = query(valuation,balance,cash_flow,income,indicator).filter(valuation.code.in_(g.feasible_stocks))
# 获得股票的基本面数据,
df = get_fundamentals(q,d)
code=array(df['code'])
df = df[factors]
df.index=code
df = df.dropna()
return df
#11
# 这个函数用来协助进行线性回归的
# 输入:factors是所有股票的上一期因子值-list,returns是股票上一期的收益
# 输出:class类
def linreg(factors,returns):
# 加入一列常数列,表示市场的状态以及因子以外没有考虑到的因素
X=sm.add_constant(factors)
# 进行多元线性回归
results = regression.linear_model.OLS(returns, X).fit()
# 返回多元线性回归的参数值
return results.params
#12
# 获得有效的回归样本因子值和收益率。取前lags个季度的数据以及其之后一段时间的收益率
# 输入:stocks为list类型,dateStr为str类型,lags为int
# 输出:DataFrame
def getDS(stocks,dateStr,lags):
#生成一个空的dataframe,列除了day还有 pubDate
cols=g.factors+['day','pubDate','return']
code_list=[val+'_lag'+str(i) for val in stocks for i in range(lags)]
len_rows=len(code_list)
len_cols=len(cols)
table=np.zeros((len_rows,len_cols)) #生成0矩阵
table[:,:]=nan
table_factors=pd.DataFrame(table,columns=cols,index=code_list)
cols2=g.factors+['day','pubDate']
for i in stocks:
# 获得前一个交易日的日期(因为当前交易日你是不知道当前收盘价的)
D=getDay(dateStr,-1)
# 一个循环,对每一个财务季度进行获取因子
for j in range(0,lags):
# 查询财务因子的语句
q = query(indicator,valuation).filter(indicator.code.in_([i]))
f_temp=get_fundamentals(q,D)
#先判定是否上市,如果上市,才将其装入
row_name=i+'_lag'+str(j)
if len(f_temp)>0:
f_temp=f_temp[cols2]
f_temp.index=[row_name]
table_factors.ix[[row_name],cols2]=f_temp
#得到本期财报的披露日期向前推一个日期:
LD=getDay(table_factors['pubDate'][row_name],-1)
p1=getHistory(i,LD)
p2=getHistory(i,D)
# 获得两个日期隔了多少个交易日
getDayDifferent = indexOf(D,g.ATD)-indexOf(LD,g.ATD)
r=math.log(p2/p1)/getDayDifferent
table_factors.ix[[row_name],['return']]=r
else:
LD=D
D=LD
table_factors=table_factors.dropna() # 将所有含有nan项的行(每行代表一只股票)删除
return table_factors
#13
# 获得历史上某个股票某一天的收盘价
# 输入:stock为str类型,dateStr为str类型
# 输出:DataFrame
def getHistory(stock,dateStr):
# 获得股价数据
df = get_price(stock, start_date=dateStr, end_date=dateStr, frequency='daily', fields=['close'])
# 如果数据存在,那么返回当天收盘价
if len(df)>0:
return df['close'][0]
# 如果数据不存在,返回NaN
else:
return float("nan")
# 14
# 日期计算之获得某个日期之前或者之后dt个交易日的日期
# 输入:precent-str类型,dt-int类型
# 输出:日期-str
def getDay(precent,dt):
t_temp=indexOf(precent,g.ATD)
if t_temp+dt>=0:
return g.ATD[t_temp+dt]
else:
t= datetime.datetime.strptime(g.ATD[0],'%Y-%m-%d')+datetime.timedelta(days = dt)
t_str=datetime.datetime.strftime(t,'%Y-%m-%d')
return t_str
'''
================================================================================
每天收盘后
================================================================================
'''
# 每日收盘后要做的事情(本策略中不需要)
def after_trading_end(context):
return