Alpha对冲策略基础详解

前言

笔者最近开始学习量化投资,先从alpha对冲模型开始,主要通过搜索网上的文章进行研究,整体来说受益匪浅,要感谢各位分享的前辈。但是在阅读这些文章的过程中我发现,当前的一些教程存在着以下的不足之处:

(1).内容过于分散,不够系统化
(2).很多都只是利用因子回测,没有进行对冲
(3).对于初学者来说并不是非常友好,有些策略甚至无法成功运行或者代码已经过时
因而,我这里尝试对这些文章的内容进行总结,加上个人的补充,希望能够进行一个分享,同时对于自身也是一个提高。所有借鉴过的文章和书籍在文末都有列出。本文较为基础,并没有涉及复杂的交易策略。

策略运行环境

优矿平台

Alpha对冲策略简介

1.Alpha策略

Alpha策略最初是由William Sharpe在1964年的著作《投资组合理论与资本市场》中首次提出的,并指出投资者在市场中交易面临系统性风险和非系统性风险,用公式表达如下:

Rs=Rf+βs∗(Rm−Rf)
式中,Rs表示股票收益,Rf表示无风险收益率,Rm表示市场收益,βs表示股票相比于市场的波动程度,用以衡量股票的系统性风险。CAPM模型主要表示单个证券或投资组合同系统风险收益率之间的关系,也就是说,单个投资组合的收益率等于无风险收益率与风险溢价之和。
由于市场并非完全有效,个股仍存在alpha(超额收益)。 根据Jensen’s alpha的定义:αs=Rs−[Rf+βs∗(Rm−Rf)],除掉被市场解释的部分,超越市场基准的收益即为个股alpha。

2.Alpha对冲策略

Alpha对冲策略也被市场中性Alpha策略。通过衍生品来对冲投资组合的系统风险β,锁定超额收益Alpha。因此首先需要寻找稳定的Alpha,构建Alpha组合,进而计算组合的β来对冲风险。Alpha策略成功的关键就是寻找到一个超越基准(具有股指期货等做空工具的基准)的策略。比如,可以构造指数增强组合+沪深 300 指数期货空头策略。这种策略隐含的投资逻辑是择时比较困难,不想承受市场风险。

多因子策略代码解析

网上有很多单因子和多因子策略,在我阅读下来的感受就是依靠个人选择因子组合来实现较高年化收益率。通过学习和研读这些策略的源码,可以帮助我们较快地去掌握策略编写的过程。这里详细介绍下一个利用净利润增长率、权益收益率(ROE)、相对强弱指标(RSI)的多因子策略。这个策略的代码是我在《量化分析师的Python日记【第14天:如何在优矿上做Alpha对冲模型】》这篇文章的代码基础上进行改进的。

1.基础参数设置

1
2
3
4
5
6
end = '2015-08-01'                         # 回测结束时间
benchmark = 'HS300' # 策略参考标准
universe = set_universe('HS300') # 证券池,支持股票和基金
capital_base = 10000000 # 起始资金
freq = 'd' # 策略类型,'d'表示日间策略使用日线回测,'m'表示日内策略使用分钟线回测
refresh_rate = 1 # 调仓频率,表示执行handle_data的时间间隔,若freq = 'd'时间间隔的单位为交易日,若freq = 'm'时间间隔为分钟

(1)benchmark
为策略参照标准,即该量化策略回测结果的比较标准,通过比较可以大致看出策略的好坏。这里我们主要和HS300进行比较。这里比较常用的策略包括有SHCI(上证综指)、SH50(上证50)、
SH180(上证180)、HS300 (沪深300)和 ZZ500(中证500)。
(2)universe
universe 指策略回测的证券池,即策略逻辑作用的域,下单与历史数据获取都只限于universe中的证券。这个策略使用的是set_universe函数。实际上uqer上支持用以下三种方式获得政权池:DynamicUniverse、set_universe 和 StockScreener。实际策略的回测中,我们建议用DynamicUniverse的方式来获得证券池。后者用于返回动态证券池实例。在使用板块成分股、指数成分股或行业成分股作为策略的交易对象时,策略框架会根据实际情况调整当天股票池的内容,避免幸存者偏差。

2.日期设置

