跳转至




2024

Barra风险模型构建完全指南

Barra风险模型是业内最有名的多因子模型之一。它最初由Barra Inc(创始人Barr Rosenberg)提出,后来被MSCI(明晟)收购,因此现在是MSCI的资产。在MSCI网站上,我们还可以看到名为BarraOne的产品推介。

很多想学习Barra模型的人,不知道Barra有一个官方的完全指南(《Barra Risk Model Handbook》),全文共204页,系统地阐述了如何构建Barra风险模型,是最权威的一本指南。

Barr Rosenberg大学主攻的是文学(UC Berkeley),后来才转攻经济学。Everbody can quant,只要你想。


关于Barra模型,常见的误解之一就是它是用来预测的投资模型。但实际上,它只是风险归因分析模型。关于这一点,最权威的说法无疑来自Barra自己的介绍:

Quote

Of course, MFMs have their limitations. They predict much, but not all, of portfolio risk. In addition, they predict risk, not return; investors must choose the investment strategies themselves.

手册先用10页左右的篇幅解释了什么是多因子模型,以及相关的数学基础。

Tip

关于因子分析的数学基础,可以参考Peter Tryfos(约克大学教授)的《商业分析与预测方法》中的第14章,讲得深入浅出。

在第二和第三部分,先是回顾了从马科维茨到CAPM以及APT,然后介绍了Barrar 权益多因子模型:

50%



下载

暴力美学!洗盘模式如何检测?

无洗盘,不拉升。 筹码收集阶段,股价呈现出上涨形态,也吸引到许多不坚定的跟风盘,它们将成为主升过程中的不利因素。

因此,在拉升之前,主力会采用洗盘的方式,将这些不坚定的低价筹码洗下车。这个过程中往往暴涨暴跌,犹如一匹烈马,要摆脱它身上的骑手一样。

暴力洗盘,某种程度上就成为了行情快速上涨之前的信号之一。

这篇文章,我们量化实现的技术问题:如何快速检测出洗盘模式?


定义

L50

暴力洗盘是在证券市场上观察到的一种经验模式,因此没有严格的定义。一般把两阳夹一阴、且涨跌幅都巨大的情况认为是暴力洗盘。在本文中我们把两阳夹两阴、且涨跌幅都较大的情况定义为暴力洗盘。但我们介绍的方法,也完全适用于其它模式,只需要微调参数即可。

如左图所示,标的在1号位置之前,经过一段时间的吸筹,由于期间股价上涨,已经吸引了一些跟风盘。主力在1号位置拉出20cm,在这一过程中,较多跟风筹码被锁定在涨停位置。

第2天起主力开始洗盘,连续两天,分别下跌14.4%和18.9%。此时在1号位置买入的筹码因为忍受不住巨大的跌幅,忍痛交出筹码。主力筹码增加,成本降低,为后面的拉升留出了空间。

第4天主力将个股拉涨9.4%,表明洗盘结束。

随后几天的整理,主要是留出时间,让下一波的跟风盘有时间发现这支标的,并且有信心跟随买入。紧接着使用一系列小阳线做出上升趋势,最终再拉出一个20cm,从第4天起,短期涨幅高达87%。


我们为什么要使用两阳夹两阴的4天模式来定义洗盘?

因为经过两天的洗盘,从时间和空间上看,洗盘效果会更好(考虑到交易者心理因素,一些人第一天亏损后,往往还不会绝望,第二天继续下跌,更容易崩溃卖出)。另外,从一些技术指标上来看,经过连续两天大幅下跌,技术指标修复比较到位,也更能为后面的拉升腾出上涨空间。

我们为涨跌幅设置一个阈值,如果期间的每个bar的涨跌幅超过这个阈值,我们就认为发生了洗盘。在我们的示例中,使用的阈值是0.05,即涨跌5%。

下面我们来看代码实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 示例1
def feature_washout(bars, threshold=0.05):
    """返回在bars中最后一次洗盘结束的位置,-1表示最后一个bar,
        0表示不存在洗盘模式
    """
    close = bars["close"]
    opn = bars["open"]
    truerange = np.maximum(np.abs(close[1:] - close[:-1]), 
                           np.abs(opn-close)[1:]) 
    # 百分比化
    tr = truerange / close[:-1]
    sign = (opn < close)[1:] * 2 - 1
    signed_tr = tr * sign

这里我们使用了truerange这个变量名,是因为这段代码脱胎于技术指标TR

这段代码解决如何将涨跌幅转换为由1,-1和0表示的模式,以便我们后面进行模式检索。

如果当天涨跌超过5%,或者实体的振幅超过5%,我们就将其标记为1或者-1,否则标记为0。标记的符号由它的形态是阴线还是阳线决定。阴线为-1,阳线为1.

我们通过这样一段简单的代码就实现了求阴阳线的功能:

1
(opn < close) * 2 -1

其结果将生成由1和-1组成的数组。无论是涨还是跌,我们总是认为,阴线是洗盘。所以,高开大阴线,即使收盘是上涨的,我们也当成洗盘来处理。

下图就是高开大阴线洗盘一例:

75%


在判断每个bar的涨跌幅、或者实体的振幅是否超过阈值时,我们使用了一个简单的技巧,即通过np.maximimum来从多个数组中,以 element-wise 的形式选取最大值。即,如果有数组\(A\)\(B\),那么\(np.maximum(A, B)\)将返回一个数组,其元素为\(A\)\(B\)对应位置的元素中的较大值。

也就是,如果结果是\(C\),那么\(C_0\)将是\(A_0\)\(B_0\)中的较大值,\(C_1\)将是\(A_1\)\(B_1\)中的较大值,以此类推。

除了使用\(np.maximimum\)这种 ufunc 之外,实际上\(np.max\)也可以用来完成这项任务,只是我们需要先将数组\(A\)\(B\)堆叠成一个矩阵:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 示例2
A = np.arange(4)
B = np.arange(3, 7)
C = np.arange(8, 4, -1)

Z = np.vstack((A,B,C))

# 通过np.max求每列最大值
r1 = np.max(Z, axis=0)

# 通过np.maximum求最大值
r2 = np.maximum.reduce([A, B, C])

# 比较两种方法的结果是否相同
np.array_equal(r1, r2)

为了提供更多信息,示例中我们演示了三个数组按元素求最大值的情况。答案是要使用reduce方法。如果只在两个数组间进行比较,则可以只使用np.maximum

经过示例1处理后,我们可能得到如下所示的数组:

[ ... 0.04 -0.02 -0.06 0.04 -0.04 -0. 0.2 -0.14 -0.19 0.09 -0.03 ...]

显然,我们还应该把它二值化,转换成为[大阳,大阴,大阴,大阳](即[1, -1, -1, 1])这样的模式:

1
2
3
4
5
6
7
8
9
# 示例3
encoded = np.select([signed_tr > threshold, 
                    signed_tr < -threshold], 
                    [1, -1], 0)

for i in range(len(encoded) - 3, 0, -1):
    if np.array_equal([-1, -1, 1], encoded[i:i+3]):
        return i - len(encoded) + 2
return 0

我们通过select方法完成了二值化转换。接下来我们通过一个逆序的循环,通过array_equal完成了模式匹配。

在回测中,我们可能需要一次性地提取出很长一段行情中所有的洗盘模式,并对其效果进行检验。上面的代码还可以通过numpy.lib.stride_tricks.sliding_window_view进行优化:


1
2
3
4
5
6
7
8
def feature_washout(bars):
    ...
    washouts = []
    for i, patten in enumerate(sliding_window_view(encoded, window_shape = 4)):
        if np.array_equal(patten, [1, -1, -1, 1]):
            washouts.append(i)

    return washouts

通过将涨跌幅二值化,我们就可以在随后方便地通过array_equal来匹配模式。我们这样做,是因为在这里定性分析基本就够了,只要涨跌幅超过5%,那么无论是跌了5.1%还是7.2%,我们都认为是洗盘。

但是,如果你觉得定量分析仍然有意义,也可以通过求皮尔逊相关系数的方法来进行模式匹配。

不过,被匹配的模式,应该使用多少呢?如果你对此感兴趣,可以评论区留言,获得推荐参数。

前视偏差 - 看似明白,实则糊涂

之前有一篇关于数据标注的笔记,得到了较多关注,也引起了一些同学质疑:

你使用了zigzag函数,这不会引起未来数据吗?

未来数据更学术化的说法叫前视偏差(look-ahead bias),回测中引入了未来数据,确实是做量化中很容易犯的一个错误。

这篇笔记解释了zigzag为什么用在标注中,不会产生未来数据。我们还列举了一些其它前视偏差的例子。这些是量化的经验之谈,来自实践,很少能在论文中看到。

Zigzag与 peak_and_valley_pivots

在之前的笔记中,我提到了使用zigzag函数来寻找k线中的波峰与波谷,并作为监督学习的标注数据。如果波峰记为1,波谷记为-1,其它的都记为0,则我们一共生成了三类标签。

这个标注数据还包含了原始的k线数据。因此,你可以基于这个数据集,构造自己的特征数据来训练模型,使之能预测一段行情的顶和底。

