【量化基础】股票价格形态与反转效应 (代码附文后)

引言

相比于粗糙的数字,视觉信息可能更有可能吸引投资者的注意力,尤其是在 A 股市场这一散户参比过大的市场中。因此,不同价格形态的股票在未来可能具有不同的收益率表现。本文通过以股价对时间的二次项进行回归分析,试图抽象地刻画出股价的变动形态。同时,将股票价格的变动形态与在中国 A 股市场中存在着明显的反转效应相结合,通过对价格形态的把握更加有效的捕捉短期内的反转效应。

一些常见的股票价格形态

策略思路

股票价格的视觉形态是能足够的吸引投资者的注意力呢?如果可以,我们又该如何刻画股票的价格形态呢?本文采取定量的方式来解决这一问题。即把股票价格的历史数据对时间的二次项回归,通过一个简单的二次型图像来刻画股价的变动趋势及变动速度。回归公式如下:

P_(i,t)=α+βt+γt^2

其中,P_(i,t) 表示股票 I 在 t 日的价格。T 是一个表示时间的自然数列,即过去 n,…,2,1 天分布对应的 t 为 1,2,…,n。以过去价格对时间的二次项回归系数(即 γ)来反映股票过去的价格形态:若γ为正(负),股价是时间的凸(凹)函数。

我们可以把系数 Y 当成一个因子,称为 Y 因子。简单的说,就是以股价对时间的二次项回归系数 Y 表示股票过去的价格变化速率。直观而言,Y 越大,表明股票价格以更快的速度变动。对于累积收益率为正的股票,Y 为正,表明整体而言,在过去一段时间中,股价以递增的速度增加;对于累积收益率为负的股票,Y 为正,表明在过去一段时间中,股价以递减的速度减小。

具体的验证方法为,将所有股票基于两个指标进行叠加分组。

根据股票过去 J 个交易日的收益从小到大排序,选出收益率最低的一部分股票。具体的数量设为参数 MOMENTUM_N

对(1)中选出的股票组合,基于其前 J 个交易日的每日收盘价对时间以及时间的平方项进行回归,然后按照其平方项系数γ从小到大排序,选出γ最小的一部分股票。具体的数量设为参数 Y_N

在实践中,我们选取以下参数值:

策略实现策略实现

1)反转效应——选取之前收益率最低的一部分股票

交易标的:A 股
调仓周期:5 天
持仓量:300 支股票
回测时间:2014~ 2017
回测时长:3 年

收益曲线

收益归因

业绩分析

可以发现,买入前一段时间收益率最低的 300 只股票,其收益曲线基本与大盘一致,且略差于大盘。这可能是因为同时持仓的股票数量过多导致的。并不能因此否认反转效应的存在,我们还需要将其收益率与跑赢组合相对比。

2)动量效应——选取之前收益率最高的一部分股票

交易标的:A 股
调仓周期:5 天
持仓量:300 支股票
回测时间:2014~ 2017
回测时长:3 年

收益曲线

收益归因

业绩分析

可以发现,如果我们买入前期收益率最高的 300 只股票,将面临 -15.6% 的负年化收益率。这表明,在 A 股市场中短期内反转效应的表现确实强于动量效应。如果我们能够卖空股票组合,就可以采取买入跑输组合、卖空跑赢组合的对冲策略,取得大约 20% 的年化收益率。

3)Y 因子——在选出的跑输组合中选取 Y 因子最低的一部股票

交易标的:A 股
调仓周期:5 天
持仓量:100 支股票
回测时间:2014~ 2017
回测时长:3 年

收益曲线

收益归因

业绩分析

可以发现,在跑输组合中再选出 Y 因子最小的股票,确实取得了优中选优的效果。

4)Y 因子——在选出的跑输组合中选取 Y 因子最高的一部股票

交易标的:A 股
调仓周期:5 天
持仓量:100 支股票
回测时间:2014~ 2017
回测时长:3 年

收益曲线

收益归因

业绩分析

在跑输组合中选择 Y 因子最大的股票,使得原本的正收益翻成负收益。综合(3)(4)我们可以发现,Y 因子确实可以帮助我们更好的抓住短期的反转效应。对于跑输组合而言,Y 因子越小反转效应越明显。

5)最差组合——在选出的跑赢组合中选取 Y 因子最高的一部股票

交易标的:A 股
调仓周期:5 天
持仓量:100 支股票
回测时间:2014~ 2017
回测时长:3 年

收益曲线

收益归因

业绩分析

最后,我们测试了理论中的最差组合——在跑赢组合中选择 Y 因子最大的股票。这样的股票往往表现出很强的反转倾向,而对于跑赢组合反转意味着收益率由正转负。因此我们的收益率表现很低。

如果我们运用叠加因子同时构建多空组合,即买入最优组合(策略(3)),卖空最差组合(策略(4)),可以取得大约 26% 的年华收益率。

小结

基于对于反转效应和 Y 因子的实证研究,我们发现,A 股市场确实存在短期内的反转效应。即前期收益率最低的组合相对与前期收益率最高的组合有更高的收益率,多空组合收益显著。同时,具有不同价格形态(Y 因子)的股票组合收益率存在差距。在同一组合中,Y 因子越小,股票的收益率表现越好。这一策略能够成功的逻辑在于,前期以递增的速度变动的股票出现反转的幅度和可能性都要大于以递减速度变动的股票。当股票前期收益为正时,Y 为正,若 Y 因子大,则其更可能反转,因此 Y 越小越好;当股票前期收益为负时,Y 为负,若 Y 因子小,其绝对值更大,也是更可能发生反转,因此 Y 越小越好。综合来说,结合 Y 因子和反转效应之后的多空组合取得了最高的收益率。不过值得注意的是,由于 A 股缺乏做空工具,如果只对该策略采取单边交易,即只持多头,收益率未必令人满意。