1
2
3
4
5
6
7
# 构建日期列表
data=DataAPI.TradeCalGet(exchangeCD=u"XSHG",beginDate=u"20110801",endDate=u"20150801",field=['calendarDate','isMonthEnd'],pandas="1")
data = data[data['isMonthEnd'] == 1]
date_list = data['calendarDate'].values.tolist()

cal = Calendar('China.SSE')
period = Period('-1B')

这部分比较简单,其实就是获取每个月的最后一天,收集一个日期集,作为交易信号。

3.帐户配置

1
2
3
accounts = {
'fantasy_account': AccountConfig(account_type='security', capital_base=10000000)
}

这里简单配置了一个股票帐户。事实上,这里可配置的帐户类型还可以有期货、场外基金等。并且可以设置包括滑点在内的多个参数。

4.帐户初始化和执行交易指令

1
2
3
4
def initialize(context):                   # 初始化虚拟账户状态
pass
def handle_data(context): # 每个交易日的买入卖出指令
#...

在uqer上,只需要定义initialize和handle_data这两个函数,系统就会帮助你执行交易策略了,具体的交易逻辑都是写在handle_data当中,也可以自己定义一些函数。

5.获取因子

(1)净利润增长率
1
NetProfitGrowRate =DataAPI.MktStockFactorsOneDayGet(tradeDate=yesterday.strftime('%Y%m%d'),secID=universe,field=u"secID,NetProfitGrowRate",pandas="1")

利用函数DataAPI.MktStockFactorsOneDayGet可以获取到我们想要的因子,这里我们获取到了净利润增长率。打印下结果是这样子的。

1
2
3
4
5
           secID  NetProfitGrowRate
0 000001.XSHE 0.4009
1 000002.XSHE 0.4955
2 000063.XSHE -1.1409
...
1
2
3
4
5
NetProfitGrowRate.columns = ['secID','NetProfitGrowRate']
NetProfitGrowRate['ticker'] = NetProfitGrowRate['secID'].apply(lambda x: x[0:6])
NetProfitGrowRate.set_index('ticker',inplace=True)
ep = NetProfitGrowRate['NetProfitGrowRate'].dropna().to_dict()
signal_NetProfitGrowRate = standardize(neutralize(winsorize(ep),yesterday.strftime('%Y%m%d')))

接下来,对数据进行排序,对数值进行一系列的去极值、标准化等的操作。

(2)ROE

1
2
3
4
5
6
ROE = DataAPI.MktStockFactorsOneDayGet(tradeDate=yesterday.strftime('%Y%m%d'),secID=universe,field=u"secID,ROE",pandas="1")
ROE.columns = ['secID','ROE']
ROE['ticker'] = ROE['secID'].apply(lambda x: x[0:6])
ROE.set_index('ticker',inplace=True)
ep = ROE['ROE'].dropna().to_dict()
signal_ROE = standardize(neutralize(winsorize(ep),yesterday.strftime('%Y%m%d'))) # 对因子进行去极值、中性化、标准化处理得信号

(3)RSI

1
2
3
4
5
6
7
8
RSI = DataAPI.MktStockFactorsOneDayGet(tradeDate=yesterday.strftime('%Y%m%d'),secID=universe,field=u"secID,RSI",pandas="1")
RSI.columns = ['secID','RSI']
RSI['ticker'] = RSI['secID'].apply(lambda x: x[0:6])
RSI.set_index('ticker',inplace=True)
ep = RSI['RSI'].dropna().to_dict()
if len(ep) == 0 :
return
signal_RSI = standardize(neutralize(winsorize(ep),yesterday.strftime('%Y%m%d'))) # 对因子进行去极值、中性化、标准化处理得信号

6.构建score矩阵

1
2
3
4
5
6
weight = np.array([0.4, 0.3, 0.3])   # 信号合成,各因子权重
Total_Score = pd.DataFrame(index=RSI.index, columns=['NetProfitGrowRate','ROE','RSI'], data=0)
Total_Score['NetProfitGrowRate'][signal_NetProfitGrowRate.keys()] = signal_NetProfitGrowRate.values()
Total_Score['ROE'][signal_ROE.keys()] = signal_ROE.values()
Total_Score['RSI'][signal_RSI.keys()] = signal_RSI.values()
Total_Score['total_score'] = np.dot(Total_Score, weight)