构建特征数据(因子)时,可以用RSI、均线拐头、上下影线、成交量放大、整数位等支撑、压力指标、压力线、支撑线等等。

数据标注使用zigzag包里的peak_and_valley_pivots函数。这个包可以通过以下命令进行安装:

1
pip install zigzag

peak_valley_pivots接受三个参数,第一个是要检测峰谷的时间序列,第二个参数用来设置下跌的幅度、第三个参数用来设置上涨的幅度。

也就是说,在序列\([t_0, t_1, t_2]\)中,如果\(th_1\)是下跌阈值,\(th_2\)是上涨阈值,则当 \(t_1 >= t_0 \times (1 + th_2)\) 并且 \(t_2 <= t_1 \times (1 + th_1)\)时,\(t_1\)就会被标记为波峰,如下图所示:

在Python中,还存在其它许多类似的库,最著名的是来自scipy.signals模块下的 find_peaks, find_peaks_cwt, argrelextrema 等。这些函数都要求我们至少设置判断峰谷的涨跌阈值(绝对值),有的还会允许设置驻点窗口大小。但是,zigzag 包中的peak_valley_pivots 方便之处在于,它不是通过绝对值来设置阈值,而是通过输入一个百分比来设置 -- 这是我们更常遇到的场景。

Tip

数字信号处理(Digital Signal Processing)在量化上有特殊的用途。文艺复兴早期在招人时,就很看重具有数字信号处理经验的人,他们从IBM viavoice团队挖了不少人。早期的声音识别是典型的数字信号处理应用,现在才全部转为深度学习。像傅立叶变换、小波变换等技巧,至今仍有不少论文在研究。

在具体使用peak_valley_pivots时,有两个简单的技巧。

首先,如何设置涨跌阈值?在实践中,我们常常先求得时间序列的每期涨跌幅。对股价波动而言,这是一个近似于平稳的序列。对这个序列求其标准差,±2倍标准差就可以作为阈值。

如果股价的涨跌近似于正态分布,那么如果t1超过t0达到2倍标准差的概率应该小于5%,把它认为是一个Outlier(离群值)显然是合适的(注意这里有很多近似处理的地方。严格来讲,股价涨跌近似于正态分布,并且股价超过平均值2倍标准差,概率才会小于5%)-- 换句话说,它就是我们要找的峰(或者谷)。

另一个技巧是,你必须理解peak_valley_pivots是如何处理头尾的数据的。

为什么说“zigzag会引起前视偏差”?

无论如何,peak_valley_pivots都在序列索引为0和-1的位置处,返回峰(1)或者谷(-1)的标签,无论这些点上的涨跌是否达到了阈值。这也是我们这篇笔记讨论前视偏差的起点。

我们再回顾一次之前的标注图。这一次,我们加入了一些序号以方便讨论。

在这个图中,peak_valley_pivots 对位置1和位置2都给出了标注,分别是-1和1。

但是,我们的标注工具没有显示位置2的标注。因为此时这个标注并不能固定下来。当我们投喂更多的数据时,我们发现,在位置2处的标注消失了(指peak_valley_pivots的返回结果,而非图中的显示),直到4月6日下午14:30分,peak_valley_pivots 才重新给出了标注(见位置3)。

这是我们在标注历史数据时会发生的情况。如果我们的标注工具是一次性地对一个非常长的序列进行的标注,那么我们将只会在头和尾遇到一次这样的问题(⚠️⚠️但是,此时阈值会在这个序列的全生命期上进行计算,这个阈值将会与只在近期较短一段时间上计算出来的不一致)。

在对数据进行分批标注时,数据被截断,从而引入了更多的头和尾,在这些边界上, peak_valley_pivots都给出标注,但往往是不稳定的(对开头而言,它很可能是错的或者多出来的)。但从倒数第二个标注起,它就已经被固定下来,不会变更。

这样会引起未来数据吗?

当然不会。这个标注过程完全正确。

⚠️但一些同学使用zigzag,并不是用来做数据标注的,而是用它来做回测的,这就会出现问题。⚠️

这个回测错误具体地说是这样产生的:在\(t_2\)时间点时,检测到了发生在\(t_1\)时间点上的一个峰,此时,如果按\(t_1\)的价格去卖出:

  1. 如果回测系统是正确的,模拟交易不可能完成。如果能够完成,意味着股价又返身向上了,此时在\(t1\)和现在的时间点\(t_0\)之间,已经出现一个谷底。这意味着我们把该持有的股票,错误地卖出了。
  2. 有的同学可能使用自己写的回测系统,比如,通过pandas dataframe来做向量回测,此时就非常容易引入错误,导致在\(t_0\)时间点时,回到\(t_1\)时间点,以该点时的价格,将股票卖出在最高点。

这是一个很容易理解,但又常常会隐藏在你的代码中的错误 -- 如果你不了解回测系统的运行原理的话。

Tip

既然zigzag的顶底标注不能用来预测,那么它还有用吗?答案是肯定的。虽然我们不能用zigzag来预测顶底,但是,它可以用来标注历史数据,以验证我们找出来的关于顶和底的特征是否属于高概率特征。

如果是,那么在实盘中,当这些特征再次出现时,我们就敢于预测,一个顶或者底出现了。

前视偏差常见的场景

产生前视偏差的场景很多。一般来说,它往往产生于数据的采集和发布过程,或者产生于加工处理方法。

比如,财务数据很容易产生前视偏差。财务数据常常以季度为单位进行“索引”。很自然地,财务数据的编制和发布时间要晚于这个“索引”时间。

以2019年一季度美国的宏观经济数据发布为例。GDP预估值发布于2019年4月25日;5月30日进行第一次修正;最终修正则是在当年的6月27日。这是正常和可预料的情况。

Tip

在回测和实盘中如何避免这种前视偏差?在我们的《量化二十四课》的第22课有详细说明。

财务造假也会造成前视偏差。举一个A股中真实的例子。某海产品养殖公司,2014,2015年连续两年亏损被ST,如果2016年再亏损,按当时规则将进入退市流程。于是,2017年3月,该公司公布了2016年年报,净利润7571万,扭亏为盈,成功保壳。2019年,最终查实他们虚增利润1.3亿,追溯修订2016年财报为亏损-5543万。

如果我们现在(2024年)运行回测,由于这些数据已经得到了修正,当回测运行到2017年3月,我们取得的财报数据将是-5543万,这会帮助我们在回测中避开这个雷;但在2017年5月,实盘中获取到的该公司上年净利润为7571万。但你的模型在实盘中无法避开这种雷。

其它引起前视偏差的典型场景还有:

  • 前复权
  • 引用错误
  • 偷价
  • 统计函数如max、min、rank等,也包括zigzag类的函数

除了前视偏差,我们在回测中还可能遇到幸存者偏差、过度拟合、冲击成本、交易容量不足、回测时长不足、交易规则错误等各种偏差。这些都是只有在实战中才能获得的经验,我们在《量化二十四课》中都有介绍,可以帮你迅速提升经验值,由量化萌新成长为量化高手。

你也可以继续关注本号,我们后续会有机会深入讲解这些概念。

封面故事

封面是石溪大学的Charles & Wang中心,由美籍华人、亿万富翁、CA科技创始人王嘉廉捐助修建,是一座帮助人们了解亚洲文化和世界其它文化相互作用的建筑。

该建筑由砖块和白色半透明玻璃组成,象征着中国传统建筑中的窗楹蒙纸,此外还有一座带台阶的拱桥,让人联想到中国的寺庙。其内部有一个供学生使用的东亚美食广场,以便让人了解东亚、特别是中国的美食文化。

石溪大学与华人很有缘。杨振宁在此执教37年。丘成桐在此担任过助教。陈省身则是石溪的荣誉博士。

石溪大学出了很多名人,共拿了至少8个诺奖。量化教父James Simons则曾任该校数学系主任。

追随美的指引-纪念西蒙斯

周六早上醒来,James Simons(西蒙斯)辞世的消息刷屏了。多数人知道他,是因为他的财富和量化对冲基金公司-文艺复兴。但他更值得为人纪念的身份,则是数学家和慈善家。

西蒙斯1938年生于麻省,毕业于MIT,在UC Berkeley获得博士学位。曾任职于MIT, Harvard,后来在SBU(Stony Brook University,纽约州立大学石溪分校)担任了数学系主任。此后他跨行到了金融界,开创了传奇式的文艺复兴对冲基金。

西蒙斯作为数学家,最大的成就是他与陈省身共同提出的Chern-Simons Theory,这一理论在低维拓扑学、量子场理论和弦理论中有着广泛的应用。


下图就是据说是流体冲击情景下的Chern-Simons Form随曲面三维位置和极坐标位置变化的示意图(根据鹿鹿数模@知乎提供的代码绘制),因为觉得很美,根据西蒙斯的“指导原则”:“be guided by beauty”,就把这个图贴上来。


Tip

Be guided by beauty. 2014年在MIT举办的AMS(美国数学学会)Einstein Lecture上,西蒙斯应邀发表了名为《Mathematics, Common Sense, and Good Luck: My Life and Careers》的演讲,在演讲中,他给听众介绍了这样一些指导原则:

始终去做一些新的东西;
追随美的指引;
不要轻易放弃。

西蒙斯很早就有做生意的兴趣。在26岁时,因为背上债务,不得不委身于军方,帮助他们破译密码。这项工作对他来说十分轻松,但报酬却很高。不过,由于他毫不避讳地坚持反战,这与他上司的上司的立场截然相反,最终被解雇。

随后他进入石溪分校,担任了数学系主任。这项工作中,最重要的任务可能就是挖角。可能是因为之前一直有做生意的历练,他在这一任务中如鱼得水,囊括了大量人才,一度导致同州的康纳尔大学向州长抗议。最终,经他的努力,石溪分校数学系终于从一文不名发展到有20多位数学家、成为世界顶尖的几何学研究中心之一。这也为他后来成功创办文艺复兴打下了人才基础。

不过,伯乐也有相不中马的时候。他在石溪最大的损失,就是没能发掘出当时还在担任助教的丘成桐,也就是最近因为在华中科技大学的一个演讲,受到项立刚等等“专家”攻击的数学巨搫。不过,西蒙斯一直跟华人科学家很有缘分。下图是他与杨振宁、陈省身的合影:


75%

虽然在平凡之辈看来,西蒙斯是天纵英才,但也有他解决不了的难题。在试图证明某些有几何定义的数字(比如\(pi\),几乎在任何情况下都是无理数这一猜想时,遇到了挫折,让他变得灰心丧气甚至有点绝望,并最终将他推向了金融界。

1978年,西蒙斯成立投资公司并专职做交易。

对于他的离职,不同的人有不同的看法。他的父亲说他放弃终身教职是犯了一个错误,他更愿意别人问起儿子的工作时,能够回答说是“数学家”而不是“企业家”。大部分数学家认为以追求金钱为目的是不光彩的。他的同事沙利文认为,数学是神圣的,吉姆是认真的数学家,是可以解决最棘手问题的人。而康纳尔大学的卡莫纳则批评得更为尖锐,认为他是“把灵魂卖给了魔鬼”。


有这样一群“清高”的人作为土壤,这也许是他们基础学科得以发展的重要原因之一吧。

不过,对西蒙斯来说,亦师亦友的陈省身对他离职的看法,则更耐人寻味。陈省身说,你又不是希尔伯特式的人物,转就转呗,反正数学界也不会损失什么。

西蒙斯转型做投资也并非一翻风顺。直到1989年,他们的交易策略仍然是主观和量化相结合的。他回忆到,有时候你像个英雄一样赚钱,有时候又像个狗熊一样的赔了。后来,他们决定完全转向量化,并且放弃一切宏观指标。由此开始,文艺复兴,特别是旗下的大奖章基金,开始了自己的狂飙时代。

从1988年到2019年,西蒙斯的大奖章基金年化收益率为39%(费前收益率则是66%!)。与之对比,巴菲特1965年—2018年的年均净值增长率为21%、索罗斯旗下量子基金从1969年—2000年的年均收益是32%。


西蒙斯也在2019年,成为财富榜上排名第21位的人。

很多人很好奇大奖章为何如此厉害,它的成功密诀究竟是什么?这也许是个永远的迷,不过,我们也有一些解读这个秘诀的一些线索。

一是美国证券交易委员会(SEC)1988年批准了电子交易,这是实现高频交易的技术基础。

二是从西蒙斯挖人来看,他招了不少之前在IBM via voice项目组做语音识别的人,语音识别与股价涨跌一样,同样是一维的时间序列。因此,预测一维时间序列的波动(即趋势和反转)应该是有意义的。只不过,随着技术的发展,我们应该、也可以使用新的技术(比如神经网络)来预测一维时间序列的波动。

第三个线索则比较有意思。在国内如果我们要挖掘一家公司的秘密,有时候会去裁判文书网搜索。西蒙斯的公司也曾卷入过诉讼。在一起诉讼中,一些商业秘密不得不被批露。两名文艺复兴的前员工表示,文艺复兴公司开发了涉及互换交易的一种策略。他们在法庭文件中将其描述为“大规模骗局”,违反了SEC管理卖空的规定。彭博社曾对此作过报道,现在这篇报道还可以在这里读到。

这里我们不评价文艺复兴的做法是否真正违法。无论如何,规则有漏洞,那是规则的问题。在交易中,找到这些规则的、技术上的“漏洞”(或者交易机会),是高胜率策略的关键之一。


文艺复兴的另一个成功秘诀,可能是所谓的壁虎式交易法则:即交易要像壁虎一样,平时趴在墙上一动不动,蚊子一旦出现就迅速将其吃掉,然后恢复平静,等待下一个机会。不过,如何理解这个法则,不同的人可能会有不同的解读了。

真正指引文艺复兴成功的因素,其实西蒙斯自己介绍过。秘诀就是围绕一些非常优秀的科学家来构建公司,这些科学家是经过相应考核的,公司独特的激励机制,也使得他们能一直跟公司在一起。这个激励机制的核心就是大奖章基金,它只对公司员工开放,而且每个人的薪资主要与整体的表现、而不是个人的表现相关。

如何复制这样一个团队,这可能不仅仅是令国内的机构感到困难的地方,甚至在全球都是这样。这才是文艺复兴真正的护城河,而这种领导力,西蒙斯从在石溪担任数学系主任时就开始构建了。

“不管我在做什么,我总觉得自己像个局外人,”西蒙斯说,“我全神贯注于数学,但我从来没有觉得自己是数学系的一员。我总是有一只脚在那个世界的外面。”

然而,当西蒙斯转身到投资界后,事情刚好反过来,他的一只脚,又踏回了数学界。

1996年,他的儿子保罗车祸遇难。2003年,小儿子尼古拉斯溺水身亡。西蒙斯选择以自己的方式逃避痛苦,开始潜心琢磨那些流传已久悬而未决的数学谜题。“那是避风港,是我心中一个安静的角落”。


2007年,他与苏利文发表了《微分形式普通上同调的公理特征》这篇论文,又杀回了数学界。

文艺复兴和高频量化赚了很多钱。西蒙斯也常常会被问起,高频交易究竟有何意义?每次西蒙斯都会回答,高频交易提供了流动性,使得价格发现更加有效,并且降低了做市商利润,从而使得总体交易成本下降。很难说西蒙斯自己对这个回答有多满意。

可以说,西蒙斯前半生是在追逐利润,后半生则是在追寻人生的意义。带着对人生意义的追寻,西蒙斯开启了慈善家的生涯。


西蒙斯于2004年创立了Math for America协会,旨在促进纽约市中学数学教师的招募。这可能跟他第一次放弃教职,转投军方项目有关。

他解释说,我们通常认为,我们的老师懂数学。你会说当然了。但是很让人吃惊的是,尤其是当你上了中学的时候,你会发现大部分的数学老师数学懂得却不多。其中一种回答就是如果他们真的懂这门学科,那他们可以带着同样多的知识去 Google,Goldman Sachs或者什么其他地方。所以我们必须使这个职位变得更加吸引人,这也就是说给他们发更高的工资,这也正是我们在纽约和几个其他城市通过我们的项目正在做的,给老师们更多的尊重,并提供更多的支持。

西蒙斯还大量捐赠以支持对基础科学的研究,捐赠超过了10亿美元。他提到,如果单从对基础科学的投资规模上讲,应该还没有一个基金的规模能够与我们相比。

2010年,他签署了一项协议,将把他所有的财富,捐给慈善事业。

西蒙斯对中国也有捐赠。清华捐有一栋专家公寓楼,就是他捐赠的,以“陈赛蒙斯”命名。


在西蒙斯逝去时,我想,他更希望被人作为数学家和慈善家被纪念,而不是企业家。这也是他父亲对他的期望。他做到了。

财富扩大了西蒙斯的影响力,数学和慈善则延长了他的生命。

这是西蒙斯在AMS演讲上给出的人生指南:

  • Do something original
  • Be guided by beauty
  • Don't give up easily
  • Hope for good luck.

终极猜想!底蓓离的成因分析

这几天圈内都在传底蓓离什么的。作为严肃的量化自媒体,我们就不跟着吃这波瓜了。不过,我一直很关注技术指标的顶背离和底背离,一直在追问它的成因如何,以及如何预测。

底蓓离把我目光再次吸引到这个领域来,于是突然有了一个猜想。虽然我还没来得及完全证实它,但这个猜想,值得你锁定我的频道。

这个猜想是:RSI的日线顶背离,是因为周线还有上升空间。当周线RSI也达到前期高点时,就会触发回调。此时日线虽然创新高,但高位筹码不稳,回调增多,导致RSI下降。


背离的定义

Investopedia对技术指标的#顶背离定义为:

Quote

背离是指资产价格的走势与技术指标(例如震荡指标)相反,或者与其他数据相反。背离警告当前价格趋势可能正在减弱,并且在某些情况下可能导致价格改变方向。

存在正背离和负背离。正背离表明资产价格可能上涨。负背离表明该资产可能会下跌。

在国内,我们一般使用顶背离和底背离这两个术语。最早大V中较多讨论这两个术语的,可能是炒股养家或者缠中说禅,说明这一现象在实战中是比较受到关注的。

下图中的右图展示了一个日线的顶背离。左图则是对应的周线情况。

附图指标我使用的是RSI,它是一个非常好的震荡指标,深刻反映了当前市场的赚钱效应和强弱力量对比。

在图中,程序自动标注出来了k线的每一个峰和谷。这些标注使用了我们自己的自适应参数算法,在绝大多数时间、无论周期是哪个级别,都能工作得很好。不过这一次,在周线级别上,没能触发它标注出3月22日那一周的高点。


从右图中我们注意到,日线上存在2月27日和3月18日两个峰,后一个峰的价格高于前一峰,但后一个峰的RSI略小于前一峰的RSI(前高78.3,后高77.7),形成了顶背离。

我们在课程中详细介绍过一个独创但更准确地理论,在运用RSI时,不是低于30就会反弹,高于70就会回调,而是要看它跟前一个峰(或者谷)的RSI相比较,如果上涨时,RSI高于前一个峰对应的RSI,则有可能回调。

但这个结论也有自身的问题:一是在图中,日线在2月27日之前,就已经突破了2024年1月25日下降通道中高点RSI,为何一直到2月27日才回调?我们在课程中已经解决了这个问题。


二是为何在2月27日之后,股价还能一直上涨,直到3月18日出现顶背离?这个顶背离问题,我也一直没有思路,但自己着手写了好几个检测顶背离的例程。

猜想和验证

现在我们给出回答第二个问题的一个猜想,即尽管2月27日的日线RSI已经达到高位,但周线RSI仍在低位,它没有表示反对和阻止上涨的意思,因此日线短暂调整后,在其它因素趋动下继续上涨,直到3月22日那一周,盘中突破前高60.5,才引发了一个周线级别的大回调。

Tip

从3月21日起,到3月27日止,中证1000连续回调超过6.94%,从而引发反弹,这是另外一个故事和机会。我们在前面的一篇文章中介绍过,统计数据给出3月27日收盘后,反弹概率超过91.4%的结论,这种情况,应该坚决抄底

RSI是反转指标。它只能以一定概率给出会不会反转,但你不应该指望它给出趋势延续的预测。那是趋势类指标的任务。最重要的趋势类指标,莫过于判断均线的斜率,如果线性回归误差在可接受范围以内的话。在回归误差较大而失效的情况下,课程中也给出了一个非常鲁棒的方法。

我们还是拿数据验证一下:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 日线2月27日、3月18日顶背离,前者RSI 78.3,后者RSI 77.7
dclose = dbars["close"].astype(np.float64)
drsi = ta.RSI(dclose, 6)

for dt in (datetime.date(2024, 2, 27), 
           datetime.date(2024, 3, 18)):
    mask = dbars["frame"] == np.datetime64(dt)
    i = np.flatnonzero(mask)[0]
    print(dt, dclose[i], drsi[i])

# --- output ---
# 2024-02-27 5394.03 78.3
# 2024-03-18 5648.01 77.7

从数据看出,尽管日线价格在上涨,但RSI却下降了,构成了日线顶背离。

接着看周线:

1
2
3
4
5
6
7
8
# 周线rsi前高出现于2023年11月17日,数值为60.5
wclose = wbars["close"].astype(np.float64).copy()

nov_17 = np.datetime64(datetime.date(2023,11, 17))
i = np.flatnonzero(wbars["frame"] == nov_17)[0]

rsi = np.round(ta.RSI(wclose, 6), 1)
rsi[i]

前高为60.5。这里有一个技巧,如果没看明白,可以给我留言:

1
2
3
4
5
6
7
8
9
# 3月22日,周线rsi数值盘中突破 61.9,高于前高60.5,触发回调

mar_22 = np.datetime64(datetime.date(2024, 3, 22))
i = np.flatnonzero(wbars["frame"] == mar_22)[0]

wclose[i] = wbars["high"][i]

rsi = ta.RSI(wclose.astype(np.float64), 6)
rsi[i]

rsi后高为61.9,因此触发回调,并且这个阻力得到了确认。

Tip

注意这里rsi没有到70就回调了。所以,你看到的所有关于RSI的教科书,都该改改了。是不存在 one size fits all。不同的标的、不同的周期以及不同的阶段,应该有不同的参数。这些参数应该可以通过量化程序(或者机器学习)计算出自适应的值。

引申结论及思考

在这个市场上,存在各种不同操作频率(这是FFT和wavelet应该有用的原因)的资金。


对高频量化,他们看的是tick级数据,可能持有几分钟就会调仓;散户和量化多在日线和周线频率上操作,持有数天就调仓;长线资金以季度为单位。越是长线的资金,资金量越大,调仓时对走向的影响越强。

现在,你应该已经猜到了,有一部分资金会在日线RSI高点时撤出;大量的资金会在周线的RSI高点撤出;而更大量的资金会在月线的RSI高点撤出。

但我猜没有资金会根据季线的RSI高点撤出。许多真理,都不能线性外推。

我这么说的原因是,季线资金会按照基本面来进行操作,而不是技术面。有的同学会拿基本面因子与技术面因子揉在一起,无论是多因子回归,还是机器学习,这都是不对的。它们只会相互打架、抵消。

所以,从现在起,你应该给你的股票上个闹钟,计算出它的日线、周线和月线RSI前期高点,然后实时监控这些指标。

一旦三者都达到高点,这轮行情就结束了。如果月线达到高点,日线和周线不在高位,但发生过顶背离,那么,势必在高位产生过滞胀(量在堆积,但上涨幅度不大),此时主力的筹码很多已经被交换掉了。

此时不走很危险。接下来的回调时间,可能以月计。老胡说,“我不割肉,你怎么割我?”但胡锡进终究是等不到那一天。

机器学习(XgBoost)预测顶和底

之前的文章中,我们对中证1000指数进行了顶和底的标注。这一篇我们将利用这份标注数据,实现机器学习预测顶和底,并探讨一些机器学习的原理。

我们选取的特征非常简单--上影线和WR(William's R)的一个变种。选取这两个因子,是基于东吴证券高子剑在2020年6月的一份研报:上下影线,蜡烛好还是威廉好?

他们的结论是,根据这两类指标的变种得到的综合因子,在2009到2020年4月,以全A为样本,进行5组分层多空测试,得到年化收益为15.86%,最大回撤仅为3.68%,可以说具有非常明显的信号意义。


Info

L33 在这次实验中,我们将使用XgBoost。

它的开发者是陈天奇,上海交大ACM班06级毕业,华盛顿大学博士。现任卡内基.梅隆大学助理教授。

除了XgBoost,他还是MXNet的开发者,这个框架是一度与Tensorflow, Pytorch等齐名的四大深度学习框架之一。

题图即为卡内基.梅隆大学校园。陈天奇现在任教于此。

在上一篇文章中,我们提到机器学习总是把要解决的问题归类为两类,一类是回归,一类是分类。如果要预测的target取值处在连续实数域上,这往往是个回归问题;如果target的值域为有限个离散状态,则是一个分类问题。

然而,具体问题总是复杂许多。初学者会觉得,既然股价的取值是在连续实数域上,因此可以把它看成回归问题,使用类似LSTM之类的神经网络来预测股价。但实际上由于金融数据的噪声问题,这么做并没有什么道理。

很可能只有在构建资产定价模型时,才可以当成回归来处理,也就是,根据公司的基本面和宏观经济指标来确定公司的市值,进而推算出股价。这本质上跟预测落杉叽的房价是同样的问题。

如果我们要构建时序方向上的预测信号呢?很可能只能用我这里的方法,不去预测每一个bar的涨跌和价格,而是改为预测顶和底,最终实现买在底部,卖出在顶部。

安装XgBoost

我们一般通过conda来安装它的Python包,但pip(需要版本在21.3以上)也是可以的。

1
conda install -c conda-forge py-xgboost

在Windows上安装时,还需要额外安装VC的分发包。

如果你的机器安装有支持cuda的GPU,那么conda会自动安装带GPU支持的xgboost。

不过,GPU对xgboost的加速并没有对CNN这样的神经网络那么明显。也就是说,即使有GPU,xgboost也只会在某些阶段利用到GPU加速,总体上可能会快几倍而已。考虑到我们的标注数据本身比较小,这个加速并不重要。

数据构造

经过顶底数据标注之后,我们已经获得了一份如下格式的数据:

这份数据包括了标签(即flag一列),但没有我们要的特征工程数据。因此,我们要先从OHLC数据中提取出特征。

我们决定先从最简单的特征提取--上影线和WR(William's R)的一个变种。选取这两个因子,是基于东吴证券高子剑在2020年6月的一份研报:上下影线,蜡烛好还是威廉好?

他们的结论是,根据这两类指标的变种tr得到的综合因子,在2009到2020年4月,以全A为样本,进行5组分层多空测试,得到年化收益为15.86%,最大回撤仅为3.68%,可以说具有非常明显的信号意义。

66%

基于这个基础,我们改用机器学习的方法来做一遍。我们用来提取上下影线和WR的方法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def wr_up(bars):
    h, c, l = bars["high"], bars["low"], bars["close"]
    shadow = h - c

    # 技巧:避免产生除零错误,且不影响结果正确
    return shadow/(h - l + 1e-7)

def wr_down(bars):
    h, c, l = bars["high"], bars["low"], bars["close"]
    shadow = c - l
    return shadow/(h - l + 1e-7)

def upper_shadow(bars):
    h, c, l = bars["high"], bars["low"], bars["close"]
    o = bars["open"]
    shadow = h - np.maximum(o, c)
    return shadow/(h - l + 1e-7)

def lower_shadow(bars):
    h, c, l = bars["high"], bars["low"], bars["close"]
    o = bars["open"]
    shadow = np.minimum(o, c) - l
    return shadow/(h - l + 1e-7)

xgboost是基于树模型的,对数据的正则化本来没有要求,不过,为了便于分析和对比,我们对这四个指标都进行了归一化处理,使得数据的取值都在[0,1]之间。

如果是上下影线,值为0.5时,表明影线占了当天振幅的一半高度。如果为1,则当天收T线或者倒T(也称为墓碑线)。

William's R 是美国作家(不要脸一下,就是博主这一类型)、股市投资家拉里.威廉在1973年出版的《我如何赚得一百万》中首先发表的一个振荡类指标,它的公式是:

\[ W\%R = \frac{H_n - C_n}{H_n - L_n} x 100\% \]

计算向下支撑的公式略。

n是区间长度,一般设置为14天。这样\(H_n\)即为14天以来的最高价。其它变量依次类推。如果我们把n设置为1天,就回归成类似于上下影线的一个指标。

与K线上下影计算方法不同之处是,它只使用收盘价,而不是像上下影线那样,使用收盘价与开盘价的最大者(计算上影线时)或者最小者(计算下影线时)。

这里还有一些技巧,比如我们使用了numpy的ufunc之一, maximum来挑选开盘价和收盘价中的最大者。另一个显而易见的方法是:

1
np.select([c>o, o<c], [c, o])

但此处使用ufunc会得到加速。

接下来,我们就可以构建训练数据集了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
data = {
    "label": raw["flag"].values,
    "data": np.vstack(
        (wr_up(bars), 
         wr_down(bars), 
         upper_shadow(bars), 
         lower_shadow(bars)
        )
        ).T
}

bars是numpy structured array, 包含了OHLC数据和flag,由之前的raw变量转换过来。

最终我们生成了一个字典,训练数据存放在"data"下,标签数据存放在"label"下。使用了np.vstack来将特征合并起来。这些函数在《量化交易中的Numpy与Pandas》课程中有讲解。

接下来,我们引入sklearn的中的方法,将上述数据集切分为训练集和测试集,然后进行训练:

1
2
3
4
from sklearn.model_selection import train_test_split

X_train, X_test, y_train, y_test = 
                train_test_split(..., test_size=.2)

我们保留了20%的数据作为测试数据。

1
2
3
4
5
bst = XGBClassifier(n_estimators=3, max_depth=2, learning_rate=0.5)
# fit model
bst.fit(X_train, y_train)
# make predictions
preds = bst.predict(X_test)

现在,训练完成,并且我们在测试数据集上进行了预测。接下来,我们希望知道这个模型的有效性。为此我们要引入sklearn.metrics中的一些度量方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
from sklearn.metrics import *

acc = accuracy_score(y_test,preds)
print(f"ACC: {acc:.3f}")

recall = recall_score(y_test,preds, average='weighted')
print(f"Recall:{recall:.1%}")

f1 = f1_score(y_test,preds, average='weighted')
print(f"F1-score: {f1:.1%}")

pre = precision_score(y_test,preds, average='weighted')
print(f"Precesion:{pre:.1%}")
mx = confusion_matrix(y_test,preds)

我们得到的结果看上去很完美:

1
2
3
4
ACC: 0.930
Recall:93.0%
F1-score: 89.6%
Precesion:86.5%

但是,这些数据能表明模型真的有效吗?幸福会不会来得太容易?所以,我们还得往下深挖一层,看看实际的预测效果究竟如何。在分析大量样本预测结果时,我们有一个利器,称为困惑矩阵(confusion matrix)。


Tip

关于confusion,有这样一则笑话。在一个选美比赛上,一位美女被要求解释孔子(Confucius)的名言:"Reading without meditating is a useless occupation (学而不思则惘)"。显然这位美女不知道Confucius是谁,于是,她望文生义地猜测, Confucius was one of the men who invented confusion。不过,中庸有时候确实让人confused,猜测孔夫子发明了困惑,也是歪打正着。

我们要将矩阵mx可视化。人类,无论男人还是女人,都是视觉动物。我们无可救药地偏好各种色图。

1
2
sns.heatmap(mx/np.sum(mx), cmap="YlGnBu", 
            annot=True, fmt=".1%")

我们会得到这样一张图:

50%

这张图表明:大约有3.8%的0类数据,被错误分类为标签1;大约有3.2%的2类数据,被错误地分类为标签1;所有的1类数据,都正确地分类为1。

从这张图我们还可以知道,这是一个严重有偏的数据集。但我们最为关注的第0类(对应于flag = -1)和第2类(对应于flag = 1),它没能正确识别。当然,它也没错到把第0类识别为第2类,或者相反。

不过,无论如何,我们有了一个好的开端。随着我们往训练数据里加入更多的数据行、更多的特征,并且使得数据按类别均匀分布后,这个模型一定会有提升。

不过,在改进之前,我们还需要掌握更多关于xgboost及评估指标的理论知识。下期见!

新国九条下,低波动因子重要性提升!

Robert (Bob) Haugen, 低波动因子之父。图片来自MarketWatch


之前我们有一期文章介绍了低换手率因子。它的背后的原理是,要买在无人问津处,藏器待时,最终卖在人声鼎沸时。这是一种博弈逻辑。

今天介绍的低波动因子,同样强调无人问津处的价值。由于它的低波动特性,在实操上,不受短线资金青睐;在学术上,它与CAPM、EMH等流行的理论相悖,因而甫一提出,就被华尔街和学术界视为异类。直到2008年,MSCI才开始涉及这一因子,编制了MSCI全球低波动率指数。

大量的实证研究表明,在长达数十年的投资史中,低波动率因子都具有明显的优势:人迹罕至的道路,有时提供更好的旅程

新国九条之后,红利股在投资中的重要性将大大加强,而低波动因子在发现红利股、白马股方面有优秀的选择能力。这是我们介绍低波动因子的时间背景。

低波动因子的数据表现

从CAPM模型发表以来,华尔街和学术界坚信风险与回报是紧密相连的:市场是有效的,投资者要获得较高的收益,就必须承担更多的风险。

然而,Robert Haugen和他的老师 James Heins 教授在60~70年代就发现,与流行的理论相反,低风险股票实际上能产生更高的回报。

Tip

Robert Haugen, 金融经济学家、量化投资和低波动性投资领域先驱。伊利诺伊大学-香槟分校金融经济学博士,教授。

Haugen认为,有关投资和公司金融的重要教科书“大错特错,需要重写”。由于Haugen对低波动因子的大力推崇,从而获得了“低波动性投资之父”的非正式称号。

Tip

查理.芒格也对现代金融理论报有类似的观点。他曾经讥讽地说,一些把现代金融理论鼓吹得头头是道的诺奖经济学家,不得不把自己管理的基金破产,转而投资伯克希尔.哈撒韦。

根据S&P Global的Tim Edwards博士等人的研究,低波动性指数的风险调整回报在多个国家的表现都超过了母指数,在日本甚至超出近一倍。这个对比图我们不放了,放一个累积收益对比图。

数据来源于S&P Dow Jones Indices, 1990~2018

这个图是低波动因子标的池与市场(标普500)历史累积回报相对比的一个图。从图中可以看出,几乎在任何一个时期,低波动因子的总体收益(而不仅仅是风险调整收益)都超过了市场表现。

他们的构建方法是,从标普500中,挑出波动率最低的20%,并按与波动率成反比的方式进行加权,并且每季度进行一次调仓。

这份研究报告是教育性质的,在jieyu.ai 上提供了免费下载。

MSCI在去年三季度的一篇文章中,披露了他们构建的低波动性指数的表现情况:

图表来源:MSCI网站

上图显示出,低波动因子在市场衰退期反而能逆市上涨,其它多数时间也跑赢指数。

低波动因子的有效性解释

关于低波动性因子有效的原因,学界也是用了很多数学和数据来解释。但实际上,回到问题的本原上来,低波动性是如何造成的?是由于它的投资者长期看好公司,并且以持有吃分红为主,很少交易造成的。

投资者为什么会长期看好一支股票呢?是因为这些公司的业务逻辑很简单(能看懂,所以投资者不会反反复复)、护城河深(竞争格局很难改变)、盈利足够好(核心还是要能赚钱),是那些世界无法改变的公司(摘自但斌近期路演)。

这就有点价值投资的味道了。实际上,学界有人做了很多研究,最后发现,低波动因子与价值因子有较强的关联性:低波动性的股票、往往也是低市净率的股票。因此,白马股、长期红利股,也往往是低波动率的个股。

Tip

既然低波动因子与价值因子强关联,如果一个市场里的财务数据不那么可靠,那么我们就应该使用低波动因子来代替。量价数据永远不说谎。如果一家过去看起来很好的公司,最近出了问题,看财报的人永远是最后一个知道的。但股价会提前反映。

一个有趣的事实是,夏普率与波动率的关系。夏普率是一种风险调整收益率,它的分母--资产收益的标准差,正是波动率的线性函数。

夏普率的提出者,正是CAPM理论的创始人威廉.夏普。而低波动率投资方法的支持者们,正是用低波动率作为武器,举起了反抗以CAPM为代表的现代金融理论的旗帜。如此说来,夏普在发表CAPM模型时,也为这一理论安排了自己的掘墓人。这很辩证法。

波动率的计算

根据investopedia,波动率的公式是:

\[ vol = \sqrt{var(R)}/T \]

这里的T是产生回报率R的周期数。但也有不除以T的做法。

在金融领域,波动率常以年化方式进行呈现和比较。这可以用pandas来计算:

1
2
3
4
5
bars = ...
close = bars["close"]

close.pct_change().rolling(window_size)
     .std() * (252**0.5)

在进行单因子检验时,我们需要求得每一天的标的的波动率,因此会需要这里的rolling版本。

或者,更简单地,使用quantpian的开源库empyrical:

1
2
3
4
from empyrical import annual_volatility

daily_returns = close.pct_change()
annual_volatility

低波动率策略

一般而言,我们无须自行检验低波动率因子的有效性,可以直接使用它来进行选股。

Tip

由于低波动率因子的特性,要检验它,必须要跨越至少一个牛熊经济周期,这样引入的数据量非常大。对A股来讲,我们每天不一样,因此还很难说走过了多少个经济周期。

要注意的是,如果我们以短期的波动率进行选股,非常有可能选中下跌状态中的个股。下图显示了相同的波动率,可以出现完全不同的股价走势:

在低波动率条件下,判断股价走势非常容易,我们对价格进行回归,如果得到的直线斜率大于0,则走势是向上的。

这里的关键点是,我们首先要使用月线,至少24个周期以上。在较短的周期上,低波动率的背后没有经济学上的意义支撑。主要代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
for symbol, name, _ in get_secs():
    bars = get_bars(symbol, 24, ...)
    if len(bars) < 24:
        continue

    close = bars["close"][-24:]
    returns = close[1:]/close[:-1] - 1
    # 计算波动率
    vol = np.std(returns)

    # 计算斜率
    a, b = np.polyfit(np.arange(24), close/close[0], degree=1)
    result.append((name, symbol, vol, a))

df = pd.DataFrame(result, columns=["name", "symbol", "vol", "slope"])

df[df.slope>0].nsmallest(10, "vol")

这样我们就选出了趋势向上,波动率最小的10支。当slope接近0时,实际上还不能认为趋势向上,所以,我们可以通过分位数来进行筛选:

1
2
3
quant_25 = df[df.slope>0].slope.quantile(0.25)

df[df.slope > quant_25].nsmallest(10, "vol")

结束语

人迹罕至的道路,有时提供更好的旅程。新国九条之后,红利股在投资中的重要性将大大加强,而低波动因子在发现红利股、白马股方面有优秀的选择能力。如果一个市场里的财务数据不那么可靠,那么我们就应该使用低波动因子来代替价值因子。量价数据永远不说谎。如果一家过去看起来很好的公司,最近出了问题,看财报的人永远是最后一个知道的。但股价会提前反映。

Don't fly solo! 量化人如何使用AI工具

在投资界,巴菲特与查理.芒格的神仙友谊,是他们财富神话之外的另一段传奇。巴菲特曾这样评价芒格:他用思想的力量拓展了我的视野,让我以火箭的速度,从猩猩进化到人类。

人生何幸能得到一知己。如果没有这样的机缘,在AI时代,至少我们做量化时,可以让AI来伴飞。

这篇文章,分享我用AI的几个小故事。


在讲统计推断方法时,需要介绍分位图(Quantile-Quantile Plot)这种可视化方法人类天生就有很强的通过视觉发现pattern的能力,所以介绍这种可视化方法几乎是不可缺少的。

左偏、正态和右偏分布的QQ图示例

但当时在编这部分教材时,我对QQ-plot的机制还有一点不太清晰:为什么要对相比较的两个随机变量进行排序,再进行绘图?为什么这样绘图如果得到的是一条直线,就意味着两个随机变量强相关?难道不应该是按随机变量发生的时间顺序为序吗?

启用GPT-4的多角色数据科学家扮演

这个问题无人可请教,哪怕我搜遍全网。后来,即使我通过反复实验和推理,已经明白了其中的道理,但毕竟这个知识点似乎无人提及过,心里多少有点不确定。于是,我请教了GPT-4。


最初的几次尝试没有得到我想要的结论,于是,我用了一点技巧,要求GPT-4把自己想像成为数据科学家。并且,为了避免错误,我使用了三个数据科学家进行角色扮演,让A和B分别提出观点,再让C来进行评论,这一次,我得到了非常理想的结果,即使请教人类专家可能亦不过如此。

先给GPT-4提供问题背景:

Quote

Q-Q图的原理是,如果X是一个有序数据集,那么[X, X]在二维平面上的图像一定是一条45度角的直线。

如果我们有随机变量X和设想中的理论分布Y,如果随机变量X服从估计中的理论分布Y,那么就应该有:

在50%分位处,X的采样点\(x_1\)应该与50%分位处的\(y_1\)非常接近(由于是随机变量,所以也很难完全相等);在25%分位处,75%分位处,或者进一步推广,在任意分位数处,两者的采样都非常接近。

在实际绘图中,我们的做法是,假设X服从正态分布,共有n个数据,我们先对X进行z-score变换,然后从标准正态分布中,也随机抽样取n个数,记为Y,对两个数组都进行排序后,对[X, Y]进行绘图,如果它的图形接近于直线,则说明X服从正态分布。

从背景可以看出,我已经掌握了QQ图的大致原理。如果X与Y是同分布的,那么,将X,Y排序后,分别以\([I,X]\)\([I,Y]\)\(I\)为数组坐标)为点绘制的曲线,将会是相似曲线。


但QQ图将其进行了变换,直接取每一个坐标\(I_i\)对应的\(X_i\)\(Y_i\)作为曲线的坐标,从而将只绘制出一条曲线。如果二者同分布,则该曲线将是一条直线。当时我对这个变换的性质还不太了解。

我的问题一共有4个:

Question

  1. 这段话中是否有事实错误?
  2. 如果不对X进行z-score化,这样[X,Y]还会是直线吗?
  3. n值对绘制图形的影响是?
  4. 假设X确实服务正态分布,从标准正态分布中进行抽样得到的Y,是如何保证它与X一一对应的?

最后一问,实际上就是要问这个变换为什么能成立。

GPT-4的角色 -- 数据科学家A发现了背景中第一段的一个错误。\([X,X]\)的图像为45度直线的条件并不需要\(X\)是有序的。任何数据集的\([X,X]\)图像都会是45度直线(显然)。对于第2问,A的回答是:

Quote

If we don't standardize X (i.e., perform a z-score transformation), the Q-Q plot may not form a 45-degree line. The reason we standardize is to put X and Y on the same scale, which makes it easier to compare them.


对于第三问,数据科学家A认为:

Quote

The number of data points (n) can affect the Q-Q plot in terms of its granularity. The more points we have, the smoother and more precise the plot will be. However, even with a small n, if the data follows the theoretical distribution, the points should still fall roughly along the line of equality.

n只影响图像的平滑度和我们判断时的精度。这一点非常准确。但我最关注的是第四位,随机变量X与Y究竟是如何对应的,为什么可以将它们按大小排序?A的回答是:

Quote

Y, sampled from a standard normal distribution, does not necessarily correspond one-to-one with X in terms of specific data points. Instead, it corresponds to X in terms of the order of data points. When we sort X and Y, the smallest value of X is paired with the smallest value of Y, the second smallest with the second smallest, and so on.

答案几乎已经呼之欲出了。即使两个随机变量服从同一分布,它们的值也不会相等,但是,出现在同一位置上的随机变量值,它们的差值会很小。因此,这样绘制出来的图,就是一条45度直线。

B和C主要是对A的结论进行质疑、比较,这里不赘述了。


无论A、B还是C都没有给出最终的解释:为什么如果随机变量X和Y服从同一分步的话,那么在同一位置i处的\(X_i\)\(Y_i\)应该是接近的。但它们确实证实了我们绘制QQ图之前,先对随机变量进行排序的思路是正确的。

Info

关于这一点,应该从CDF/PPF的概念入手来理解。如果\(X\)\(Y\)是同分布的,那么在任一分位\(i\)上,随机变量的值(通过ppf,即cdf的逆函数来计算)都应该非常接近。而排序后的数组,其坐标天然就有了分位数的意义。既然\(X\)\(Y\)在任一坐标\(i\)上都应该接近,那么点\(X_i, Y_i\)就应该落在直线\(y=x\)上。这个变换的作用,是利用人眼对直线更为敏感的现象,把不易分辨的两条曲线相似度的检测,转换成一条曲线是否为直线的检测。

事实上,这一概念在英文wiki上解释的比较清楚。但我当时只看了中文的wiki。

如果上述概念还不好理解,我们可以再举一个更直观的例子。通过QQ图来判断两个证券标的是否存在强相关性。比如,我们以两支同行业个股为例,取它们最近250期日线,计算每日回报率,对其进行排序后绘图:

1
2
3
4
5
6
7
8
9
import matplotlib.pyplot as plt

r1 = hchj["close"][1:]/hchj["close"][:-1] - 1
r2 = xrhj["close"][1:]/xrhj["close"][:-1] - 1

plt.scatter(sorted(r1), sorted(r2))
x = np.linspace(np.min(r1), np.max(r1), 40)
plt.plot(x,x, '-', color='grey', markersize=1)
plt.text(np.max(r1), np.max(r1), "x=x")

我们将得到如下的分位图:

这就非常直观地显示出,两支个股的走势确实相关:在涨幅4%以下的区域,如果A下跌,那么B也下跌,并且幅度都差不多;如果A上涨,那么B也上涨;幅度也差不多。这正是相关性的含义。这里我们排除了时间,只比较了两个随机变量即日收益率。

Tip

注意看这张图中涨幅大于4%的部分。它意味着,某个标的涨幅大于4%时,另一个标的的上涨幅度没有跟上。这里可能隐藏了潜在的机会。你知道该怎么分析吗?


跟着copilot学编程

有两个版本的copilot。一个是copilot,另一个,现在被叫作github copilot,是vscode中的一个扩展。后者2022年中就发布了,当时有6个月的免费试用期。试用期内一炮而红,迅速开启了收费模式。这也直接导致了同年11月同赛道的工具软件Kite的退出。

现在github copilot每月$10,尽管物有所值,但作为不是每天都coding的人来说,感觉如果能推出按token付费的模式是最好了。

它的两个免费版本,一个是对学生免费。有edu邮箱的可以抓紧在github上申请下。另一个是如果你的开源项目超过1000赞,则有机会申请到免费版。

一般我使用copilot作为编程补充。它在错误处理方面可以做得比我更细腻,另外,在写单元测试用例时(建议每个量化人都坚持这样做),自动补齐测试数据方面是一把好手。

但是我没有想到的是,有一天它还能教我学编程,让我了解了一个从来没有听说过的Python库。

整个事情由ETF期权交割日引起。近年来似乎形成了这样一个规律,每逢期权交割日,A股的波动就特别大,而且以向下波动为主。因此,量化程序需要把这些交割日作为因子纳入交易体系。


但是这些交割日的确定,出入意料地--。它的规则是:

股指期货的交割日为每月的第三周周五;ETF期权交割日为每月第四周的周三;A50交割日为相关月的倒数第二个交易日。

在刚刚过去的4月19日,是一个股指期货交割日。接下来的4月24日,则是ETF交割日。今年的交割日如下:


随手写了几行代码,发现这个日子还是比较难算的。于是打开copilot chat,问它:

Quote

股指期货的交割日为每月的第三周周五;ETF期权交割日为每月第四周的周三;A50交割日为相关月的倒数第二个交易日。

根据上述信息,写一个方法,求出2018年到2024年12月31日止的各个交割日。

Copilot并不知道股市日历,所以无法独立完成上述任务。这并不奇怪。令人惊喜地是,它使用了calendar.monthcalendar这个方法。尽管calendar是一个标准库,但我之前并不知道它还有这个用法。

1
2
3
import calendar

calendar.monthcalendar(2024, 4)

75%


在这个基础上改就容易多了。另一个有趣的例子,是让copilot帮我想函数名。

我喜欢使用这样一个因子,最近20天内,涨幅中位数与跌幅中位数的比率。显然,这个比值越大,说明做多的力量越强。

最初我使用caculate_ratio这个函数名。但几乎所有的因子计算都可以使用这个名字。因此,我让copilot给一个准确点的名字。copilot回答说,根据函数的功能,我们可以将函数名从calculate_ratio改为calculate_downward_exceeding_upward_median_ratio。这个名字更准确地描述了函数的功能:计算下跌幅度超过上涨中位数的比例。

不过,这个名字太长了,也不优雅。于是,我问copilot,名字可以再短一点吗?可以使用metaphore吗?Coplilot回答说:

Quote

当然可以。我们可以使用"bear_bull_ratio"作为函数名。这个名字较短,且使用了金融市场的术语"bear"和"bull",分别代表市场下跌和上涨,这样可以更好地传达函数的含义。

为了机器能学习,我标注了 2 万条行情数据

芝加哥大学海德公园。芝大是经济学重镇,其学者开创了著名的芝加哥经济学派,共产生了 100 位诺奖、10 位菲尔兹奖、4 位图灵奖。今天量化人追逐的 Alpha, 最早就来自于 Michael Jessen 在芝大时的博士论文。


很多人对基于机器学习的量化策略很好奇,常常问什么时候有机器学习的课。其实,对很多人(我自己就是)来说,没有能力改进机器学习的算法和框架,机器学习都是作为黑盒子来学习,难度主要是卡在训练数据上。

这篇文章,将介绍一种数据标注方法和工具。

有监督的机器学习需要标注数据。标注数据一般是一个二维矩阵,其中一列是标签(一般记为 y),其它列是特征(一般记为 X)。训练的过程就是:

$$

fit(x) = WX -> y' \approx y

$$

训练就是通过反向传播来调整权重矩阵\(W\),使之得到的\(y'\)最接近于\(y\)

特征矩阵并不困难。它可以是因子在某个时间点上的取值。但如何标注是一个难题。它实际上反应的是,你如何理解因子与标签之间的逻辑关系:因子究竟是能预测标的未来的价格呢,还是可以预测它未来价格的走势?

应该如何标注数据

前几年有一篇比较火的论文,使用 LSTM 来预测股价。我了解到的一些人工智能与金融结合的硕士专业,还把类似的题目布置给学生练习。


作为练习题无可厚非,但也应该讲清楚,使用 LSTM 来预测股价的荒谬之处:你无法利用充满噪声的时序金融数据,从价格直接推导出下一个价格。

坊间还流传另一个方法,既然数据与标签之间不是逻辑回归的关系,那么我们把标签离散化,使之转换成为一个分类问题。比如,按第二天的涨跌,大于 3%的,归类为大幅上涨;涨跌在 1%到 3%的,归类为小幅上涨。在-1%到 1%的,归类为方向不明。

其实这种方法背后的逻辑仍然是逻辑回归。而且,为什么上涨 2.99%是小幅上涨,上涨 3%就是大幅上涨呢?有人就提出改进方法,在每个类之间加上 gap,即 [-0.5%, 0.5%] 为方向不明,[1%,3%] 为小幅上涨,而处在 [0.5%, 1%] 之间的数据就丢掉,不进行训练。这些技巧在其它领域有时候是有效的,但在量化领域,我认为它仍然不够好。因为原理不对。

我们应该回归问题的本质。要判断每一天的涨跌,其实是有难度的。但如果要判断一段趋势是否结束,则相对来讲,特征会多一点,偶然性会低一点。用数学语言来讲,我们可以把一段 k 线中的顶点标注为 1,底部标注为-1,中间的部分都标注为 0。每一个峰都会有一个谷对应,但中间的点会显著多一些,数据分类不够平衡。在训练时,要做到数据分类平衡,把标签为 0 的部分少取一点即可。


顶底数据的标注

鉴于上面的思考,我做了一个小工具,用来标注行情数据的顶和底。

这个工具要实现的任务是:

  1. 加载一段行情数据,绘制 k 线图
  2. 自动识别这段 k 线中的的顶和底,并在图上标记出来
  3. 把这些顶和底的时间提取出来,放到峰和谷两个编辑框中,供人工纠错
  4. 数据校准后,点击“记录 > 下一组"来标注下一段数据

我们使用 zigzag 库来自动寻找 k 线中的顶和底。相比 scipy.signals 包中的 argrelextrema 和 find_peaks 等方法,zigzag 库中的 peaks_valleys_pivot 方法更适合股价数据 -- 像 find_peaks 这样的方法,要求的数据质量太高了,金融数据的噪声远远超过它的期待。

peaks_valleys_pivot 会自动把首尾的部分也标记成为峰或者谷 -- 这在很多时候会是错误的 -- 因为行情还没走完,尾部的标记还没有固定下来。因此,我们需要手动移除这部分标记。此外,偶尔会发现峰谷标记太密的情况 -- 一般是由于股价波动太厉害,但如果很快得到修复,我们也可以不标记这一部分。这也需要我们手动移除。

最终,我们将行情数据的 OHLC、成交量等数据与顶底标记一起保存起来。最终,我们将得到类似下面的数据:

当然,它只能作为我们训练数据的一个底稿。我们说过,不能直接使用价格数据作为训练数据。我们必须从中提取特征。显然,像 RSI 这样的反转类指标是比较好的特征。


另外,冲高回落、均线切线斜率变化(由正转负意味着见顶,反之意味着见底)、两次冲击高点不过、k 线 pattern 中的早晨之星、黄昏之星(如果你将它们的 k 线进行 resample, 实际上它是一个冲高回落过程,或者说长上影、长下影)等等都是有一定指示性的特征。

标注工具构建方法

Tip

这里我们介绍的是 jupyter 的 ipywidgets 来构建界面的方法。此外,Plotly Dash, streamlit, H2O wave 也是主要为此目标设计的工具。

为了在 notebook 中使用界面元素,我们需要先导入相关的控件:

1
2
3
from ipywidgets import Button, HBox, VBox, Textarea, Layout,Output, Box

from IPython.display import display

在一个单元格中,如果最后的输出是一个对象,那么 notebook 将会直接显示这个对象。如果我们要在一个单元格中显示多个对象,或者,在中间的代码中要显示一些对象,就需要用到 display 这个方法。这是我们上面代码引入 display 的原因。

这里我们引入了 HBox, VBox 和 Box 三个容器类控件,Button, TextArea 这样的功能性控件。


Layout 用来指定控件的样式,比如要指定一个峰值时刻输入框的宽度和高度:

1
2
3
4
5
6
peaks_box = Textarea(
    value='',
    placeholder='请输入峰值时间,每行一个',
    description='峰值时间',
    layout=Layout(width='40%',height='100px')
)

按钮类控件一般需要指定点击时执行的动作,我们通过 on_click 方法,将点击事件和一个事件处理方法相绑定:

1
2
3
4
5
6
7
8
save_button = Button(
    description='存盘'
)
save_button.on_click(save)

def save(c):
    # SAVE DATA TO DISK
    pass
这里要注意的是,事件响应函数(比如这里的 save),在函数签名上一定要带一个参数。否则,当按钮被点击时,事件就无法传导到这个函数中来,并且不会有任何错误提示。

HBox, VBox 用来将子控件按行、列进行排列。比如:

1
2
3
4
5
# K 线图的父容器
figbox = Box(layout=Layout(width="100%"))
inputs = HBox((peaks_box, valleys_box))
buttons = HBox((backward_button, keep_button, save_button, info))
display(VBox((buttons, inputs, figbox)))

Output 控件是比较特殊的一个控件。如果我们在事件响应函数中进行了打印,这些打印是无法像其它单元格中的打印那样,直接输出在单元格下方的。我们必须定义一个 Output 控制,打印的消息将会捕获,并显示在 Output 控件的显示区域中。

1
2
3
4
5
6
7
info = Output(layout=Layout(width="40%"))

def save(c):
    global info
    # DO THE SAVE JOB
    with info:
        print("数据已保存到磁盘!")

与此类似,plotly 绘制的 k 线图,也不能直接显示。我们要通过 go.FigureWidget 来显示 k 线图。

1
2
3
4
5
import plotly.graph_objects as go

figure = ... # draw the candlestick with bars
fig = go.FigureWidget(figure)
figbox.children = (fig, )

我们特别给出这段代码,是要展示更换 k 线图的方法。在初始化时,我们就必须把 figbox 与其它控件一起安排好,但如何更新 figbox 的内容呢?

答案是,让 figbox 成为一个容器,而 go.FigureWidget 成为它的一个子控件。每次要更新 k 线图时,我们生成一个新的 fig 对象,通过figbox.children = (fig, )来替换它。


最后,谈一点 troubleshooting 的方法。所有通过 on_click 方法绑定的事件函数,即使在运行中出了错,也不会有任何提示。因此,我们需要自己捕获错误,再通过 Output 控件来显示错误堆栈:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def log(msg):
    global info

    info.clear_output()
    with info:
        if isinstance(msg, Exception):
            traceback.print_exc(msg)
        else:
            print(msg)

def on_save(b):
    try:
        # DO SOMETHING MAY CAUSE CHAOS
        raise ValueError()
    except Exception as e:
        log(e)

info = Output(layout = Layout(...))
save_button = Button()
save_button.on_click(on_save)

利用这款工具,大概花了两小时,最终我得到了2万条数据,其中顶底标签约1600个。

月亮和Pandas - Wes Mckinney的传奇故事

正如死亡和税收不可避免,Pandas对量化人而言,也具有同样的地位 -- 每个人都不可避免地要与之打交道。而Wes Mckinney正是Pandas的创建者。Pandas是有史以来,最成功的Python库之一,以一已之力,开拓了Python的生存空间。


毛姆的《月亮和六便士》讲述了这样一个故事,一个富有的英国股票经纪人,思特里克兰德,为了追求艺术和美,放弃自己的生活和家庭,来到巴黎,以艺术家的身份过着贫穷却目中无人的充足的生活,最终留下无数画作,享誉世界。

这本书表达了这样的意义,不要因终日寻找地上的六便士,而错过了头顶的月亮。金融和量化界充满着浮躁和虚荣,许多人追逐着快钱,然而easy come, easy go,最终却是一无所获。

20世纪初的金融人思特里克兰德,勘破了俗世红尘,转而潜心研究艺术,尽管后半生在物质上不如之前安逸,但能做自己喜欢的事,才是幸福

我们今天要介绍的Wes Mckinney,其经历也与思特里克兰德有异曲同工之处。不过,Wes Mckinney在经济上只有很小一段时间没有收入,随后很快就取得了事业上的成功。


Wes Mckinney, 大学毕业于MIT,后来拿到了杜克大学的数学和统计学两个博士学位。

他于2007年加入投资管理公司AQR。这是一家巨头公司,员工人数有1000人左右,管理规模达到了1400多亿美元。他们为机构客户提供另类和传统投资工具。

在AQR任职期间,大量的基础数据分析和统计工作是基于Microsoft Excel 手工完成的,这种工作极其繁琐和低效率。于是,Wes Mckinney从2008年开始,尝试开发Pandas,并选中了Python作为开发语言。

来源:pandas logo

在当时Python还不是一种很流行的数据分析和统计语言,不过Wes Mckinney很快就让它流行开来,Pandas在某种意义上,成为了“立王者”的角色 -- 当然Python最终取得今天这般地位,还要借助AI的风力,才能够青云直上。


Pandas非常成功,但带来的问题是,Wes Mckinney的业余时间已经不能满足开发的需求。于是,他像思特里克兰德一样,离开了金融行业(并且退学。似乎每一个成功的美国创业者,都必须有dropout的经历),专心一意开发Pandas。

Pandas是开源产品,最初并不能提供任何收入,Wes Mckinney靠着之前工作的丰厚收入,以及兼职来养活自己。这期间,他还出版了一本名为《Python for Data Analysis》的书,获得了一些稿费。

随着Pandas的成功,第二年,Wes Mckinney就从AQR招募了两名同事,与他一起探索Python和Pandas在金融行业的创业机会,随后, 他和Chang一起创建了DataPad公司。

现在, Pandas由超过2500名贡献者共同维护。 Wes Mckinney不再参与日常的pandas开发。但社区授予他终身仁慈独裁者(BDFL)的荣誉,拥有对Pandas未来发展的决定权,就像Guido之于Python一样。

2016年,Wes Mckinney与他人一起共同创建了Apache Arrow项目,现在Arrow是跨编程语言共享科学数据的主要格式。Arrow仍然是开源的项目,但Wes Mckinney提出一个非营利行业联盟的想法,成功地吸引到Two Sigma(就是大家熟悉的量化投资巨头)作为赞助商,此后,又吸引到NVidia, Intel, bloomberg等大公司的赞助,从而能够支持一个有六名全职开发人员的分布式团队。


从AQR开始,到主要以推广开源数据分析和数据科学工具作为职业生涯的主题,Wes Mckinney 经历了华丽的转身。他现在是Usra Labs的负责人,利用他的声誉,为Usra Labs获得资金,以促使开源和创新持续向前发展。

与坐在办公室里日复一日地数着绿纸相比,他显然更享受现在在田纳西纳什维尔的生活,因为他的目标是为这世界增添一份美好:do as much good in the world as I can。

Wes Mckinney的经历是一个传奇,他的成功也为现在想要进入金融行业的新人提供了一个范例:并不一定亲自下场做为金钱厮杀的角斗士,解决行业在软件方面的痛点,也许更容易成功。这世界并不缺少量化研究员,但Pandas和Arrow的创建者,永远只有一个。

如果你盯着月亮看,最终至少也能获得满天的清辉,它的光芒,远超六便士。

最后是一点来自 Wes Mckinney的小福利。

如果要推荐一本讲解Pandas的书,毫无疑问,没人任何书籍能比《Python for Data Analysis》更权威了。因为它是由Pandas的创建者 Wes McKinney 撰写的!这本书现在提供有网页版供开放访问。读者也可点击此链接阅读。在2023年4月进行更新后,它现在支持到了pandas 2.0版本。


75%