关于回测平台、历史数据,可以在这里下载:http://www.yunkuanke.com/#/introduce
这个平台的数据都是经过清洗的,而且策略不会外露,有保密系统,还是比较安全的。
想学习更多量化策略,也可以来这里看看:http://www.yunkuanke.com/#/lzClass
想学习更多关于量化投资策略相关内容的,可以关注微信:量化投资与金融科技(微信号:QuantumFintech)
微信二维码:

Code

# -*- coding:utf-8 -*-

_from _CloudQuant _import _SDKCoreEngine  # 导入量子金服SDK
_from _CloudQuant _import _AssetType
_from _CloudQuant _import _QuoteCycle
_from _CloudQuant _import _OrderType
_import _numpy _as _np  # 使用numpy
_import _pandas _as _pd
_import _scipy
_from _sklearn.linear_model _import _LinearRegression

np.seterr(invalid='ignore')

config = {
'username': 'username',
'password': 'password',
'rootpath': 'c:/cStrategy',  # 客户端所在路径
'assetType': AssetType.Stock,
'initCapitalStock': 100000000,  # 初始资金
'startDate': 20140101,  # 交易开始日期
'endDate': 20170101,  # 交易结束日期
'cycle': QuoteCycle.D,  # 回放粒度为1分钟线
'feeRate': 0.001,
'feeLimit': 5,
'strategyName': '价格形态',  # 策略名
"logfile": "ma.log",
'dealByVolume': True
}

J=10
K=5
MOMENTUM_N=300
Y_N=100

_def _initial(_sdk_): #整个回测前需要的操作
_sdk_.setGlobal('c',0)

_def _initPerDay(_sdk_): #每天回测前需要的操作
_pass__
__
__def _strategy(_sdk_): #交易策略
count=_sdk_.getGlobal('c')
_if _count==0:
    stock_list = _sdk_.getStockList()
price = _sdk_.getFactorData('LZ_CN_STKA_QUOTE_TCLOSE')[-J:]
return_rate = price[-1] / price[0] - 1
    return_rate = pd.Series(return_rate, index=stock_list)
return_rate = return_rate.sort_values(ascending=False)
momentum_pool = return_rate.index[:MOMENTUM_N]
    momentum_pool=list(momentum_pool)
    hist=_sdk_.getLatest(momentum_pool,J)
    y=[]
    _for _s _in _momentum_pool:
        _if _len(hist[s])==J:
            target=[i.close _for _i _in _hist[s]]
            target=np.array(target)
            t=range(1,J+1)
            t=np.array(t)
            t_2=[i^2 _for _i _in _t]
            t_2=np.array(t_2)
            train=zip(t.T,t_2.T)
linreg = LinearRegression()
            model = linreg.fit(train, target)
            y.append(linreg.coef_[1])
        _else_:
            y.append(np.nan)
    y=pd.Series(y,index=momentum_pool)
    y=y.sort_values(ascending=False)
    y_pool=y.index[:Y_N]
    transferPosition(_sdk_,y_pool)
count+=1
_if _count==K:
    count=0
_sdk_.setGlobal('c',count)

_def _transferPosition(_sdk_,_stock_pool_):
position = _sdk_.getPositions()
position_dict = dict([i.code, i.optPosition] _for _i _in _position)
stock_to_buy=[]
stock_to_sell=[]
_for _s _in __stock_pool_:
    _if _s _not in _position_dict:
        stock_to_buy.append(s)
_for _s _in _position_dict.keys():
    _if _s _not in __stock_pool_:
        stock_to_sell.append(s)
# stock_to_buy = set(stock_pool) - set(position_dict.keys())
# stock_to_sell = set(position_dict.keys()) - set(stock_pool)
quotes = _sdk_.getQuotes( stock_to_sell)
_if _stock_to_sell:
    sell_orders = []
    _for _stock _in _stock_to_sell:
        _if _stock _in _quotes.keys():
            price = quotes[stock].current
volume = position_dict[stock]
order = [stock, price, volume, -1]
            sell_orders.append(order)
    _if _sell_orders:
        _sdk_.makeOrders(sell_orders)
        _sdk_.sdklog("------------------------------------------")
        _sdk_.sdklog(_sdk_.getNowDate(),"DATE")
        _sdk_.sdklog(sell_orders,"SELL")
_if _stock_to_buy:
    available_cash = _sdk_.getAccountInfo().availableCash
available_cash_one_stock = available_cash / len(stock_to_buy)
buy_orders = []
    quotes=_sdk_.getQuotes(stock_to_buy)
    _for _stock _in _stock_to_buy:
        _if _stock _in _quotes.keys():
            price = quotes[stock].open
volume = int(available_cash_one_stock / (price * 100)) * 100
            _if _volume > 0:
                order = [stock, price, volume, 1]
                buy_orders.append(order)
    _if _buy_orders:
        _sdk_.makeOrders(buy_orders)
        _sdk_.sdklog("------------------------------------------")
        _sdk_.sdklog(_sdk_.getNowDate(),"DATE")
        _sdk_.sdklog(buy_orders,'BUY')
_def _main():
# 将策略函数加入
config['initial'] = initial
config['strategy'] = strategy
config['preparePerDay'] = initPerDay
# 启动SDK
SDKCoreEngine(**config).run()

_if ___name__ == "__main__":
main()