我们获得了三个因子之后,还需要利用这三个因子选择股票。因而,这里设置了一个权重来选股。通过改变权重,策略也会相应改变,可以试着尝试不同的权重来看看年化收益率的变化。

7.获取目标股票

1
2
total_score = Total_Score['total_score'].to_dict()
wts = simple_long_only(total_score, today.strftime('%Y%m%d')) # 调用组合构建函数,组合构建综合考虑各因子大小,行业配置等因素,默认返回前30%的股票

这里主要用到的是simple_long_only这个函数,返回了利用total_score获得的排名前30%的股票。

8.获取目标股票的SecID

1
2
3
4
5
6
RSI['wts'] = np.nan
RSI['wts'][wts.keys()] = wts.values()
RSI = RSI[~np.isnan(RSI['wts'])]
RSI.set_index('secID', inplace=True)
RSI.drop('RSI', axis=1, inplace=True)
buy_list = RSI.index

这里的操作就是获取目标股票的SecID,后面买操作的时候需要。这里是利用RSI这个DataFrame来进行操作的。

9.买卖股票

1
2
3
4
5
6
7
8
# 先卖出
sell_list = account.get_positions()
for stk in sell_list:
order_to(stk, 0)

# 再买入
for stk in buy_list:
account.order_pct_to(stk, 1.0/len(buy_list))

这里的策略很简单,直接全卖了,然后再把buylist里面的股票买一遍。看到这里的话,整个策略的逻辑已经非常清晰了,如果有其他的策略,可以在这个模板的基础上进行修改。我这里附上整个完整的策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from CAL.PyCAL import *
import numpy as np
import pandas as pd

end = '2015-08-01' # 回测结束时间
benchmark = 'HS300' # 策略参考标准
universe = set_universe('HS300') # 证券池,支持股票和基金
capital_base = 10000000 # 起始资金
freq = 'd' # 策略类型,'d'表示日间策略使用日线回测,'m'表示日内策略使用分钟线回测
refresh_rate = 1 # 调仓频率,表示执行handle_data的时间间隔,若freq = 'd'时间间隔的单位为交易日,若freq = 'm'时间间隔为分钟

# 构建日期列表
data=DataAPI.TradeCalGet(exchangeCD=u"XSHG",beginDate=u"20110801",endDate=u"20150801",field=['calendarDate','isMonthEnd'],pandas="1")
data = data[data['isMonthEnd'] == 1]
date_list = data['calendarDate'].values.tolist()
cal = Calendar('China.SSE')
period = Period('-1B')

accounts = {
'fantasy_account': AccountConfig(account_type='security', capital_base=10000000)
}

def initialize(context): # 初始化虚拟账户状态
pass

def handle_data(context): # 每个交易日的买入卖出指令

account = context.get_account('fantasy_account')
today = Date.fromDateTime(context.current_date)
yesterday = context.previous_date

if yesterday.strftime("%Y-%m-%d") in date_list:
# 净利润增长率
NetProfitGrowRate =DataAPI.MktStockFactorsOneDayGet(tradeDate=yesterday.strftime('%Y%m%d'),secID=universe,field=u"secID,NetProfitGrowRate",pandas="1")
NetProfitGrowRate.columns = ['secID','NetProfitGrowRate']
NetProfitGrowRate['ticker'] = NetProfitGrowRate['secID'].apply(lambda x: x[0:6])
NetProfitGrowRate.set_index('ticker',inplace=True)
ep = NetProfitGrowRate['NetProfitGrowRate'].dropna().to_dict()
signal_NetProfitGrowRate = standardize(neutralize(winsorize(ep),yesterday.strftime('%Y%m%d'))) # 对因子进行去极值、中性化、标准化处理得信号

# 权益收益率
ROE = DataAPI.MktStockFactorsOneDayGet(tradeDate=yesterday.strftime('%Y%m%d'),secID=universe,field=u"secID,ROE",pandas="1")
ROE.columns = ['secID','ROE']
ROE['ticker'] = ROE['secID'].apply(lambda x: x[0:6])
ROE.set_index('ticker',inplace=True)
ep = ROE['ROE'].dropna().to_dict()
signal_ROE = standardize(neutralize(winsorize(ep),yesterday.strftime('%Y%m%d'))) # 对因子进行去极值、中性化、标准化处理得信号

