文章目录
一、概述
该文主要是对这段时间在BigQuant量化投资平台上尝试的一些方法进行的总结,包括基于技术指标的策略和基于QP的策略。
二、BigQuant平台编程策略介绍
BigQuant平台提供了一套完整的用于编写,执行,回测量化策略的接口,我们用到的主要包括以下几个方法:
1. M.trade
该模块主要用于回测,即输出指定策略的测试结果,使用方法如下:
m = M.trade.v2(
instruments = instruments,
start_date = test_start_date,
end_date = test_end_date,
initialize = initialize,
handle_data = handle_data,
order_price_field_buy = 'open',
order_price_field_sell = 'open',
capital_base = 100000
)
其中,instruments表示总的股票池,start_date和end_date表示回测时间段,initialize为初始化函数,handle_date是在模拟回测过程中每天都会自动回调的函数,order_price_field_buy和order_price_field_sell分别表示买入和卖出股票的时机,capital_base表示初始资金。
接口的详细介绍可见:big_quant_trade
2. D.history_data
该接口提供了所有的股票历史数据,使用方法如下:
data = D.history_data(
instruments,
start_date,
end_data,
fields
)
其中,instruments表示选定的股票池,start_date和end_date表示时间段,fields一般为[‘open’]开盘时间和[‘close’]闭盘时间。
接口的详细介绍可见:big_quant_history_data
二、基于技术指标的策略
1. 策略:MACD金叉和MA多头策略
以下表格定义我们实施策略的实验条件:
条件名称 | 值 |
---|---|
初始金额 | 100000元 |
调仓周期 | 20天 |
最大持仓股票数量 | 10支 |
清仓日 | T |
调仓日 | T+1(在清仓第二天执行买入) |
股票权重策略 | 所有要买入的股票均以相同权重买入 |
回测起始时间 | 2019年1月3日 |
回测结束时间 | 2021年1月22日 |
交易费率 | 0.03%(最低5元) |
策略描述:
在清仓日,将所持所有股票清仓,然后根据MACD金叉策略/MA多头策略选定明天要买入的股票;在调仓日,将所持现金均分到最多10份(根据要买入的股票数量决定),购入选好的股票;持仓周期内不进行调仓或清仓。
除此之外,我们使用随机选股策略作为对照组,用于表示指标策略是否有用。
MACD金叉策略:
MACD包含DIF线和DEA线,DIF线是12日的EMA线减去26日的EMA线,DEA是9日的DIF值的EMA线,当DIF上穿DEA时,为MACD金叉。
MA多头策略:
当MA5大于MA20时,为MA多头。
策略代码:
# 手续费
context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0003, min_cost=5))
# MACD 均线参数
context.dea_period = 9
context.dif_short_period = 12
context.dif_long_period = 26
# MA 均线参数
context.ma_short_period = 5
context.ma_long_period = 20
# 调仓周期
context.transfer_period = 20
# 购买清单
context.buy_list = []
def handle_data(context, data):
# 根据时间判断状态
condition = context.trading_day_index % context.transfer_period
if condition == 1:
# 购买
cash = context.portfolio.cash
order_per_stock = cash / len(context.buy_list[:10])
for sid in context.buy_list[:10]:
latest_price = data.current(sid, 'price')
amount = int(order_per_stock / latest_price / 100) * 100
if amount > 0:
context.order(sid, amount)
elif condition == 0:
# 清仓
for sid in context.buy_list[:10]:
if data.can_trade(sid):
context.order_target_percent(sid, 0)
context.buy_list = []
# 选股
for i in range(len(instruments)):
k = instruments[i]
sid = context.symbol(k)
# MACD选股
prices_history = data.history(sid, 'price', context.dif_long_period + context.dea_period, '1d')
dea = 0
deas = []
difs = []
for j, day_index in enumerate(range(context.dif_long_period, len(prices_history))):
prices_ema_short = prices_history[day_index-context.dif_short_period:day_index]
prices_ema_long = prices_history[day_index-context.dif_long_period:day_index]
assert(len(prices_ema_short) == context.dif_short_period)
assert(len(prices_ema_long) == context.dif_long_period)
ema_short = prices_ema_short[0]
ema_long = prices_ema_long[0]
for k in range(1, len(prices_ema_short)):
ema_short = (2*prices_ema_short[k] + k*ema_short) / (k+2)
for k in range(1, len(prices_ema_long)):
ema_long = (2*prices_ema_long[k] + k*ema_long) / (k+2)
dif = ema_short - ema_long
dea = (2*dif + j*dea) / (j+2)
deas.append(dea)
difs.append(dif)
# ma 多头选股
ma_short = data.history(sid, 'price', context.ma_short_period, '1d').mean()
ma_long = data.history(sid, 'price', context.ma_long_period, '1d').mean()
# 1. 单纯使用macd金叉
#if difs[-1] > deas[-1] and difs[-2] < deas[-2]:
# context.buy_list.append(sid)
# 2. 单纯使用ma多头
#if ma_short > ma_long:
# context.buy_list.append(sid)
# 3. macd金叉+ma多头
if difs[-1] > deas[-1] and difs[-2] < deas[-2] and ma_short > ma_long:
context.buy_list.append(sid)
策略结果:
1)随机选股结果:
2)只选择MA多头的策略
3)只选择MACD金叉策略
4)既要求有MACD金叉,又要求有MA多头
策略分析:
1)从该轮实验结果中可见,随机选股的策略表现没有MACD策略和MA策略好,而MACD策略又没有MA策略效果好;
2)从持仓占比可见,符合MACD金叉的股票数量其实非常少,有一段时间甚至是出现了无持仓的情况,这个主要是因为金叉这个定义本身就具有一定的稀有性;
3)该方法的收益稳定性是不能保证的,即我们有可能通过调整回测时间范围,使得收益率发生巨大变化,比如以下的回测结果是将起始时间调整到2019年1月1日的MACD金叉收益。
2. 策略1的改进
策略内容:
根据上面的一些思考,我们可以对策略1的MACD金叉策略进行改进:
- 将调仓周期从20天改到15天,以提高MACD的命中率;
- 如果没有任何股票符合条件的话,我们可以保持持仓。
策略代码:
# 手续费
context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0003, min_cost=5))
# MACD 均线参数
context.dea_period = 9
context.dif_short_period = 12
context.dif_long_period = 26
# MA 均线参数
context.ma_short_period = 5
context.ma_long_period = 20
# 调仓周期
context.transfer_period = 15
# 购买清单
context.buy_list = []
# 是否继续持仓
context.keep_holdings = False
def handle_data_hold_when_no_choice(context, data):
# 根据时间判断状态
condition = context.trading_day_index % context.transfer_period
if condition == 1 and not context.keep_holdings:
# 购买
cash = context.portfolio.cash
order_per_stock = cash / len(context.buy_list[:10])
for sid in context.buy_list[:10]:
latest_price = data.current(sid, 'price')
amount = int(order_per_stock / latest_price / 100) * 100
if amount > 0:
context.order(sid, amount)
elif condition == 0:
buy_list = []
# 选股
for i in range(len(instruments)):
k = instruments[i]
sid = context.symbol(k)
# MACD选股
prices_history = data.history(sid, 'price', context.dif_long_period + context.dea_period, '1d')
dea = 0
deas = []
difs = []
for j, day_index in enumerate(range(context.dif_long_period, len(prices_history))):
prices_ema_short = prices_history[day_index-context.dif_short_period:day_index]
prices_ema_long = prices_history[day_index-context.dif_long_period:day_index]
ema_short = prices_ema_short[0]
ema_long = prices_ema_long[0]
for k in range(1, len(prices_ema_short)):
ema_short = (2*prices_ema_short[k] + k*ema_short) / (k+2)
for k in range(1, len(prices_ema_long)):
ema_long = (2*prices_ema_long[k] + k*ema_long) / (k+2)
dif = ema_short - ema_long
dea = (2*dif + j*dea) / (j+2)
deas.append(dea)
difs.append(dif)
# macd金叉
if difs[-1] > deas[-1] and difs[-2] < deas[-2]:
buy_list.append(sid)
if len(buy_list) == 0:
context.keep_holdings = True
else:
context.keep_holdings = False
# 清仓
for sid in context.buy_list[:10]:
if data.can_trade(sid):
context.order_target_percent(sid, 0)
context.buy_list = buy_list
策略结果:
策略分析:
1)策略调整后,收益率有一定的提升,但实际上离基础收益(大盘)还有不小的差距,可见单纯依靠该策略是很难获得令人满意的结果的;
2)MACD金叉和MA多头等策略实际上都具有一定的滞后性,即在股票已经涨了一段时间以后才能反映出来,而且对于更长远的判断,如顶背离,底背离等现象是没有判断的,所以有可能会在股票的高点进入,从而导致亏损。
三、基于QP优化的策略
1. 经典QP优化
以下表格定义我们实施策略的实验条件:
条件名称 | 值 |
---|---|
初始金额 | 100000元 |
调仓周期 | 60天 |
最大持仓股票数量 | 无限制 |
清仓日 | 无清仓日 |
股票权重策略 | 股票以QP解输出的权重进行调仓 |
回测起始时间 | 2019年1月3日 |
回测结束时间 | 2021年1月22日 |
交易费率 | 0.03%(最低5元) |
平均收益计算窗口 | 5天 |
策略描述:
我们使用二次规划(QP),将选股定义为最大化收益,且最小化风险的一个优化问题,即
m
i
n
i
m
i
z
e
−
μ
T
x
+
λ
1
2
x
T
σ
x
minimize \ \ \ \ -\mu^{T}x+\lambda\frac{1}{2}x^{T}\sigma x
minimize −μTx+λ21xTσx
S
.
t
.
∑
x
i
=
1
S.t.\ \ \ \sum{x_i} =1
S.t. ∑xi=1
x
i
>
=
0
;
i
=
1
,
.
.
.
,
n
x_i >= 0;\ \ i=1,...,n
xi>=0; i=1,...,n
其中,
μ
\mu
μ为股票的平均收益,在这里是每个股票每隔5天计算出来的5日平均收益率,
x
x
x为我们最终要求的值,表示每支股票购买的权重,其满足
x
i
x_i
xi必须大于等于0,且和为1,
σ
\sigma
σ为所有股票收益两两之间的协方差,即股票风险。
策略分析与代码:
通过优化方法选股的一个很重要的可视化方法,就是画出收益率和风险的图。
如下代码,我们随机选取20支股票,计算其在指定时段每隔5个工作日获得的收益,然后随机生成10000个策略,输出的每个策略的收益率和波动性的散点图:
import numpy as np
import matplotlib.pyplot as plt
import cvxopt as opt
from cvxopt import blas, solvers
import pandas as pd
np.random.seed(123)
# 关掉进度展示,进度展示是运行过程进度的一个打印输出,可以通过其查看代码运行进度
solvers.options['show_progress'] = False
# 产生随机权重的函数
def rand_weights(n):
''' Produces n random weights that sum to 1 '''
k = np.random.rand(n)
return k / sum(k)
# 返回组合收益率和波动性
def portfolio(returns):
p = np.asmatrix(np.mean(returns, axis=1))
w = np.asmatrix(rand_weights(returns.shape[0]))
C = np.asmatrix(np.cov(returns))
profit = w * p.T
risk = np.sqrt(w * C * w.T)
return profit, risk
# 获取收益
subsets = [random.choice(instruments) for _ in range(20)]
#data = D.history_data(subsets, train_start_date, train_end_date, fields=['close'])
data = D.history_data(subsets, test_start_date, test_end_date, fields=['close'])
data = pd.pivot_table(data,values='close',index=['date'],columns=['instrument'])
prices = data.values.T
returns = []
for ps in prices:
# 处理nan
if np.any(np.isnan(ps)):
returns.append([0 for i in range(0, prices.shape[1]-5, 5)])
else:
returns.append([(ps[i+5]-ps[i])/ps[i]*100 for i in range(0, prices.shape[1]-5, 1)])
returns = np.array(returns)
# 重复10000次实验
means, stds = [], []
for i in range(10000):
mu, sigma = portfolio(returns)
means.append(mu)
stds.append(sigma)
means = np.array(means).squeeze()
stds = np.array(stds).squeeze()
# 画图
plt.plot(stds, means, 'o', markersize=5)
plt.xlabel('std') # 标准差-波动性
plt.ylabel('mean(%)') # 平均值-收益率
plt.title('Mean and standard deviation of returns of randomly generated portfolios') # 每个投资组合的收益率和波动性的散点图
最终输出的图片如下,横轴代表风险,纵轴代表收益率:
从中可见,策略的收益和风险一般都成正比。
QP的任务,就是从中找到持平风险和收益的选项,以最大化我们的综合收益:
def optimal_portfolio(returns):
n = len(returns)
returns = np.asmatrix(returns)
N = 100
mus = [10**(5.0 * t/N - 1.0) for t in range(N)]
# 转化为cvxopt matrices
S = opt.matrix(np.cov(returns))
pbar = opt.matrix(np.mean(returns, axis=1))
# 约束条件
G = -opt.matrix(np.eye(n)) # opt默认是求最大值,因此要求最小化问题,还得乘以一个负号
h = opt.matrix(0.0, (n ,1))
A = opt.matrix(1.0, (1, n))
b = opt.matrix(1.0)
# 使用凸优化计算有效前沿
portfolios = [solvers.qp(mu*S, -pbar, G, h, A, b)['x']
for mu in mus]
## 计算有效前沿的收益率和风险
returns = [blas.dot(pbar, x) for x in portfolios]
risks = [np.sqrt(blas.dot(x, S*x)) for x in portfolios]
return returns, risks
returns_val, risks = optimal_portfolio(returns)
plt.plot(stds, means, 'o')
plt.ylabel('mean')
plt.xlabel('std')
plt.plot(risks, returns_val, 'y-o')
代码最后输出的可视化图如下,黄线可以认为是所有策略外部的一个包络线,表示最好的一批策略。
策略代码:
我们根据以上分析,将这个策略用到我们的回测系统中:
def optimal_portfolio(returns):
n = returns.shape[0]
# 转化为cvxopt matrices
covs = opt.matrix(np.cov(returns))
means = opt.matrix(np.mean(returns, axis=1))
lbd = 1
# 约束条件
G = -opt.matrix(np.eye(n)) # opt默认是求最大值,因此要求最小化问题,还得乘以一个负号
h = opt.matrix(0.0, (n ,1))
A = opt.matrix(1.0, (1, n))
b = opt.matrix(1.0)
# 使用凸优化计算最优结果
xs = solvers.qp(lbd * covs, -means, G, h, A, b)['x']
return list(xs)
def handle_data(context, data):
curr_day = context.trading_day_index
if curr_day < 60 or curr_day % context.transfer_period != 0:
return
print(curr_day)
# 计算60天内的收益值,更新权重
sids = []
returns = []
for k in instruments:
sid = context.symbol(k)
_prices = data.history(sid, 'price', 60, '1d')
if np.any(np.isnan(_prices)):
_returns = []
for i in range(0, 55, 5):
if np.isnan(_prices[i]) or np.isnan(_prices[i+5]):
_returns.append(0)
else:
_returns.append((_prices[i+5] - _prices[i])/_prices[i])
else:
_returns = [(_prices[i+5] - _prices[i])/_prices[i] for i in range(0, 55, 5)]
sids.append(sid)
returns.append(_returns)
returns = np.array(returns)
weights = optimal_portfolio(returns)
for i in range(len(sids)):
if data.can_trade(sids[i]):
context.order_target_percent(sids[i], weights[i])
策略结果:
策略分析:
1)该策略的结果并不怎么好,且调参后也没有明显的改善,除了说明我们当前的策略还不够完善外,还需要进一步的研究
2. 经典QP的改进
策略描述:
上一个策略实际上有很多问题还没有考虑到,可能会导致出现:单一股票仓位过高,单一行业仓位过高,股票持仓量太大等问题,这里我们优先解决股票或行业仓位过高的问题。
方法比较简单,就是直接在约束条件中加入对单一股票和单一行业最高持仓的限制,使得我们的数学描述变成:
m
i
n
i
m
i
z
e
−
μ
T
x
+
λ
1
2
x
T
σ
x
minimize \ \ \ \ -\mu^{T}x+\lambda\frac{1}{2}x^{T}\sigma x
minimize −μTx+λ21xTσx
S
.
t
.
∑
x
i
=
1
S.t.\ \ \ \sum{x_i} =1
S.t. ∑xi=1
x
i
>
=
0
;
i
=
1
,
.
.
.
,
n
x_i >= 0;\ \ i=1,...,n
xi>=0; i=1,...,n
x
i
<
=
m
;
i
=
1
,
.
.
.
,
n
x_i <= m; i=1,...,n
xi<=m;i=1,...,n
∑
x
i
<
m
k
;
k
=
1
,
.
.
.
,
K
\sum{x_i} < m_k; k=1,...,K
∑xi<mk;k=1,...,K
策略代码:
def optimal_portfolio(returns):
n = returns.shape[0]
# 转化为cvxopt matrices
covs = opt.matrix(np.cov(returns))
means = opt.matrix(np.mean(returns, axis=1))
lbd = 1
# 约束条件
# 1->大于等于0
# 2->个别股票小于等于0.2
# 3->每个类别总和都要小于等于0.4
Gs = []
Gs.append(-np.eye(n))
Gs.append(np.eye(n))
hs = []
hs.append(np.zeros((n ,1)))
hs.append(np.zeros((n ,1))+0.2)
for idxes in industry2idxes.values():
_temp = np.zeros((1, n))
_temp[0, np.array(idxes)] = 1
Gs.append(_temp)
hs.append(np.array([[0.4]]))
G = opt.matrix(np.concatenate(Gs, 0))
h = opt.matrix(np.concatenate(hs, 0))
A = opt.matrix(1.0, (1, n))
b = opt.matrix(1.0)
# 使用凸优化计算最优结果
xs = solvers.qp(lbd * covs, -means, G, h, A, b)['x']
return list(xs)
def handle_data(context, data):
curr_day = context.trading_day_index
if curr_day < 60 or curr_day % context.transfer_period != 0:
return
print(curr_day)
# 计算60all天内的收益值,更新权重
sids = []
returns = []
for k in instruments:
sid = context.symbol(k)
_prices = data.history(sid, 'price', 60, '1d')
if np.any(np.isnan(_prices)):
_returns = []
for i in range(0, 55, 5):
if np.isnan(_prices[i]) or np.isnan(_prices[i+5]):
_returns.append(0)
else:
_returns.append((_prices[i+5] - _prices[i])/_prices[i])
else:
_returns = [(_prices[i+5] - _prices[i])/_prices[i] for i in range(0, 55, 5)]
sids.append(sid)
returns.append(_returns)
returns = np.array(returns)
weights = optimal_portfolio(returns)
sorted_idxes = np.argsort(-np.array(weights))
for i in sorted_idxes:
if data.can_trade(sids[i]):
context.order_target_percent(sids[i], weights[i])
策略结果:
策略分析:
1)该策略的结果较经典QP策略已经有了一定的提升,但同样也跑不过基准收益
4. 总结
我们在本文中对量化投资中的指标策略和优化策略进行了基本的探索,对各个策略的效果也有了一定的把握。在实际情况中,很多策略往往是在回测时具有较好的效果,但一旦进入真实股市就失效了,所以量化投资更多是帮助我们排除错误的选项,而不能完全接管股票的买卖。