前言
我是从IT行业转来做量化研究的。读硕士两年习惯了白天上班,晚上周末学习的时光。从我在知乎发表第一篇量化系列文章(Charlie:对冲基金与量化交易解密)到现在已经一年多了。感谢2w多读者对我的肯定。当我对这个行业了解更深刻之后想尽可能的回赠给对量化投资感兴趣的同学或者从业的同行。
最近4个月没有更新知乎的原因并不是没有时间,而是前一周写的稿子经常在下一周就被新的认知取代。入行之后的学习曲线非常陡峭,很多东西需要理解,沉淀后才能准确给第三个人解释。‘传统定价量化’和‘策略量化’观点是相互反驳的,定价量化假设不能做预测,因此几十年的数学理论能一直延用;策略量化是做主观预测,为了战胜随机游走策略要不断进步。 为了保证内容的时效性,发表后我会定期更新全部内容。
量化行业聚集了各个领域的专家,我从知乎大V的文章中开阔了不少眼界。也因为量化和知乎,非常感恩在2021年认识了将要出国读量化的同学,同行,同事,和前辈,打了很多线下德州,也玩了很多线下剧本杀。
文章信息均来自公开数据,已开源代码,和个人观点,不构成任何投资建议。
交易本质
假设
如果你想在德州扑克里长期赢钱,那就需要正期望值的策略(胜率大于赔率)。在计算胜率的时候要假设自己能赢什么牌。同理,任何交易也离不开假设。这个假设需要简单,可归因,可证伪,解释最终赚的是什么钱。最好是有经验的大佬告知可行后,在别人做过确定的方向上疯狂挖掘。做量化需要很多尝试,最怕的是不能区分是不是自己做的不够好,还是市场不给这个策略alpha。举几个假设的例子:
- 假设A股基本面在对冲和风险归因后是有pure alpha的
- 假设期货通过量价预测15分钟后收益可以有很高的夏普
- 假设因子高频预测期货能用追涨杀跌的执行交易获取肥尾收益
- 假设金融市场在高频有稳定的规律并且机器学习可以在iid,去噪,稳定的时间序列里提取特征
- 假设监督学习最小误差法都是垃圾进垃圾出,强化学习做策略优化却可以赚钱
- 假设截面选股的y比时间序列的y更iid,更容易在大量股票数据中提取有效的牛股规律
- 假设期权波动率曲面可以从短期历史平均中做回归套利
- 假设期权市商没有方向或者波动率的预测,短时间内无法调整报价,导致被squeeze或者定价不准
公开信息
曾在一局德扑中我拿的6,9 off suited,翻牌是7,8,10彩虹面。结果我翻牌allin输给了J,9。虽然我的天顺能赢过很多牌,但是对方拿到更大天顺的概率是0.5而不是根据公开信息能推算出的我的胜率。因为每次发牌得到的都是一个属于独立同分布的随机数,然而多个独立同分布做运算是均值加减,方差相加。虽然现在市场上公开的量化资源越来越多:有基本面因子(天风证券,因子日历),量价因子(alpha101,talib,technical analysis),机器学习遗传因子(华泰证券),但有的人手牌是J,9,有的人是6,9。市场上不会有统计和逻辑上都很强的因子,不然就违反了百年不变的有效理论,赢都是赢在了细节上。目前预测最好的做法是把因子库里较强的少部分因子做线性组合,这样的解释性更好,但也会导致不同私募策略的同质性很高。随着量化赛道越来越拥挤,风格因子失效也越来越多,开设然后倒闭的资管公司也越来越多。这个市场是零和的,预测好的公司赚预测不好的钱。个人认为能在这个行业长期生存必须要有一套靠非参数逻辑稳定赚钱的策略并且一定不会公开(可以像巴菲特,可以是人工手动t0,可以是量化做市套利,可以是赚的返佣)+完善的新老迭代内部调用库体系+对新方向的研究(比如可转债,期权,对抗网络,强化学习等等)+数据质量和多样性,我在城堡基金工作时就有被他们对数据的投入惊讶到。
标的
德扑中,在什么位置有纪律的打什么样的牌是创造正期望值的关键。比如在盲位打3bet转诈唬的次数等等。交易中,各类“标的”是获取利润的工具:股票,衍生品,方向,波动率,gamma,离差。我分别聊一下交易它们的好坏,什么时候去打什么手牌。
1. 股票
因为现金流和ROE的存在,企业本身就是为社会创造一定价值的,因此长期的收益率应当为正,适合长期持有。
2. 衍生品
衍生品发明的本质是一个对冲的工具,因此要付出代价,长期收益率应当为负。由于衍生品相对于股票更好交易,如果用它来投机的话长期收益率为0。
3. 方向(delta one)
现货或者期货价格的收益率(return)是白噪音,随机过程,自相关性极低,方差大,隔夜或者事件驱动带来的自相关性高噪声大,具有可重复特征的时段少。鞅论讲了最好预测明天股价的工具是今天的股价,或者只能用0来预测明天的return。由于未处理过的样本非iid所以本身就无法对预测的方向有效分类。市值越大像银行股或etf,其return的时间序列也更稳定,噪声小,自相关性强,方差小,预测相对不容易过拟合。
图1(价格本身的acf与pacf)
4. 波动率(vega)
atmvol的收益率return是服从一定ARMA(autoregressive-moving-average)时间序列模型(图2右是聚集,图二左是回归)。因此才会有2003年获得诺贝尔奖的GARCH(Generalized AutoRegressive Conditional Heteroskedasticity)(Charlie:复合波动率预测模型模型。(Charlie:高频波动率交易
图2(波动率本身的acf与pacf)
波动率好预测但是难转化为利润。原因是IV(隐含波动率)是人给的,RV(实际波动率)是市场给的,期权组合的利润是通过未来IV和RV的差实现。在国外,IV和RV的差基本可以通过VXX(vix的etf)的基差来模拟。
执行端准确的回测很难做。要做四个到期日atmvol转化到连续一条波动率曲面,并回测到可交易的标的上。这就需要很大的工程。比如,vega和根号下到期日ttm是正相关的,ttm越小vega减小的程度每个expiry都不同,或者实际波动率减小的程度每个expiry也不同。又比如快到期前几天,vega很小的时候应该剔除还是用后到期日的前几天补进去等等。
图2.1左:波动率对vega的影响,右:到期日对vega的影响
研究端波动率和波动率收益率的不平稳(GARCH作为证据)会产生误导的IC。从时间序列上如果用-log(IV)来预测未来的波动率就能有0.6以上的强相关性,但这转化不成利润。图3左侧蓝色线是把未来1天实际波动率的变化率作为y,图3右侧蓝色线是把VXX未来一天价格的收益率作为y。橙色线都是S&P500的价格走势。原因是如果用今天的波动率来预测明天的波动率收益率,y是不平稳的(价格的收益率是平稳的)。对明天的预测实际上是基于今天已经得到能解释明天的信息了,因此研究段的强相关是用到了未来函数,和y自己的acf来作弊。
图3 (虚拟atmvol回测 vs.用真正vxx回测)
导致上图利润无法实现的原因是波动率无法直接被交易。如果只是通过买卖跨式组合,测试后也没有很好的效果。就算能交易到VXX,绩效不好的原因是波动率作为标的y背后真正能用利润实现的是IV和RV的差。IV滞后于RV,能大于也能小于RV,它们之间的差是不平稳的,因此很难预测。如果要预测,可以从以下三点入手:
- correlation premium:指数做空波动率大部分时间赚钱,而个股模拟期权做空波动率有时候赚有时候不赚钱
- skewness premium:指数波动率曲面的斜率比股票的大
- variance premium:买虚值认购的人希望得到更高的利润而使IV升高
因为市场的不同,导致IV和RV差还有其他非主流因素:
- 投资者因为新闻或季节性事件改变“保险”价格,提高IV
- 大部分认为发展中或发达国家的etf指数是长期向上的,所以IV>RV
- 卖保险产生negative skewness和excess kurtosis是大部分人风险厌恶的,所以需要更高的IV补偿
- 由于做空波动率和股市下跌有收益同向性,这点也需要更高的IV来补偿
- 股票投资者可能选择卖call来稳定收益,进一步降低RV
- 投机者会高估市场下跌带来的损失,而要求更高的IV
所以,从研究层面讲,预测波动率的规律难易度完全不低于预测方向或预测选股。但是交易vega却比交易delta更难。从高频角度波动率短期的变化很小,希腊值很难计算精确,delta和vega互相影响,导致混合希腊值的对冲和目标仓位很难做对,更别说和研究的利润周期匹配了。
结论是如果波动率作为标的,因子程序自动化不如手动检测隐含波动率平面的term structure,skewness或者convexity变化然后挑单独期权组合来套利。
5. 离差(dispersion)
图4(离差定义)
当离散作为标的被交易,它的理论基础是之前解释IV和RV差的correlation premium。从图5中可以看出离散本身(图5中没有做变化率)有很高的均值回归性。但是在国内用股票模拟期权来交易会非常困难。我之前在香港eclipse trading工作时,离差对于市商来说不仅是个对冲风险工具,也是个有利润的策略。
图5(离差的acf与pacf)
6. gamma(delta的变化率)
若持有期权到期,gamma的盈利全等于theta的亏损。快到期时它们的曲线一个急速上翘一个急速下跌。gamma的利润只能通过delta hedge得到,需要一个稳定的价格箱体,在波动率略低的时候建权力仓或者用日历价差calendar spread来对冲vega,并且RV高到能覆盖theta的利润时才有希望赚钱。 @许哲 曾在这篇回答里阐述了类似策略 (Long Gamma Trade 如何对冲掉 Vega?)但是实际操作起来还是有以下问题:
- 高抛低吸箱体需要回到预测时间序列的均值
- 之前提到的离差交易在gamma策略里是亏钱的(好比图6被换成了更陡峭的山峰)
- 任何高抛低吸策略只要设止损或者调仓就一定会亏钱,因此怎么样设计仓位管理才是gamma策略的重中之重
下图6中,蓝色箭头是卖,黑色箭头是买。1和7几乎是不赚不亏,2,3,4如果持有到最后减去5,6应该也是赚的。但如果在4止损,那2和3都是亏钱的。示例说明了高抛低吸长期总是能赚钱,但如果策略在山峰或者波谷触发止损,就一定亏钱。
图6(仓位控制的重要性)
单标的择时为什么是伪命题
突破是无法预见的,回归是自然法则必须的,趋势是可能赚到肥尾收益的。任何以滚动,高抛低吸,或者均值回归为核心的策略包括统计套利,做市,接针,箱体等都依赖于一个稳定的均值。比如价格的均值不稳定,就很难做高抛低吸。预测均值的话就又回到了单标的的时序预测。
图7(价格,离差,波动率的布林带)
所以时间序列上以量价为基础的技术型因子和价格都是信息滞后的,无法再去预测。而且参数化很严重,没有核心利润的逻辑点。因此如前言中提到的,量价因子中不可能有统计和逻辑都兼顾的强分类器。
模型有别于预测的地方在于,模型是最优拟合,或最小化估计偏差,用不变的规律刻画不变的事实。预测利润却永远是市场给的,量纲上利润和模型有着非线性的关系。模型到利润的转化是量化最难的地方,长期不变的逻辑,和iid的y至关重要。可金融市场不像猫的品种不变,新的规律不停在生成,让单标的时间序列上的择时几乎成为一个伪命题,除非运气好碰到2017-2019年的样本规律重复性大才能赚择时的钱。目前很多机器学习的分类,回归(regression)本质上是一样东西基于另一样东西的预测,比如未来的均值,未来的波动率,未来的协方差矩阵。但是量化作为金融市场里5万亿的20%,量化的优势在海量相对iid的横截面上找稳定的量价回归(mean reversion)逻辑,从而获取少于主观的利润,平抑市场的波动率。
什么是因子
因子是x,用来预测未来的return y。
把许多因子x组合后做回归就是y_hat。y_hat是一个预测项。
信号到仓位
转化的思路有以下五种:
- 把y_hat模拟为下单时的目标仓位或者目标仓位加减。y_hat变大说明策略预测未来的收益会变大,此时我就要持有更多的仓位,y_hat 变成了我回测时的信号x。这样的前提是y_hat和y都是标准正态分布的,纲量(base unit)的方差足够小,能允许x直接映射到y。但是事实是model.fit做出来的y_hat天然数值就比y小,说明return不能预测。
- 把y_hat纯粹看成一个信号,然后加入一套rule based的仓位管理系统,如趋势跟踪只做当y_hat在90分位数以上的时候,如希格斯用utility function来控制position sizing,如凯利公式,如海归法则等
- 把因子x本身通过一定的组合,而不是经过回归model.fit后作为信号去预测真实的y。这样做的好处是信号不受纲量方差(样本不iid)的影响,却换来了组合不同x因子在不用y拟合的前提下,每个因子的权重系数能否在全样本保持稳定,或者稳定好过等权的因子x组合。本质上3和1都假设了样本规律的延续,不同的是1假设的纲量的稳定,3假设了x组合系数的稳定。
- 用因子x去预测条件概率上的分类问题,把回归换成上涨概率和下跌概率的预测,有了高于0.6-0.7,并且时间序列上稳定的AUC(area under curve)之后就可以计算凯利公式里的盈亏比和触发风控了。如果使用概率,还需要遍历不同precision和recall得出的threshold来找最优盈利的参数。分类详见后文机器学习章节。
- 用深度奖励函数的强化学习,通过概率组合优化单次下单金额,在全局中寻最优策略
高频与低频
高频的范畴是做时间序列上的统计性量价预测,或截面的统计套利。同在时间序列预测上,高频因子以趋势为主,低频以回归为主。由于高频策略逻辑性低,需要在事后解释(ex-post)和归因。
低频因子会涉及到基本面,报表,会计,行业轮动,另类数据等。由于数据量少,它是靠事前解释(ex-ante)从截面多空逻辑性入手。低频很难有足够多iid的样本做量价。
做高频的意义在于有足够多的尝试来复现期望值为正的概率。希望数据点变多后处理好的样本更iid,更容易提取规律。但高频数据固定的量价统计特征变化很快,从提取稳定规律的角度看,高频其实是另一种低频。所以高频的数据样本并不会比低频的好很多。复现总体正期望值可能需要更长的时间。
高频数据的信息量是这样依次递减的,如果用到实时的逐笔order和trade来更新0.5秒一次的订单簿,那一年可能要储存十个t的数据量。对gpu运算和硬件读写的要求都很高。一个好的私募一定会有一个非常强大的数据体系提供给投研。目前只用bar数据是很难做好预测的。
order>trade>tick>bar
bar = open high low close amount, volume, pct_chg
在绝对高频的,如果比逐笔更细的信息还有撤单,成交概率,挂单排队,订单引流,各个交易所的延迟等等。对于掌握这些不对外公开的信息者,优势会更明显。
截面因子
截面因子是从信息的角度去搜索统计量,比如:
- 分布
- 时间
- 关联
- 跨品种,股票的rank截面
- lead lag 统计套利截面
- 在期货中低频策略里,每x分钟截面往往被用来把各个品种对齐做对冲或者波动率对比
时序因子
也叫统计类因子,是量价里最普遍的因子类型
- 和固定的中枢或者各类avg(sma, ewa, trima, arima, arma, sarima, sarimax, demean, kalman等)比较
- 同一个频段的y可以用不同频段的x来预测(预测什么时候的y应该在“交易本质”的假设中,或者“机器学习”的样本研究中完成)
- 固定时间点因子
- 动量(volume)
- 动能(return)
- acf
- auto regression, vector autoregression, varma, varmax (x = exogenous regressors)
- exponential smoothing, ses, hwes
- true range
- residual
- rolling window
- expanding window
- lag
- diff
- tsfresh是个可以提取时间序列特征的python包
另类因子
- 用股票信息预测股指期货
- 用期权信息预测股票
- 反向工程(reverse engineering)设计一套有逻辑有止损的交易系统,再把仓位转换成因子
- 把已有的x去80分位数
- 把已有的x做demean
- 把已有的x做long short
稀疏类因子
稀疏类因子是根据条件,规则(rule based),或阈值触发。比如 [0, 0, 0, 1, 2, 1, 0, 0],或者只有0和1构成的状态类因子。单独拿出来讲是因为这类因子最为优秀。它们的信号持续性好,换手率少,资金体量大,IC/turnover比例高,特别有利于持续上涨的高gamma高vega行情。很对期货CTA交易系统本身就是一个稀疏类因子,比如
- 海龟
- dual thrust
- 优势比率
- 《Systematic Trading》by Robert Carver
因子评估标准
因子评估标准可能每家私募用的都不同,但这是非常重要的环节。没有这个基础,任何因子挖掘都无法有效的开展。特别是对信息增量与弱分类器的定义。信息增量指每加一个因子对总体策略的贡献。弱分类器指三个臭皮匠能顶一个诸葛亮,但是三个臭皮匠自身不能太差,也不能太相似。
教科书给的评估方式是单一评判correlation高或者mse低没有意义。要同时结合因子指标和pnl来衡量(Meucci A. Risk and Asset Allocation[J]. springer finance, 2005)图8举例说明控制变量后,correlation差的因子回测曲线可能更好。因此要尊重以下“因子指标”与PnL衡量共同的结论。
图8 (预测的结果不能直接转换为利润)
因子指标
因子的衡量标准(fitness function)或者信息系数(Information Coefficient,简称IC)可以是以下任何一个:pearson corr , R^2, spearman corr, weighted corr, mse, rmse, entropy, gini, accuracy, recall, precision, f1 score。(如果有截距 ,如果没有 )如果用pearson correlation,corr是对cov值的标准化无量纲量(后文的IC都假设为它) 的公式就要符合一定的前提假设,不然会有用到未来的mean等问题(图9)。
图9(皮尔斯相关性定义)
correlation描述的是线性的关系,由于数据量足够多的长期时间序列上收益率的自相关性很小,可以暂时忽略非线性效应,因此只用线性来刻画的correlation就是一个好的评判标准。correlation作为IC要满足的假设是:(以下列表为学术摘抄直译)
- x和y都是正态分布
- x和y不能有极值
- x和y都出自随机样本
- x和y要被归一化,在同一数量级上度量(如min_max_scaler或者zscore)。因为相关性是线性缩放的
- 皮尔斯相关性不能解释两者之间的非线性关联
因为实际数据一定会违反了某些假设,还有因子在训练集归一化,去极值等处理后的失真, 导致了上面所说的correlation差的因子在测试集回测曲线更好。(计算相关性Corr或者R方是否用到的未来的mean?是不是去mean后计算的相关性才准确?)预测模型转化不了利润。
好消息是此文机器学习章节里的样本数据分析会给出详细解决方案,假设以上都成立,我们接着做以下无偏的因子评估。
- 一: 样本外总体IC(pearson correlation e.g.)的绝对值要高。
- 高频0.2及格,低频0.1及格
- 数据点越多,IC会越小,但是会更显著
- 二: IC的自相关性高
- 如果IC是缓缓下降或者升高,都能说明IC能延续且一定存在某种逻辑性。IC衰减的普遍是高频因子会。IC升高的普遍是低频因子。但是,因为x和y相似,违反协方差平稳定义,由acf导致的IC升高是非常需要警惕的。换句话说,当x包含能解释y的信息再去预测y,那就是在用未来函数作弊。(比如价格预测价格)
- IC自相关不要衰减到负数,否则会导致预测相反亏钱
- 高频IC的半衰期要小于低频
- 当交易执行遍历开平仓周期的时候可以借鉴decay的最高点
- 三: signal acf decay越慢越好
- 信号自相关不要在IC预测周期内衰减到负数,否则会当最近的tick吃不到高IC,在后面几个tick IC变负
- 因子acf decay快说明换手费用高,不利于持续上涨的高gamma高vega行情,但有利于低gamma高vega的行情
- 这是一个利润换夏普的概念。换手越高,夏普应该越高。
- 四: 用cdf检查IC是不是只存在于y的尾部
- 五: 线性回归异方差的存在(conditional heteroskedasticity)会导致correlation的不准。需要把residual画出来
- 六: 切片IC mean要约等于IC total,滚动或者递归IC需要在时间序列上相对平滑
- IC mean不等于IC total的本质也是因子在时序上的分布不同
- 切片IC mean>IC total表达了虽然回测中有很多短期的大幅度回撤,但是上升的斜率比总体的斜率更大,夏普更差,如图10左。切片IC mean<IC total表达了大部分样本涨的没有总的多,利润只是从极少许样本中得到的,如图10右。
图10(IC mean要等于IC total才好)
PnL衡量
样本外回测的pnl衡量不需要复杂的止损止盈,或建仓关仓法则。只需要最基础的调仓周期参数,然后用控制变量法衡量不同因子的好坏。从研究过程到最终策略都不应该是一个很参数化的东西。
- 净值用信号rolling(预测周期).sum()的80%分为模拟。(这里的预测周期就是执行策略的调仓,如果与研究端的最高IC decay和最低sig acf匹配可以尽可能还原研究的利润。预测周期与策略容量是成正比的)
图11(把信号当作仓位做回测来评估因子好坏)
- 把信号或者信号的first diff模拟成仓位乘以未来return再累加
- 当信号(仓位)大于净值的时候,pnl用(净值/预测周期/信号*pnl)按比例缩减
- 交易手续费率用(|信号-预测周期前信号| * 费率)来模拟
- 最终的净值曲线用(累计pnl收益/净值+1)来画,并且把指数也换算成净值画在一张图上
- 样例见图3
################### evaluation #################
df = df_etf
df[&#39;sig&#39;] = df_etf[&#39;alpha_10&#39;]
use_sig_as = &#39;pos&#39; # or pos or diff
use_zscore = False # best not to use zscore because it might use future data
check_tail_ic = False # use future data to know the median
demean = False
positive_z = True # false is reversion
prediction_time = 1
sig_lag = 1
add_fee = 0
df[&#39;return&#39;] = df_etf[f&#39;return+{prediction_time}&#39;]
if use_sig_as == &#39;pos&#39;:
df[&#39;sig&#39;] = df[&#39;sig&#39;]
elif use_sig_as == &#39;diff&#39;:
df[&#39;sig&#39;] = df[&#39;sig&#39;]-df[&#39;sig&#39;].shift(sig_lag)
#df[&#39;sig&#39;] = df[&#39;sig&#39;].shift(5)-df[&#39;sig&#39;].shift(2)
df = df.dropna()
if demean:
df[&#39;sig&#39;] = df[&#39;sig&#39;] - df[&#39;sig&#39;].rolling(sigma_period).mean()
df.dropna(inplace=True)
if not positive_z:
df[&#39;sig&#39;] = -df[&#39;sig&#39;]
if use_zscore:
df[&#39;zsig&#39;] = zscore(df[&#39;sig&#39;]) # this might use future data
else:
df[&#39;zsig&#39;] = df[&#39;sig&#39;]
if check_tail_ic:
# 检查尾部IC
df[&#39;zsig&#39;] = np.where(df[&#39;zsig&#39;].abs()>=np.percentile(df[&#39;zsig&#39;].abs(),80),df[&#39;zsig&#39;],0)
# 净值, 根据信号在zscore周围多少调测, gotten by rolling sum of the prediction period
df.dropna(inplace=True)
#asset = np.percentile(pd.Series(list(filter(lambda x: x != 0, df[&#39;zsig&#39;].rolling(prediction_time,min_periods=1).sum().abs()))),80) # 根据信号值来设置净值nav
asset = np.percentile(df[&#39;zsig&#39;].rolling(prediction_time,min_periods=1).sum().abs(),80) # 根据信号值来设置净值nav
df_copy = df.copy()
importlib.reload(option_orderbook_hf)
ic_mean,ic_total=option_orderbook_hf.single_factor_analysis(data=df_copy, x_col=&#39;zsig&#39;, alpha=0.0000000001, pieces=100, interval=30, df_corr=pd.DataFrame(), graphornot=True) # 26 day slice
df_pos = df[df[&#39;zsig&#39;]>0]
df_neg = df[df[&#39;zsig&#39;]<0]
df[&#39;pnl&#39;] = df[&#39;zsig&#39;]*df[&#39;return&#39;]
# 加资金利用率
df[&#39;pnl&#39;] = np.where(df[&#39;zsig&#39;].abs()<=asset/prediction_time,df[&#39;pnl&#39;], asset/prediction_time/df[&#39;zsig&#39;].abs()*df[&#39;pnl&#39;])
if add_fee:
df[&#39;fee&#39;] = abs((df[&#39;zsig&#39;] - df[&#39;zsig&#39;].shift(prediction_time))) * 0.0003
else:
df[&#39;fee&#39;] = abs((df[&#39;zsig&#39;] - df[&#39;zsig&#39;].shift(prediction_time))) * 0
df[&#39;pnl&#39;] = df[&#39;pnl&#39;] - df[&#39;fee&#39;]
df.dropna(inplace=True)
sharpe = df[&#39;pnl&#39;].mean()/df[&#39;pnl&#39;].std()*np.sqrt(252)
annual_return = (stats.gmean((df[&#39;pnl&#39;]+asset)/asset)-1)*252
#annual_std = (stats.gstd((df[&#39;pnl&#39;]+asset)/asset)-1)*252
#sharpe = (stats.gmean((df[&#39;pnl&#39;]+asset)/asset)-1)/(stats.gstd((df[&#39;pnl&#39;]+asset)/asset)-1) * np.sqrt(252)
ic = df[&#34;zsig&#34;].corr(df[&#34;return&#34;])
#df.index = pd.DatetimeIndex(df[&#39;trade_date&#39;])
fig = plt.figure()
plt.plot(df[&#39;pnl&#39;].cumsum()/asset+1,label=&#39;zscore with cash ratio&#39;) # 单利
#plt.plot(((df[&#39;pnl&#39;]+asset)/asset).cumprod(),label=&#39;zscore with cash ratio&#39;) # 复利
plt.plot(df[&#39;close&#39;]/df[&#39;close&#39;].iloc[0],label=&#39;etf spot price&#39;)
#plt.plot(df[&#39;zsig&#39;]/df[&#39;zsig&#39;][0]/20+1,label=&#39;pos&#39;,color=&#39;grey&#39;,alpha=0.3,linewidth=0.3)
plt.legend()
plt.grid()
plt.title(f&#34;no compound graph, sharpe={round(sharpe,3)}, annual return={round(annual_return*100,3)}%, ic={round(ic*100,3)}%&#34;)
plt.xticks(rotation=25)
plt.axvline(x=test_start_date,color=&#39;red&#39;)
plt.show()
plt.figure()
plt.plot(df[&#39;zsig&#39;],label=&#39;zsig&#39;)
plt.legend()
plt.title(&#39;check sig and zsig, should be around 0&#39;)
plt.grid()
plt.xticks(rotation=25)
plt.show()
如何挖因子
从研究员挖因子的角度因子可以分类成逻辑性因子与非逻辑性因子。逻辑性因子靠的是事前解释(ex_ante)需要一定的金融和交易经验;非逻辑性因子靠的是事后解释(ex-post)需要的是编程的功底。国内许多私募找很多找普通的本硕来挖因子,然后请深度学习的博士利用因子库里的因子建模,或者像我这种啥都懂不多的人,看看git,看看理论,看看视频,调一下包,调一下参的。但是深度学习的博士可能并不知道因子生成的逻辑。虽然对公司而言策略可以保密,组合后结果可能出现无法解释的大规模回撤。
逻辑性因子
- 2021年底私募大规模回撤会导致机构减持对冲期货仓位,导致股指期货升水
- 期货在交割前一定是均值回归的
- 当小米被纳为成分股,由于机构跟踪效应,指数期货一定升水
- order imbalance (盘口压力对价格有显著的影响)
- 5档订单量反映了市场流动性与估值分布
- 限价单的数量与价差大小正相关
- 订单流的方向具有长期记忆性, 由于投资者的拆单行为
- 指数法则:不同价格深度上新增限价单的到达概率与价格深度的指数成反比
- put call parity
- 收益的偏度衡量skewness是单调函数一定能够回归(Charlie:为什么偏度skewness反转可以作为因子?)
- 波动率越大的标的含有的风险越大,因此需要更高的预期收益来补偿
- Charlie:从零搭建因子库投资模型框架
- Charlie:预测因子库索引
逻辑性因子通常是通过线性(OLS,lasso,lassocv)或者非线性(xgboost,lgbm)的方式组合。非线性是在牺牲可解释性来换利润。两者没有很大本质上的区别。这种方法最大的问题是因子不稳定带来的过拟合,而且是在单一时间切片进行的多因子拟合;虽然线性可以用正则项,树模型可以优化叶子数据量和深度来防过拟合。真正对付过拟合和欠拟合的方法还是会在此文机器学习篇里详述。
def baseline():
df_train, df_test = train_test_split(df_etf, test_size=0.2, shuffle=False)
np.random.seed(41)
#scaler1 = MinMaxScaler(feature_range=(0, 1))
#x_train = scaler1.fit_transform(df_train.loc[:, df_train.columns.str.contains(&#39;feature&#39;)]).copy()
#scaler2 = MinMaxScaler(feature_range=(0, 1))
#x_test = scaler2.fit_transform(df_test.loc[:, df_test.columns.str.contains(&#39;feature&#39;)]).copy()
x_train = df_train.loc[:, df_train.columns.str.contains(&#39;feature&#39;)].copy()
x_test = df_test.loc[:, df_test.columns.str.contains(&#39;feature&#39;)].copy()
y_train = df_train[[&#39;return&#39;]].copy()
y_test = df_test[[&#39;return&#39;]].copy()
model = linear_model.Lasso(alpha=0, normalize=False, positive=False, fit_intercept=False) # this is just OLS
model.fit(x_train, y_train)
# cv = RepeatedKFold(n_splits=4, n_repeats=2, random_state=1)
# model = LassoCV(alphas=np.arange(0.00000001,0.000001,0.00000005), cv=cv, n_jobs=-1, fit_intercept=False)
coef = model.coef_ # the model.coef_ returns an array with size 1*n
print(coef)
y_pred_test = np.dot(coef, x_test.T)
y_pred_train = np.dot(coef, x_train.T)
# eval
rmse_test_test = mean_squared_error(y_test, y_pred_test) ** 0.5
print(f&#39;The RMSE of prediction for test is: {round(rmse_test_test*100,4)}%&#39;)
print(f&#39;IC is {pearsonr(y_test.values.T[0], np.array(y_pred_test))}&#39;)
return df_test,y_pred_test
def lassocv():
df_train, df_test = train_test_split(df_etf, test_size=0.2, shuffle=False)
np.random.seed(41)
#scaler1 = MinMaxScaler(feature_range=(0, 1))
#x_train = scaler1.fit_transform(df_train.loc[:, df_train.columns.str.contains(&#39;feature&#39;)]).copy()
#scaler2 = MinMaxScaler(feature_range=(0, 1))
#x_test = scaler2.fit_transform(df_test.loc[:, df_test.columns.str.contains(&#39;feature&#39;)]).copy()
x_train = df_train.loc[:, df_train.columns.str.contains(&#39;feature&#39;)].copy()
x_test = df_test.loc[:, df_test.columns.str.contains(&#39;feature&#39;)].copy()
y_train = df_train[[&#39;return&#39;]].copy()
y_test = df_test[[&#39;return&#39;]].copy()
cv = RepeatedKFold(n_splits=4, n_repeats=2, random_state=1)
model = LassoCV(alphas=np.arange(0.00000001,0.000001,0.00000005), cv=cv, n_jobs=-1, fit_intercept=False)
model.fit(x_train, y_train)
coef = model.coef_ # the model.coef_ returns an array with size 1*n
print(coef)
y_pred_test = np.dot(coef, x_test.T)
y_pred_train = np.dot(coef, x_train.T)
# eval
rmse_test_test = mean_squared_error(y_test, y_pred_test) ** 0.5
print(f&#39;The RMSE of prediction for test is: {round(rmse_test_test*100,4)}%&#39;)
print(f&#39;IC is {pearsonr(y_test.values.T[0], np.array(y_pred_test))}&#39;)
return df_test,y_pred_test
def lgbm():
df_train, df_test = train_test_split(df_etf, test_size=0.3, shuffle=False)
np.random.seed(41)
x_train = df_train.loc[:, df_train.columns.str.contains(&#39;feature&#39;)].copy()
x_test = df_test.loc[:, df_test.columns.str.contains(&#39;feature&#39;)].copy()
y_train = df_train[[&#39;return&#39;]].copy()
y_test = df_test[[&#39;return&#39;]].copy()
lgb_train = lgb.Dataset(x_train, y_train)
lgb_test = lgb.Dataset(x_test, y_test, reference=lgb_train)
params = {
&#39;boosting_type&#39;: &#39;gbdt&#39;, # Gradient Boosting Decision Tree
&#39;objective&#39;: &#39;regression&#39;,
&#39;metric&#39;: {&#39;l2&#39;, &#39;l1&#39;}, # mean_absolute_error and mean squared error
&#39;num_leaves&#39;: 62, # default complexity of each tree depth, controls final number of nodes (important as it increases number of unique y&#39;s), reduce value to prevent overfitting
&#39;max_depth&#39;: 20, # lgbm grows leaves first then depth, controls overfitting
&#39;min_data_in_leaf&#39;: 10, # controls overfitting, increase the number of nodes to reduce overfitting 叶子节点均值
&#39;learning_rate&#39;: 0.05, # lambda shrinkage rate, small lambda requires large number of trees; if small it takes a long time to converge, if large it may never converge
&#39;feature_fraction&#39;: 0.9, # deal with overfitting by randomly select % subset of features on each iteration tree
&#39;bagging_fraction&#39;: 0.6, # deal with overfitting by randomly select % subset of data on each iteration tree
&#39;bagging_freq&#39;: 5, #perform bagging at every 5th iteration
&#39;verbose&#39;: -1, # <0=fatal, 0=error, 1=info, >1=debug
&#39;num_iterations&#39;: 100 # number of class * number of iterations, or number of boosting trees
}
print(&#39;Starting training...&#39;)
gbm = lgb.train(params,lgb_train,valid_sets=lgb_test,num_boost_round=20,early_stopping_rounds=5,verbose_eval=False)
print(&#39;Saving model...&#39;)
gbm.save_model(&#39;model.txt&#39;)
print(&#39;Starting predicting...&#39;)
y_pred_test = gbm.predict(x_test, num_iteration=gbm.best_iteration)
# evaluation
y_test.replace(to_replace=[-np.inf, np.inf, np.nan], value=0, inplace=True, regex=False)
y_pred_test[(y_pred_test == -np.inf) | (y_pred_test == np.inf) | (y_pred_test == np.nan)] = 0
rmse_test_test = mean_squared_error(y_test.values, y_pred_test) ** 0.5
print(f&#39;The RMSE of prediction for test is: {round(rmse_test_test*100,4)}%&#39;)
print(f&#39;IC is {pearsonr(y_test.values.T[0],np.array(y_pred_test))}&#39;)
print(f&#39;count of unique y: {len(np.unique(y_pred_test))}&#39;)
#print(pd.DataFrame({&#39;Feature&#39;: df_train.loc[:, df_train.columns.str.contains(&#39;feature&#39;)].columns, &#39;Value&#39;: gbm.feature_importance()}).sort_values(by=&#39;Value&#39;, ascending=False)[0:10])
plt.figure()
plt.plot(y_test.values, label=&#39;y_test&#39;)
plt.plot(y_pred_test, label=&#39;y_pred_test&#39;)
plt.legend()
plt.grid()
plt.xticks(rotation=25)
plt.show()
return df_test,y_pred_test遗传规划-genetic planning(非逻辑性因子)
遗传规划是通过自定义的函数与算子输入,根据目标fitness函数,优化产生一系列高阶组合后的表达式因子。俗称的“挖”因子一部分归功于遗传规划里寻找不同的输入函数去组合,输入函数也可以是已经失效的talib量价因子。遗传规划是优生劣汰的概念,所以本质也是样本内线性对因子的组合。因此它的效果其实不如大家口中的圣杯深度学习,因为后者是多个隐藏层非线性的特征学习。
图12(遗传规划制作的因子示例)
比如worldquant因子alpha101就是用GP构造事后再人工通过经济意义筛选的。把GP产生的因子通过人工逻辑检验,再放入深度学习。类似于线性,非线性,与深度学习相加的ensemble learning集成学习。比如华泰的AlphaNet。
以下代码包含了:
- 构建函数集
- 算子
- fitness函数
- gplearn模型的调参与调用:(API reference - gplearn 0.4.1 documentation)
#x的泛化函数
user_function = [square, cube, delta1, delta2, delay1, delay2, ts_argmax, sma, stddev, ts_argmin, ts_max, ts_min, ts_sum, ts_rank, ts_argmaxmin, corr, rank, scale, product,
dema, kama, ma, midpoint,natr, trix, beta, lr_angle, lr_intercept,
lr_slope, ht, midprice, aroonsc, willr, cci, adx, mfi, macd, rsi, natr, norm_cdf,residual,beta]
init_function = [&#39;add&#39;, &#39;sub&#39;, &#39;mul&#39;, &#39;div&#39;, &#39;sqrt&#39;, &#39;log&#39;, &#39;abs&#39;, &#39;neg&#39;, &#39;inv&#39;, &#39;max&#39;, &#39;min&#39;, &#39;sin&#39;,
&#39;cos&#39;, &#39;tan&#39;, &#39;diff&#39;]
def _pearson(y, y_pred, w):
&#34;&#34;&#34;Calculate the weighted Pearson correlation coefficient.&#34;&#34;&#34;
with np.errstate(divide=&#39;ignore&#39;, invalid=&#39;ignore&#39;):
y_pred_demean = y_pred - np.average(y_pred)
y_demean = y - np.average(y)
up = pd.Series(list(filter(lambda x: x != 0, abs(y_demean)))).median() * 4
down = pd.Series(list(filter(lambda x: x != 0, abs(y_demean)))).median() * -4
y_demean[y_demean > up] = up
y_demean[y_demean < down] = down
corr = (np.sum(y_pred_demean * y_demean) / np.sqrt(np.sum(y_pred_demean ** 2) * np.sum(y_demean ** 2)))
if np.isfinite(corr):
return np.abs(corr)
return 0.
best_gp = SymbolicTransformer(
feature_names=fields,
function_set=init_function + user_function,
generations=3, # The number of generations to evolve (depth should be small)
metric=my_metric, # my_metric or &#39;mean absolute error&#39;, &#39;mse&#39;, &#39;rmse&#39;, &#39;pearson&#39;, &#39;spearman&#39;, &#39;log loss&#39;, https://gplearn.readthedocs.io/en/stable/_modules/gplearn/fitness.html?highlight=rmse
population_size=8000, # This controls the number of programs competing in the first generation and every generation thereafter.
# If you have very few variables, and have a limited function set, a smaller population size may suffice. (length cannot be too big or small)
tournament_size=20, # Now that we have a population of programs, we need to decide which ones will get to evolve into the next generation.
# In gplearn this is done through tournaments. From the population, a smaller subset is selected at random to compete, the size of which is controlled by the tournament_size parameter.
# The fittest individual in this subset is then selected to move on to the next generation.
random_state=0,
n_components=20, # return number of alpha factors
verbose=2,
parsimony_coefficient=0.0001,
p_crossover=0.4,
p_subtree_mutation=0.01,
p_hoist_mutation=0,
p_point_mutation=0.01,
p_point_replace=0.4,
n_jobs=6
)
best_gp.fit(X_train, y_train)深度学习(可用于逻辑性或非逻辑性因子)
神经网络由以下构成:输入神经元特征,权重,bias/threshold来给激活函数最后输出。反向传播用来梯度优化损失函数求得最优权重。转换函数(relu/sigmoid),深度隐藏层,遗忘值用来控制基于时间序列的特征学习与求导时的梯度消失/爆炸。
用深度学习的主要原因有如下。
- 从学术角度深度学习预测时间序列明显是有用的
- 比如预测天气我想把湿度因子*0.2和过去2.8天的平均温度相加。深度学习可以储存和表达传统统计难以描述的东西。股票信息比衍生品多得多,深度学习更适合于复杂度高的数据。
- 深度学习的数据是基于三维的tensor(数据点 * 滚动窗口步长* 特征数量),可以用最新的数据多时间切片滚动拟合,但是步长不能太长
- 深度学习的模型是超参的,理论上不会过拟合的。因为当模型复杂度包括变量,数目,结构远超过数据量的时候,冗余的参数权重会逐渐降低,导致测试集误差先升后降(double descent),所谓大力出奇迹。但是也要过1-2个月做一次重新训练调参来融合最近的市场风格突变。最新样本的信息量是很大的,更新的慢就会被收割。
- 深度学习也不欠拟合。因为可以在每个隐藏层都遗忘80%的数据model.add(Dropout(0.8)),来保证当隐藏层数量增多,从右到左每个权重的偏导数不会慢慢消失,以得到一个显著比初始参数化好的模型
def RNN_LSTM():
start_lstm = time.time()
df_train, df_test = train_test_split(df_etf, test_size=0.2, shuffle=False)
tf.random.set_seed(41)
np.random.seed(41)
dropout_rate = 0.8 # % of layers will be dropped
lstm_size = 64 # number of memory cells, less could be underfitting
#scaler1 = MinMaxScaler(feature_range=(0, 1))
#df_feature_train = scaler1.fit_transform(df_train.loc[:, df_train.columns.str.contains(&#39;feature&#39;)]).copy()
df_feature_train = df_train.loc[:, df_train.columns.str.contains(&#39;feature&#39;)].copy().values
timestep = 60 # use days to predict next 1 day return
x_train = []
y_train = []
for i in range(timestep, df_feature_train.shape[0]): # disgard the last &#34;timestep&#34; days
# x_train.append(df_train[[&#39;feature0&#39;,&#39;feature1&#39;,&#39;feature2&#39;,&#39;feature3&#39;,&#39;feature4&#39;,&#39;feature5&#39;,&#39;feature6&#39;,&#39;feature7&#39;]].iloc[i-timestep:i].values)
x_train.append(df_feature_train[i - timestep:i]) # rolling_timestep * features
y_train.append(df_train[[&#39;return&#39;]].iloc.values) # days * (no rolling_timestep) * features
x_train, y_train = np.array(x_train), np.array(y_train) # days * rolling_timestep * features
# x_train = np.reshape(x_train, (x_train.shape[0], x_train.shape[1], 1))
# Initialising the RNN
model = Sequential()
# Adding the first LSTM layer and some Dropout regularisation
model.add(LSTM(units=lstm_size,return_sequences=True,input_shape=(timestep, x_train.shape[2]))) # units are neurons or cells, represents the sequence as an internal embedding
model.add(Dropout(dropout_rate))
# Adding a second LSTM layer and some Dropout regularisation
model.add(LSTM(units=lstm_size,return_sequences=True))
model.add(Dropout(dropout_rate))
# Adding a third LSTM layer and some Dropout regularisation
model.add(LSTM(units=lstm_size,return_sequences=True))
model.add(Dropout(dropout_rate))
# Adding a fourth LSTM layer and some Dropout regularisation
model.add(LSTM(units=lstm_size,return_sequences=False)) # return one output for each input time step
model.add(Dropout(dropout_rate))
# adding output layer
model.add(Dense(units=1)) # reducing dimension of vectors, weight sharing dense layer for every # of timestep
# compiling the RNN
model.compile(optimizer=&#39;adam&#39;,loss=&#39;mean_squared_error&#39;)
model.fit(x_train,y_train,epochs=30,batch_size=128) # batch size is amount of data used in gradient computation and back propagation for speeding up
x_test = []
#scaler2 = MinMaxScaler(feature_range=(0,1))
#df_feature_test = scaler2.fit_transform(df_test.loc[:,df_test.columns.str.contains(&#39;feature&#39;)]).copy()
df_feature_test = df_test.loc[:, df_test.columns.str.contains(&#39;feature&#39;)].copy().values
for i in range(timestep, df_feature_test.shape[0]):
x_test.append(df_feature_test[i-timestep:i]) # timestep * features
x_test = np.array(x_test) # days * timestep * features
y_pred = model.predict(x_test)
y_test = df_test[timestep:][[&#39;return&#39;]]
rmse_test_test = mean_squared_error(y_test, y_pred) ** 0.5
print(f&#39;RMSE of prediction for test set is: {round(rmse_test_test*100,4)}%&#39;)
print(f&#39;IC is {pearsonr(y_test.values.T[0], y_pred.T[0])}&#39;)
print(f&#39;LSTM finished in {round((time.time() - start_lstm) / 60,2)} mins&#39;)
return df_test[timestep:],y_pred
机器学习
机器学习的核心是数据,和用一个固定的benchmark来评估模型拟合输入输出后的好坏。
一:数据
如果因子是模型的天花板,那数据就是因子的天花板。除了基本的数据清洗如去空值,补缺失,复权等,我们还需要去噪音(去涨停,去期权到期日,去重大新闻日,去极值),分布检验,平稳性检验,随机性检验,和样本标签分类。
1. y需要独立同分布I.I.D
无效的波动能产生alpha,但是规律时时刻刻在变。所有预测能转化为利润的前提是,测试集样本的分布和训练集样本的分布足够相似,只有在这个前提下,历史上发生过的规律才有可能被用来预测未来。如果y不是独立同分布,那滚动60天预测与滚动1天预测明天的return就没有区别了。如果y不是独立同分布,那模型能预测明天的return不代表模型也能预测后天的return。如果滚动从9:35-10:35和9:36-10:36算两个样本,这样y的分布就很难是IID。
这一步的结果要得到一个最优y的预测周期,而不能是由不同y周期加权靠到高频的求和,比如:
以上公式的由来是,每一刻都有不同周期的交易者。在最高频30分钟的那一刻,看向所有未来周期的交易者,那么weighted sum就是用来模拟实际利润。但这个公式的问题是,未来周期与分母权重的挑选又变成了策略的一部分,而且它并不是完全真实的,导致模型预测无法转化为利润。因此对于y的选择还得回归到假设,是赚日内的钱还是隔夜跳空的钱。遍历的时候y可以往频率高的上面设再看看decay怎么样,优质样本下对不同y预测效果不会差很多。只不过是之后换手率和策略容量的问题了。
如果把截面选股的收益当作y,那是比做时间序列预测用的y更IID的,未出现规律更少。目前所有私募的cta或者股票量价预测都在往低频横截面走,就是想把选择标的产生的rank IC当作y。具体的操作并不在这篇文章的scope中,这也是作者目前主要的研究方向。
1.1 标准误差检验
标准误差检验的核心是剔除波动率风格因子。如果y是独立同分布,原则上IC mean应该等于IC total,并且IC不会有衰减因为 。但是在“因子评估标准”章节中演示了实际IC是很不稳定的。
标准误差的定义是:
若样本在j到t天是独立同分布那么:
df[&#39;pct_chg&#39;].rolling(60).std()/np.sqrt(60)
图13(用标准误差检验样本是否iid)
样本标准误差越小说明样本对整体分布的代表性越好。左图13是60日滚动窗口的低频样本,右图13是1小时滚动窗口的高频样本。结论是虽然我们无法预测未来样本的分布,但是长期来说样本外的分布不会永久偏离样本内的分布,这给了我们信心做下文会介绍的样本标签。
1.2 Kolmogorov-Smirnov 检验
K-S检验是考察两个样本所服从的分布是否显著不同的非参数方法,K-S统计量越小,表明两个分布越接近。
图14(K-S检验样本IID)
1.3 平稳性检验
时间序列上每个点都是独立同分布的假设不现实,因此我们定义弱平稳:
- 均值不变
- 方程稳定
- 两点之间协方差只和步长有关,与时间无关
实际交易中平稳性很重要,因为买卖都是基于时间序列上一个固定的均值来定义的。而且用rolling往往解决不了不平稳的问题,因为你不可能知道最佳的参数是rolling1分钟还是5分钟还是20分钟。
平稳统计性质可以泛化到不平稳的时间序列,但反之就不可以“There is no way to infer something from non-stationary and non-ergodic time series.”虽然价格的收益率大部分时间平稳,在研究时也要滤除不平稳的时段当作噪音,因为这部分的规律自相关性太高,无法提取并用到预测。
检查平稳性的方法有如下:(Why Non-Stationarity shouldn’t be ignored in Time Series Forecasting?)
- 肉眼看
- 观察acf图
- 各种unit root测试
- multiple differencing差分处理
- demean处理
之前机器学习论坛里有个非常热门的话题:为什么股票价格跟得紧,但是收益率预测却很差。在随机数学里,股票是用二项分布的涨跌模拟的;硬币抛无限多次后,binomial的极限就是连续指数函数,也因此有log return一说。(Charlie:金融衍生品(上)数学模型与编程)因为指数函数的不平稳,交易员不能知道现在的价格和过去比是贵了,还是和未来比便宜了。股价预测股价的IC形状会像图14.3单调,因为t0和t1股价的相关性几乎是1,t0和t2股价的相关性也几乎是1(见图1左)。证明了用今天的价格来预测明天,y是不平稳的,而且对明天价格的预测实际上是基于今天已经得到能最优解释明天股价的信息了,因此研究段价格预测价格得到的高IC其实用到了未来函数,和y自己的acf来作弊。
同理,当我们看到像图14.3这种IC decay单调递增的形状要特别小心。因为预测未来1天,2天,或3天的收益相当于用过去1天,2天,或3天的x作图。如果x是一个时间序列上很像y的东西,那研究员看到的IC延续性本质上是y在违反第三条弱平稳定义后产生的自相似度acf,而不是真正的信号持有越久利润就越高!
图14.3 abs IC decay
1.4 去时间序列相关性
- 皮尔森相关
- 斯皮尔曼相关
- 时间滞后相关
- 偏相关
- DTW动态时间扭曲
- 残差分析
- 余弦距离
1.5 去噪音
去噪音目的很简单,就是便于提取规律。
- 去涨停板
- 去新闻日
- 去到期日
- 傅里叶变换
- Charlie:Stock Prediction by Signal Processing - FFT, Kalman Filter, and Unit Root Tests
- Charlie:用傅里叶变换做股价预测
- 大波小波定理
- Savitzky-Golay过滤器
- 插值平滑处理(cubic spline)
- 滑动平均过滤
2. x信号处理
为了做研究的对比和求线性相关性,信号值x也需要做到平稳,正态分布,归一化,和去极值。如果需要,转换工具之后还可以再把x转回原始因子值。x的泛化函数参考遗传规划部分代码。
def zscore(series):
series_copy = pd.Series(series).copy()
series_copy.replace(to_replace=[-np.inf, np.inf], value=np.nan, inplace=True, regex=False)
up = pd.Series(list(filter(lambda x: x != 0, series_copy.abs()))).median() * 12
down = pd.Series(list(filter(lambda x: x != 0, series_copy.abs()))).median() * -12
series_copy[series_copy > up] = up
series_copy[series_copy < down] = down
zscore = (series_copy - series_copy.mean())/series_copy.std()
zscore.replace(to_replace=[-np.inf, np.inf, np.nan], value=0, inplace=True, regex=False)
return np.array(zscore) # to prevent index mismatching在测试集回测的时候信号极值也应该被处理,但不能用到总体population的任何信息,因为unshuffle的测试集都是未来数据。只有当假设x是时间序列平稳成立后,才能用相同的均值在样本内和样本外做归一化。
3. 样本标签分类
给样本分类的出发点是,找出类似分布的行情打标签,为了更有效的提取规律。分类天然比回归不容易过拟合,解释性强,而且目标值已经被泛化过模型预测难道大幅下降。如果分类没有结果,那回归一定不会有结果,所以分类应该优先研究。可分类的难点是这个概率也是不停变动的,不同时期的分布不同。而且0和1无法把交易量提上去。
图14.1 分类联合概率
分类还有一个用处是可以用到条件概率和和函数找多维度的希尔伯特空间映射。比如我在所有历史数据中找到了中等波动率并且会上涨的概率。一个预测基于另一个预测是条件概率贝叶斯的概念,后验概率A基于B的准确率是先验概率A和B之间联合概率的乘积再除以B的准确率。B的准确率包含了A是对的和A是错的。丹尼尔卡尔曼在《思考快与慢》里讲了,贝叶斯容易误导人的原因是下面公式的分子 , B是条件概率A的证据,因此也叫likelihood。人们的偏见是把likelihood当作真实概率,但其实B只能更新A的概率而不是取代A的概率。(这个知识点最好的教材可以谷歌搜索3blue1brown)
偏见容易把B的概率直接当成A基于B的条件概率:
- 比如A是得新冠的概率,B是核算检测错误的概率
- 比如A是期权delta预测准确率,B是期权vega预测准确率
- 比如A是股票量价预测准确率,B是股票基本面预测准确率
以条件贝叶斯为原型的机器学习方法LDA,QDA,Naive Bayes等。原理是通过损失函数优化多元正态分布里的均值和协方差矩阵,来达到预测的目的。
图14.2 多元正态分布
可这里偷换的概念是,分类的预测又变成了不同标签协方差矩阵变化的预测。
3.1 ATR样本分类 - average true range
True range指的是真实波动幅度,ATR的计算方法如图15
图15(用ATR做样本标签)
其他实际波动幅度的刻画方式见(Charlie:高频波动率交易)
3.2 波动率样本分类
分钟波动率<18%:回归态,pacf在零上徘徊
分钟波动率在18%-25%:趋势态,实际波动率高于隐含波动率,高gamma高vega
分钟波动率>25%:反转态,波动率过高容易反转,容易产生vega敞口,实际波动率低于隐含波动率,高gamma,低vega,交易换手率高,beta行情下策略要止损因为信号容易掰不回来
3.3 宏观分类+主观择时
今天从吾执2022年策略会上我了解到孙总还可以用经验对宏观调控政策,板块聚集性指标,波动率指标来主观择时。用分类均线做benchmark,分别在线上和线下调配不同策略。比如配短线的cta来防守,配长线cta来进攻波动率趋同性的肥尾收益。主观择时还包括了切换股票,cta,期权策略的权重。
二:过拟合与欠拟合
除了之前介绍的深度学习和因子评估外,还有一个重要的概念是过拟合。好消息是在样本数据合格的情况下,机器学习对拟合问题有许多解决方案。
- 降维的本质是减少没有贡献的弱分类器(lasso加正则项,去共线性,best subset,mutual information,correlation matrix,树模型feature importance)。测试后我还没有发现一个模型的打分机制显著最优。但是确定的是要在去因子共同风险因素之后再看它们的相关性。
- 数值标准检验:PBO, Performance degradation, probability of loss, stochastic dominance
- 样本内外损失函数画图检验(见图16):
图16(机器学习样本内外过拟合分析结果)
强化学习(Reinforcement Learning)
RL的作用是利用信号产生最大交易收益的无监督模型。当样本之间没有前后时间依赖关系,监督学习就是RL的一个特例,具有马尔科夫性。一个强化学习算法包括以下部分:
- model
- 转移矩阵
- 奖励函数比如cnn,难在agent会尽可能作弊来获取最大收益,策略区间广,收敛速度慢
- policy
- 从状态到决策的映射
- value function
- 未来可以获得收益折现求和之后的期望值
隐形马尔科夫链(HMM)
class StockPredictor(object):
def __init__(self,
test_size=0.33,
n_hidden_states=4,
n_latency_days=10,
n_steps_frac_change=20,
n_steps_frac_high=5,
n_steps_frac_low=5):
self._init_logger()
self.n_latency_days = n_latency_days
self.hmm = GaussianHMM(n_components=n_hidden_states, verbose=True) # multinomial gaussian consists of transition matrix, prior probability, mu and sigma
self._split_train_test_data(test_size)
self._compute_all_possible_outcomes(n_steps_frac_change, n_steps_frac_high, n_steps_frac_low)
def _init_logger(self):
self._logger = logging.getLogger(__name__)
handler = logging.StreamHandler()
formatter = logging.Formatter(&#39;%(asctime)s %(name)-12s %(levelname)-8s %(message)s&#39;)
handler.setFormatter(formatter)
self._logger.addHandler(handler)
self._logger.setLevel(logging.DEBUG)
def _split_train_test_data(self, test_size):
#data = pd.read_csv(&#39;data/company_data/{company}.csv&#39;.format(company=self.company))
df_etf0 = pro.fund_daily(ts_code=&#39;510050.SH&#39;, start_date=&#39;20081126&#39;, end_date=&#39;20101125&#39;)
df_etf1 = pro.fund_daily(ts_code=&#39;510050.SH&#39;, start_date=&#39;20101126&#39;, end_date=&#39;20121125&#39;)
df_etf2 = pro.fund_daily(ts_code=&#39;510050.SH&#39;, start_date=&#39;20121126&#39;, end_date=&#39;20141125&#39;)
df_etf3 = pro.fund_daily(ts_code=&#39;510050.SH&#39;, start_date=&#39;20141126&#39;, end_date=&#39;20161125&#39;)
df_etf4 = pro.fund_daily(ts_code=&#39;510050.SH&#39;, start_date=&#39;20161126&#39;, end_date=&#39;20181125&#39;)
df_etf5 = pro.fund_daily(ts_code=&#39;510050.SH&#39;, start_date=&#39;20181126&#39;, end_date=&#39;20211125&#39;)
df = pd.concat([df_etf0, df_etf1, df_etf2, df_etf3, df_etf4, df_etf5], axis=0)
df.index = df[&#39;trade_date&#39;].map(lambda x: dt.datetime.strptime(str(x), &#39;%Y%m%d&#39;))
df.sort_index(inplace=True)
_train_data, test_data = train_test_split(df, test_size=test_size, shuffle=False)
self._train_data = _train_data
self._test_data = test_data
@staticmethod
def _extract_features(data):
open_price = np.array(data[&#39;open&#39;])
close_price = np.array(data[&#39;close&#39;])
high_price = np.array(data[&#39;high&#39;])
low_price = np.array(data[&#39;low&#39;])
# 计算收盘价、高价和低价的分数变化
# 这会用到一个特征
frac_change = (close_price - open_price) / open_price
frac_high = (high_price - open_price) / open_price
frac_low = (open_price - low_price) / open_price
return np.column_stack((frac_change, frac_high, frac_low))
def fit(self):
self._logger.info(&#39;>>> Extracting Features&#39;)
feature_vector = StockPredictor._extract_features(self._train_data)
self._logger.info(&#39;Features extraction Completed <<<&#39;)
self.hmm.fit(feature_vector)
def _compute_all_possible_outcomes(self, n_steps_frac_change,n_steps_frac_high, n_steps_frac_low):
frac_change_range = np.linspace(-0.1, 0.1, n_steps_frac_change)
frac_high_range = np.linspace(0, 0.1, n_steps_frac_high)
frac_low_range = np.linspace(0, 0.1, n_steps_frac_low)
self._possible_outcomes = np.array(list(itertools.product(frac_change_range, frac_high_range, frac_low_range)))
def _get_most_probable_outcome(self, day_index):
previous_data_start_index = max(0, day_index - self.n_latency_days)
previous_data_end_index = max(0, day_index - 1)
previous_data = self._test_data.iloc[previous_data_start_index:previous_data_end_index]
previous_data_features = StockPredictor._extract_features(previous_data)
outcome_score = []
for possible_outcome in self._possible_outcomes:
total_data = np.row_stack((previous_data_features, possible_outcome))
outcome_score.append(self.hmm.score(total_data))
maa = np.argmax(outcome_score)
most_probable_outcome = self._possible_outcomes[np.argmax(outcome_score)]
return most_probable_outcome
def predict_close_price(self, day_index):
global test_df
test_df = self._test_data
open_price = self._test_data.iloc[day_index][&#39;open&#39;]
predicted_frac_change, frac_high_range, frac_low_range = self._get_most_probable_outcome(day_index)
return open_price * (1 + predicted_frac_change)
def predict_close_prices_for_days(self, days, with_plot=False):
global predicted_close_prices
predicted_close_prices = []
for day_index in tqdm(range(days)):
predicted_close_prices.append(self.predict_close_price(day_index))
if with_plot:
test_data = self._test_data[0: days]
#days = np.array(test_data[&#39;trade_date&#39;], dtype=&#34;datetime64[ms]&#34;)
days = np.array(test_data.index)
actual_close_prices = test_data[&#39;close&#39;]
fig = plt.figure()
axes = fig.add_subplot(111)
axes.plot(days, actual_close_prices, &#39;bo-&#39;, label=&#34;actual&#34;)
axes.plot(days, predicted_close_prices, &#39;r+-&#39;, label=&#34;predicted&#34;)
axes.set_title(&#39;{company}&#39;.format(company=&#39;etf&#39;))
fig.autofmt_xdate()
plt.legend()
plt.show()
return predicted_close_prices
stock_predictor = StockPredictor()
stock_predictor.fit()
stock_predictor.predict_close_prices_for_days(50, with_plot=True)
df_etf = test_df.copy()
结语
通过个人经验,同行交流,kaggle,参加头部策略会,我发现只用几十个稍有逻辑性的量价因子,不管怎么组合分类,单品种时间序列上的预测高频或低频上都不会有好的效果。
暴力挖掘一千以上足够多有逻辑的因子(不能是比如tangent这种无用的三角函数),然后根据因子结构离散程度,选择树模型做分类的概率预测,或者强化深度模型做回归预测,有时候能大力出奇迹,有时候不能。这就要看用在哪,怎么用,和每家的功力了。
最后从截面角度,无论是高频,中频,或低频的多品种CTA(统计套利)或者股票策略(选牛股)都是2022年每家私募的重点策略。一定可以做,但就要看谁在冷门因子挖掘方面更卷了。
微信公众号:gnu_isnot_unix
添加图片注释,不超过 140 字(可选) |
|