# RSI
RSI = DataAPI.MktStockFactorsOneDayGet(tradeDate=yesterday.strftime('%Y%m%d'),secID=universe,field=u"secID,RSI",pandas="1")
RSI.columns = ['secID','RSI']
RSI['ticker'] = RSI['secID'].apply(lambda x: x[0:6])
RSI.set_index('ticker',inplace=True)
ep = RSI['RSI'].dropna().to_dict()
if len(ep) == 0 :
return
signal_RSI = standardize(neutralize(winsorize(ep),yesterday.strftime('%Y%m%d'))) # 对因子进行去极值、中性化、标准化处理得信号

# 构建组合score矩阵
weight = np.array([0.4, 0.3, 0.3]) # 信号合成,各因子权重
Total_Score = pd.DataFrame(index=RSI.index, columns=['NetProfitGrowRate','ROE','RSI'], data=0)
Total_Score['NetProfitGrowRate'][signal_NetProfitGrowRate.keys()] = signal_NetProfitGrowRate.values()
Total_Score['ROE'][signal_ROE.keys()] = signal_ROE.values()
Total_Score['RSI'][signal_RSI.keys()] = signal_RSI.values()
Total_Score['total_score'] = np.dot(Total_Score, weight)

total_score = Total_Score['total_score'].to_dict()
wts = simple_long_only(total_score, today.strftime('%Y%m%d')) # 调用组合构建函数,组合构建综合考虑各因子大小,行业配置等因素,默认返回前30%的股票

# 找载体,将ticker转化为secID
RSI['wts'] = np.nan
RSI['wts'][wts.keys()] = wts.values()
RSI = RSI[~np.isnan(RSI['wts'])]
RSI.set_index('secID', inplace=True)
RSI.drop('RSI', axis=1, inplace=True)
buy_list = RSI.index
print(type(RSI))

# 先卖出
sell_list = account.get_positions()
for stk in sell_list:
order_to(stk, 0)

# 再买入
for stk in buy_list:
account.order_pct_to(stk, 1.0/len(buy_list))

else:
return

策略的回测结果如图。

Alpha对冲策略

在了解了如何利用多因子策略后,我们就可以加入购买期货合约的部分来实现一个对冲了。而2015年的大涨和大跌可以作为我们进行检验回测的时间段。下面用来讲解策略的代码主要来自于文章《多因子股票-股指期货 多空对冲策略 小白解读》。讲解的内容则主要是期货部分的代码。

1.帐户设置

1
2
3
4
accounts = {
'stock_account': AccountConfig(account_type='security', capital_base=capital_base, commission=stock_commission, slippage=slippage),
'futures_account': AccountConfig(account_type='futures', capital_base=capital_base, commission=futures_commission, slippage=slippage)
} #设定股票和股指期货账户

帐户这里多了一个期货帐户,主要是为了后面增加期货交易的逻辑。

2.获取当前合约持仓状态

1
2
3
4
5
future_account = context.get_account("futures_account")
contract_current_month = context.get_symbol('IFL0')
contract_holding = context.contract_holding
if not contract_holding:
contract_holding = contract_current_month

这里IFL0代表的是当月合约。如果当前没有持仓合约,那么就准备购买当月主力合约。

3.对持有合约进行处理

1
2
last_trade_date = get_asset(contract_holding).last_trade_date 
days_to_expire = (last_trade_date- context.current_date).days

先取得合约的最后交割日,计算下离今天还有多久。因为在交割日前波动大,为了降低风险,我们选择在交割日前3天平仓。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if days_to_expire == 3:   #接近交割日股指期货一般波动异常,所以一般要提前几天交割     
log.info(u'距离%s到期,还有%s天' % (contract_holding, days_to_expire))
contract_next_month = context.get_symbol('IFL1') #取次月合约的代码,这里是'IF1502'

futures_position = future_account.get_position(contract_holding) #取当月合约的持仓量

if futures_position: #不空,即有持仓
current_holding = futures_position.short_amount #将空单持仓量赋给当前持仓current_holding
log.info(u'移仓换月。[平仓旧合约:%s,开仓新合约:%s,手数:%s]' % (contract_holding, contract_next_month, int(current_holding)))

if current_holding == 0: #没有持仓,则不用进行下面的换仓动作
return

future_account.order(contract_holding, current_holding, "close") #将当月合约以收盘价进行平仓(以close价买入),没有考虑滑点?
future_account.order(contract_next_month, -1 * current_holding, "open") #以开盘价重新在下月合约重新开仓(以open价卖出current_holding量),持仓量保持不变
context.contract_holding = contract_next_month #在context中修改当前持仓的属性,即当月持仓变为'IF1502'
return

接下来的操作比较简单,如果快到交割日了,获取当前持仓量,全平后以相同的数量买进下月的期货。

4.对当前持有股票进行对冲

1
2
3
4
5
6
7
8
contract_current_month = context.get_symbol('IFL0')
multiplier = get_asset(contract_current_month).multiplier # get_asset('IF1501')
futures_price = context.current_price(contract_current_month) #取合约价格
total_hedging_amount = int(stock_positions_value / futures_price / multiplier) #由股票市值折算成等量的股指市值,算出股指期货等量的手数,实际上是对冲,是低风险的策略
log.info(u'%s没有持仓,准备建仓。空头开仓%s手' % (contract_current_month, contract_holding))
future_account.order(contract_current_month, -1 * total_hedging_amount, "open") #建立空头仓位,即卖出开仓
context.contract_holding = contract_current_month #登记持有的仓位
print(context.contract_holding)

如果当前的股票没有对冲,那么就购买期货合约进行对冲。这里需要先获取一个合约乘数,计算出要购买的合约的数量,也就是total_hedging_amount。然后购买相应数量的期货合约。

5.对期货进行调仓

1
2
3
4
5
6
7
8
9
10
11
12
13
total_hedging_amount = int(stock_positions_value / futures_price / multiplier) #计算对冲总数量
hedging_amount_diff = total_hedging_amount - futures_position.short_amount #由于市场变化,造成原对冲的手数不足,需要计算补充差量

# 调仓阈值,可以适当放大,防止反复调仓
threshold = 2
if hedging_amount_diff >= threshold:
log.info(u'空头调仓。[合约名:%s,当前空头手数:%s,目标空头手数:%s]' % (contract_holding, int(futures_position.short_amount), total_hedging_amount))
# 多开空仓
future_account.order(contract_holding, -1 * int(hedging_amount_diff), "open") #对差值补充开空仓
elif hedging_amount_diff <= -threshold:
log.info(u'空头调仓。[合约名:%s,当前空头手数:%s,目标空头手数:%s]' % (contract_holding, int(futures_position.short_amount), total_hedging_amount))
# 平掉部分空仓
future_account.order(contract_holding, int(abs(hedging_amount_diff)), "close") #差值为负,表明股票的市值低于期货的市值,需要平掉相应的期货差额手数

在当前有持仓的情况下,判断是否需要调整。和上面一样,计算出一个total_hedging_amount,并计算出和当前持仓量的差异,如果差异大过阈值2,就需要进行一个调整。比较尴尬的是,这个策略在某书籍中给出的时候没有测量15年全年的收益,而是测了半年,所以书中的收益率很高,在我将事件设置为全年后,模型的年化收益率感人。

整体的期货逻辑就是这样子,原文对每行代码有详细解读,如果想继续深入看的可以看看这个链接的内容:
https://uqer.io/v3/community/share/594aa088fba7a70057b0c0e6

Fama-French三因子模型对冲

Fama和French 1993年指出可以建立一个三因子模型来解释股票回报率。模型认为,一个投资组合(包括单个股票)的超额回报率可由它对三个因子的暴露来解释,这三个因子是:市场资产组合(Rm− Rf)、市值因子(SMB)、账面市值比因子(HML)。
β、si和hi分别是三个因子的系数,回归模型表示如下:

1
Rit− Rft= ai+ βi(Rmt− Rft) + SiSMBt+ hiHMIt+ εit

在文章 破解Alpha对冲策略——观《量化分析师Python日记第14天》有感 的最后,作者给出了这个三因子模型,实际上作者选择的是PE 和 LFLO这两个因子来选股。整个模型获得了不错的年化收益率。于是我大胆想象,如果将这个三因子模型放到对冲模型中,会有怎样的效果。事实就是,在收益率在上面的对冲模型的基础上进一步下跌了。如果有兴趣的朋友可以试下我下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
from CAL.PyCAL import *
import numpy as np
from pandas import DataFrame
import pandas as pd

start = '2015-01-01' # 回测起始时间
end = '2015-12-31' # 回测结束时间
benchmark = 'HS300' # 策略参考标准
universe = DynamicUniverse('HS300') + ['IFL0', 'IFL1'] # 证券池,支持股票和基金
capital_base = 10000000 # 起始资金
freq = 'd' # 策略类型,'d'表示日间策略使用日线回测,'m'表示日内策略使用分钟线回测
refresh_rate = 1
cal = Calendar('China.SSE')
period = Period('-1B')

# 账户初始化配置
stock_commission = Commission(buycost=0.0, sellcost=0.0, unit='perValue')
futures_commission = Commission(buycost=0.0, sellcost=0.0, unit='perValue')
slippage = Slippage(value=0, unit='perValue')

accounts = {
'stock_account': AccountConfig(account_type='security', capital_base=capital_base, commission=stock_commission, slippage=slippage),
'futures_account': AccountConfig(account_type='futures', capital_base=capital_base, commission=futures_commission, slippage=slippage)
}

#策略算法
def initialize(context):
context.signal_generator = SignalGenerator(Signal('NetProfitGrowRate'), Signal('ROE'), Signal('RSI'))
context.need_to_switch_position = False
context.contract_holding = ''

def handle_data(context):
# log.info(context.current_date)
universe = context.get_universe(exclude_halt=True)

yesterday = context.previous_date

Factor1 = DataAPI.MktStockFactorsOneDayGet(tradeDate=yesterday,secID=universe[:400],field=['secID','PE','LFLO'],pandas="1")
Factor2 = DataAPI.MktStockFactorsOneDayGet(tradeDate=yesterday,secID=universe[400:],field=['secID','PE','LFLO'],pandas="1")
Factor = pd.concat([Factor1, Factor2])
Factor['ticker'] = Factor['secID'].apply(lambda x: x[0:6])
Factor.set_index('ticker',inplace=True)

# 市盈率PE
Factor['PE'] = 1.0 / Factor['PE'] # 低市盈率
factor = Factor['PE'].dropna().to_dict()
signal_PE = standardize(neutralize(winsorize(factor),yesterday)) # 因子处理

# 对数流通市值LFLO
Factor['LFLO'] = 1.0 / Factor['LFLO'] # 低市值
factor = Factor['LFLO'].dropna().to_dict()
signal_LFLO = standardize(neutralize(winsorize(factor),yesterday)) # 因子处理

# 构建组合score矩阵
Total_Score = pd.DataFrame(index=Factor.index, columns=['PE','LFLO'], data=0)
Total_Score = Total_Score.loc[~Total_Score.index.duplicated(keep='first')]
Total_Score['PE'][signal_PE.keys()] = signal_PE.values()
Total_Score['LFLO'][signal_LFLO.keys()] = signal_LFLO.values()
Total_Score['total_score'] = np.dot(Total_Score, np.array([0.5, 0.5])) # 综合两个因子的大小,不失一般性,等权求和
total_score = Total_Score['total_score'].to_dict()

wts = simple_long_only(total_score,yesterday) # 组合构建函数
Factor= Factor.loc[~Factor.index.duplicated(keep='first')]
Factor['wts'] = np.nan
Factor['wts'][wts.keys()] = wts.values()
Factor = Factor[~np.isnan(Factor['wts'])]
Factor.set_index('secID', inplace=True)
Factor.drop(['PE','LFLO'], axis=1, inplace=True)
buy_list = Factor.index

handle_stock_orders(context, wts,buy_list)
handle_futures_orders(context)
# handle_futures_position_switch(context)

# 订单委托
def handle_stock_orders(context, wts, buy_list):
account = context.get_account('stock_account')

# 先卖出
sell_list = account.get_positions()
for stk in sell_list:
account.order_to(stk, 0)

# 再买入
total_money = account.portfolio_value
prices = account.reference_price
for stk in buy_list:
try:
if np.isnan(prices[stk]) or prices[stk] == 0: # 停牌或者还没有上市等原因不能交易
continue
except:
continue
account.order(stk, int(total_money * wts[stk[0:6]] / prices[stk] /100)*100)

def handle_futures_orders(context):
stock_account = context.get_account('stock_account')
future_account = context.get_account("futures_account")

# 将主力连续合约映射为实际合约
contract_current_month = context.get_symbol('IFL0')

# 判断是否需要移仓换月
contract_holding = context.contract_holding
if not contract_holding:
contract_holding = contract_current_month

if contract_holding:
last_trade_date = get_asset(contract_holding).last_trade_date

# 当月合约离交割日只有3天
days_to_expire = (last_trade_date- context.current_date).days
if days_to_expire == 3:
log.info(u'距离%s到期,还有%s天' % (contract_holding, days_to_expire))
contract_next_month = context.get_symbol('IFL1')
futures_position = future_account.get_position(contract_holding)
if futures_position:
current_holding = futures_position.short_amount
log.info(u'移仓换月。[平仓旧合约:%s,开仓新合约:%s,手数:%s]' % (contract_holding, contract_next_month, int(current_holding)))
if current_holding == 0:
return
future_account.order(contract_holding, current_holding, "close")
future_account.order(contract_next_month, -1 * current_holding, "open")
context.contract_holding = contract_next_month
return
stock_position = stock_account.get_positions()

# 有多头股票仓位,使用期货进行空头对冲
if stock_position:
stock_positions_value = stock_account.portfolio_value - stock_account.cash
# print u'当前股票多头市值', stock_positions_value
futures_position = future_account.get_position(contract_holding)

# 没有空头持仓,则建仓进行对冲
if not futures_position:
contract_current_month = context.get_symbol('IFL0')
multiplier = get_asset(contract_current_month).multiplier
futures_price = context.current_price(contract_current_month)
total_hedging_amount = int(stock_positions_value / futures_price / multiplier)
log.info(u'%s没有持仓,准备建仓。空头开仓%s手' % (contract_current_month, contract_holding))
future_account.order(contract_current_month, -1 * total_hedging_amount, "open")
context.contract_holding = contract_current_month

# 已经有空头持仓,则判断是否需要调仓
else:
contract_holding = context.contract_holding
contract_current_month = context.get_symbol('IFL0')
futures_price = context.current_price(contract_current_month)
multiplier = get_asset(contract_holding).multiplier
# 计算当前对冲需要的期货手数
total_hedging_amount = int(stock_positions_value / futures_price / multiplier)
hedging_amount_diff = total_hedging_amount - futures_position.short_amount
# 调仓阈值,可以适当放大,防止反复调仓
threshold = 2
if hedging_amount_diff >= threshold:
log.info(u'空头调仓。[合约名:%s,当前空头手数:%s,目标空头手数:%s]' % (contract_holding, int(futures_position.short_amount), total_hedging_amount))
# 多开空仓
future_account.order(contract_holding, -1 * int(hedging_amount_diff), "open")
elif hedging_amount_diff <= -threshold:
log.info(u'空头调仓。[合约名:%s,当前空头手数:%s,目标空头手数:%s]' % (contract_holding, int(futures_position.short_amount), total_hedging_amount))
# 平掉部分空仓
future_account.order(contract_holding, int(abs(hedging_amount_diff)), "close")

由此可见,找到一个好的策略实在是非常困难。

总结

此处我分析了多因子策略、对冲策略、三因子模型对冲,总结下来,其实当前很多策略代码都是加过某种“修饰”,具体效果并不好。后期我的研究可能会有以下内容

1.学习更多策略
2.尝试不用平台实现自己的回测框架

参考资料

1.https://uqer.io/v3/community/share/5b07797b8d8dd2011405ce10 2.https://uqer.io/v3/community/share/55e662f9f9f06c1ea481f9cf 3.https://uqer.io/v3/community/share/55ff6ce9f9f06c597265ef04 4.https://mp.weixin.qq.com/s/a_yWHV9qzlPsSY7qX4dKBg 5.https://uqer.io/v3/community/share/56893bb1228e5b67159beb38 6.https://uqer.io/v3/community/share/57693102228e5b8192a559ff 7.https://uqer.io/v3/community/share/57c6ac5a228e5b6d237c771e 8.https://uqer.io/v3/community/share/594aa088fba7a70057b0c0e6