跳转至




2024

快速傅里叶变换与股价预测研究

一个不证自明的事实:经济活动是有周期的。但是,这个事实似乎长久以来被量化界所忽略。无论是在资产定价理论,还是在趋势交易理论中我们几乎都找不到周期研究的位置 -- 在后者语境中,大家宁可使用“摆动”这样的术语,也不愿说破“周期”这个概念。

这篇文章里,我们就来探索股市中的周期。我们将运用快速傅里叶变换把时序信号分解成频域信号,通过信号的能量来识别主力资金,并且根据它们的操作周期进行一些预测。最后,我们给出三个猜想,其中一个已经得到了证明。

FFT - 时频互转

(取数据部分略)。

我们已经取得了过去一年来的沪指。显然,它是一个时间序列信号。傅里叶变换正好可以将时间序列信号转换为频域信号。换句话说,傅里叶变换能将沪指分解成若干个正弦波的组合。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 应用傅里叶变换
fft_result = np.fft.fft(close)
freqs = np.fft.fftfreq(len(close))

# 逆傅里叶变换
filtered = fft_result.copy()
filtered[20:] = 0
inverse_fft = np.fft.ifft(filtered)

# 绘制原始信号和分解后的信号
plt.figure(figsize=(14, 7))
plt.plot(close, label='Original Close')
plt.plot(np.real(inverse_fft), label='Reconstructed from Sine Waves')
plt.legend()

我们得到的输出如下:

在数字信号处理的领域,时间序列被称为时域信号,经过傅里叶变换后,我们得到的是频域信号。时域信号与频域信号可以相互转换。Numpy 中的 fft 库提供了 fft 和 ifft 这两个函数帮我们实现这两种转换。

np.fft.fft 将时域信号变换为频域信号,转换的结果是一个复数数组,代表信号分解出的各个频率的振幅 -- 也就是能量。频率由低到高排列,其中第 0 号元素的频率为 0,是直流分量,它是信号的均值的某个线性函数。

np.ff.ifft 则是 fft 的逆变换,将频域信号变换为时域信号。

将时域信号变换到频域,就能揭示出信号的周期性等基本特征。我们也可以对 fft 变换出来的频域信号进行一些操作之后,再变换回去,这就是数字信号处理。

高频滤波和压缩

如果我们把高频信号的能量置为零,再将信号逆变换回去,我们就会得到一个与原始序列相似的新序列,但它更平滑 -- 这就是我们常常所说的低通滤波的含义 -- 你熟悉的各种移动平均也都是低通滤波器。

在上面的代码中,我们只保留了前 20 个低频信号的能量,就得到了与原始序列相似的一个新序列。如果把这种方法运用在图像领域,这就实现了有损压缩 -- 压缩比是 250/20。

在上世纪 90 年代,最领先的图像压缩算法都是基于这样的原理 -- 保留图像的中低频部分,把高频部分当成噪声去掉,这样既保留了图像的主要特征,又大大减少了要保存的数据量。

当时做此类压缩算法的人都认识这位漂亮的小姐姐 -- Lena,这张照片是图像算法的标准测试样本。在漫长的进化中,出于生存的压力,人类在识别他人表情方面进化出超强的能力。所以相对于其它样本,一旦压缩造成图像质量下降,肉眼更容易检测到人脸和表情上发生的变化,于是人脸图像就成了最好的测试样本。

Lena 小姐姐是花花公子杂志的模特,这张照片是她为花花公子 1972 年 11 月那一期拍摄的诱人照片的一小部分 -- 在原始的照片中,Lena 大胆展现了她诱人的臀部曲线,但那些不正经的科学家们只与我们分享了她的微笑 -- 从科研的角度来讲,这也是信息比率最高的部分。无独有偶,在 Lena 成为数字图像处理的标准测试样本之前,科学家们一直使用的是另一位小姐姐的照片,也出自花花公子。

好,言归正传。我们刚刚分享了一种方法,去掉信号中的高频噪声,使得信号本身的意义更加突显出来。我们也希望在证券分析中使用类似的技法,使得隐藏在 K 线中的信号显露出来。

但如果我们照搬其它领域这一方法,这几乎就不算研究,也很难获得好的结果。实际上,在证券信号中,与频率相比,我们更应该关注信号的能量,毕竟,我们要与最有力量的人站在一边。

所以,我们换一个思路,把分解后的频域信号中,能量最强的部分保留下来,看看它们长什么样。

过滤低能量信号

 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
# 保留能量最强的前 5 个信号
amp_threshold = np.sort(np.abs(fft_result))[-11]

# 绘制各个正弦波分量
plt.figure(figsize=(14, 7))

theforce = []
for freq in freqs:
    if freq == 0:  # 处理直流分量
        continue
    elif freq < 0:
        continue
    else:
        amp = np.abs(fft_result[np.where(freqs == freq)])
        if amp < amp_threshold:
            continue
        sine_wave = amp * np.sin(2 * np.pi * freq * np.arange(len(close)))
        theforce.append(sine_wave)
        plt.plot(dates, sine_wave, label=f'Frequency={freq:.2f}')

plt.legend()
plt.title('Individual Sine Wave Components')
ticks = np.arange(0, len(dates), 20)
labels_to_show = [dates[i] for i in ticks]
plt.xticks(ticks=ticks, labels=labels_to_show, rotation=45)
plt.show()

FFT 给出的频率总是一正一负,我们可以简单地认为,负的频率对我们没有意义,那是一种我们看不到、也无须关心的暗能量。所以,在代码中,我们就忽略了这一部分。

我们看到,对沪指走势影响最强的波(橙色)的周期是 7 个月左右:从峰到底要走 3 个半月,从底到峰也要走 3 个半月。由于它的能量几乎是其它波的一倍,所以它是主导整个叠加波的走势的:如果其它波与它同相,叠加的结果就会使得趋势加强;反之,则是趋势抵消。其它几个波的能量相仿,但频率不同。

这些波倒底是什么呢?它可以是一种经济周期,但是说到底,经济周期是人推动的,或者反应了人的判断。因此,我们可以把波动的周期,看成资金的操作周期

从这个分解图中,我们可以猜想,有一个长线资金(对应蓝色波段),它一年多调仓一次。有一个中线资金(对应橙色波段),它半年左右调一次仓。其它的资金则是短线资金,三个月左右就会做一次仓位变更。还有无数被我们过滤掉的高频波段,它们操作频繁,可能对应着散户,但是能量很小,一般都可以忽略;只有在极个别的时候,才能形成同方向的叠加,进而影响到走势。

现在,我们把这几路资金的操作合成起来,并与真实的走势进行对比,看看情况如何:

在大的周期上都基本吻合,也就是这些资金基本上左右了市场的走势。而且,我们还似乎可以断言,在 3 月 15 到 5 月 17 这段时间,出现了股价与主力资金的背离趋势:主力资金在撤退了,但散户还在操作,于是,尽管股价还在上涨,但最终的方向,由主力资金来决定。

Tip

黑色线是通过主力资金波段合成出来的(对未来有预测性),在市场没有发生根本性变化之前,主力的操作风格是相对固定的,因此,它可能有一定的短时预测能力。如果我们认可这个结论的话。那么就应该注意到,末端部分还存在另一个背离 -- 散户还在离场,但主力已经进场。当然,关于这一点,请千万不要太当真。

关于直流分量的解释

我过去一直以为直流分量表明资产价格的趋势,但实际上所有的波都是水平走向的 -- 但只有商品市场才是水平走向的,股票市场本质上是向上的。所以,直流分量无法表明资产价格的趋势。

直到今天我突然涌现一个想法:如果你把一个较长的时序信号分段进行 FFT 分解,这样会得到若干个直流分量。这些直流分量的回归线才是资产价格的趋势。

这里给出三个猜想:

  1. 如果分段分解后,各个频率上的能量分布没有显著变化,说明投资者的构成及操作风格也没有显著变化,我们可以用 FFT 来预测未来短期走势,直到条件不再满足为止。
  2. 沪指 30 年来直流分量应该可以比较完美地拟合成趋势线,它的斜率就等于沪指 20 年回归线的斜率。
  3. 证券价格是直流分量趋势线与一系列正弦波的组合。

下面我们来证明第二个猜想(过程略)。最终,我们将直流分量及趋势线绘制成下图:

而 2005 年以来的 A 股年线及趋势线是这样的:

不能说十分相似,只能说几乎完全一致。

趋势线拟合的 p 值是 0.055 左右,也基本满足 0.05 的置信度要求。

本文复现代码已上传到我们的课程环境,加入我们的付费投研圈子即可获得。

这篇文章是我们《因子分析与机器学习策略》中的内容,出现在如何探索新的因子方法论那一部分。对 FFT 变换得到的一些重要结果,将成为机器学习策略中用以训练的特征。更多内容,我们课堂上见!

[0825] QuanTide Weekly

本周要闻

  • 美联储主席鲍威尔表示,美联储降息时机已经到来
  • 摩根大通港股仓位近日大量转仓,涉及市值超1.1万亿港元

下周看点

  • 广发明星基金经理刘格菘的首只“三年持有基”即将到期,亏损超58%
  • 周四A50指数交割日、周五本月收官日
  • 周六发布8月官方制造PMI

本周精选

  • 如何实现Alpha 101?
  • 高效量化编程 - Mask Array and Find Runs
  • 样本测试之外,我们还有哪些过拟合检测方法?

要闻详情

  • 美联储主席鲍威尔表示,通货膨胀率仅比美联储2%的目标高出半个百分点,失业率也在上升,“政策调整的时机已经到来”。财联社
  • 摩根大通港股仓位近日大量转仓,涉及市值超1.1万亿港元。转仓后,券端持股市值排名由第4名下跌至14名,持股市值不足2000亿港元。1个月前,摩根大通亦有超6000亿元转仓。金融界
  • 广发基金明星基金经理刘格菘的首只“三年持有基”即将到期,初期募资148.70亿元,截至今年8月22日,该基金(A/C)成立以来亏损超58%。近期,三年持有期基金集中到期。回溯来看,三年持有期主动权益基金在2021年——2022年间,公募基金行业密集推出了至少73只三年持有期主动权益基金。打开封闭之后,基民会不会巨量赎回?这是下周最重要的波动因素之一。相信有关方面已经做好了准备。新浪财经
  • 8月25日,北京商报发表《外资今天对A投爱答不理,明天就让他们高攀不起》一周年。在一年前的这篇评论中,北京商报指出,在目前股票具有极高投资价值的阶段,有一些外资流出A股,可能就是他们所谓的技术派典型代表,对指数患得患失,但他们最终一定会后悔,等想再回来的时候,势必要支付更高的价格,正所谓今天对A股爱答不理,明天就让他们高攀不起。该评论发布次日,沪指开盘于3219点。一年之后,沪指收盘于2854点。

如何实现Alpha 101?

2015 年,World Quant 发布了报告 《101 Formulaic Alphas》,它包含了 101 种不同的股票选择因子,这些因子中,有 80%是当时正在 World Quant 交易中使用的因子。该报告发表之后,在产业界引起较大反响。

目前,根据 Alpha101 生成的因子库,已几乎成为各量化平台、数据提供商和量化机构的必备。此外,一些机构受此启发,还在此基础上构建了更多因子,比如国泰君安推出的 Alpha 191 等。这两个因子库都有机构进行了实现。比如 DolphinDB聚宽 都提供了这两个因子库。

这篇文章就来介绍如何读懂 《101 Formulaic Alphas》 并且实现它。文章内容摘自我们的课程《因子分析与机器学习策略》的第8课,篇幅所限,有删节。

Alpha 101 因子中的数据和算子

在实现 Alpha101 因子之前,我们首先要理解其公式中使用的数据和基础算子。

Alpha101 因子主要是基于价格和成交量构建,只有少部分 Alpha 中使用了基本面数据,包括市值数据和行业分类数据 [^fundmental_data]。

Tip

在 A 股市场,由于财报数据的可信度问题 [^fraut],由于缺乏 T+0 和卖空等交易机制,短期内由交易行为产生的价格失效现象非常常见。因此,短期量价因子在现阶段的有效性高于基本面因子。


在价量数据中,Alpha101 依赖的最原始数据是 OHLC, volume(成交额), amount(成交量),turnover(换手率),并在此基础上,计算出来 returns(每日涨跌幅)和 vwap(加权平均成交价格)。

returns 和 vwap 的计算方法如下:

1
2
3
4
5
6
7
# THE NUMPY WAY
vwap = bars["amount"] / bars["volume"] / 100
returns = bars["close"][1:]/bars["close"][:-1] - 1

# THE PANDAS WAY
vwap = df.amount / df.volume / 100
returns = df.close.pct_change()

除此之外,要理解 Alpha101,重要的是理解它的公用算子。在 Alpha101 中,总共有约 30 个左右的算子,其中有一些像 abs, log, sign, min, max 以及数学运算符(+, -, *, /)等都是无须解释的。

下面,我们就先逐一解释需要说明的算子。

三目运算符

三目运算符是 Python 中没有,但存在于 C 编程语言的一个算子。这个运算符可以表示为:"x ? y : z",它相当于 Python 中的:

1
2
3
4
5
6
expr_result = None

if x:
    expr_result = y
else:
    expr_result = z

rank

在 Alpha101 中,存在两个 rank,一个是横截面上的,即对同一时间点上 universe 中所有的股票进行排序;另一个是时间序列上的,即对同一股票在时间序列上的排序。


在横截面上的 rank 直接调用 DataFrame 的 rank。比如,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import pandas as pd

data = {
    'asset': ["000001", "000002", "000004", "000005", "000006"],
    'factor': [85, 92, 78, 92, 88],
    'date': [0] * 5
}
df = pd.DataFrame(data).set_index('date').pivot(index=None, columns="asset", values="factor")

def rank(df):
    return df.rank(axis=1, pct=True, method='min')

在上面这段代码中,date 为索引,列名字为各 asset,factor 为其值,此时,我们就可以通 rank(axis=1) 的方法,对各 asset 的 factor 值在截面上进行排序。当我们使用 axis=1 参数时,索引是不参与排序。pct 为 True 表示返回的是百分比排名,False 表示返回的是排名。

有时候我们也需要在时间序列上进行排序,在 Alpha101 中,这种排序被定义为 ts_rank,通过前缀 ts_来与截面上的 rank 相区分。此后,当我们看到 ts_前缀时,也应该作同样理解。

1
2
3
4
from bottleneck import move_rank

def ts_rank(df, window=10, min_count=1):
    return move_rank(df, window, axis=0, min_count=min_count)

在这里我们使用的是 bottleneck 中的 move_rank,它的速度要显著高于 pandas 和 scipy 中的同类实现。如果使用 pandas 来实现,代码如下:

1
2
3
4
5
def rolling_rank(na):
    return rankdata(na,method='min')[-1]

def ts_rank(df, window=10):
    return df.rolling(window).apply(rolling_rank)

注意第 3 行中的 [-1] 是必须的。


rank 和 ts_rank 的使用在 alpha004 因子中的应用最为典型。这个因子是:

1
2
3
# ALPHA#4    (-1 * TS_RANK(RANK(LOW), 9))
def alpha004(low):
    return -1 * ts_rank(rank(low), 9)

在这里,参数 low 是一个以 asset 为列、日期为索引,当日最低价为值的 dataframe,是一个宽表。下面,我们看一下对参数 low 依次调用 rank 和 ts_rank 的结果。通过深入几个示例之后,我们就很快能够明白 Alpha101 的因子计算过程。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from bottleneck import move_rank
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

df = pd.DataFrame(
       [[6.18, 19.36, 33.13, 14.23,  6.8 ,  6.34],
       [6.55, 20.36, 32.69, 14.52,  6.4,  6.44 ],
       [7.  , 20.79, 32.51, 14.56,  6.0 ,  6.54],
       [7.06, 21.35, 33.13, 14.47,  6.5,  6.64],
       [7.03, 21.56, 33.6 , 14.6 ,  6.5,  6.44]], 
       columns=['000001', '000002', '000063', '000066', '000069', '000100'], 
       index=['2022-01-04', '2022-01-05', '2022-01-06', '2022-01-07', '2022-01-10'])

def rank(df):
    return df.rank(axis=1, pct=True, method='min')

def ts_rank(df, window=10, min_count=1):
    return move_rank(df, window, axis=0, min_count=min_count)

df
rank(df)

-1 * ts_rank(rank(df), 3)

示例 将依次输出三个 DataFrame。我们看到,rank 是执行在行上,它将各股票按最低价进行排序;ts_rank 执行在列上,对各股票在横截面上的排序再进行排序,反应出最低位置的变化。

比如,000100 这支股票,在 2022 年 1 月 4 日,横截面的排位在 33%分位,到了 1 月 10 日,它在横截面上的排位下降到 16.7%。


通过 ts_rank 之后,最终它在 1 月 10 日的因子值为 1,反应了它在横截面上排位下降的事实。同理,000001 这支股票,在 1 月 4 日,它的横截面上的排位是 16.7%(最低),而在 1 月 5 日,它的排序上升到 50%,最终它在当日的因子值为-1,反应了它在横截面排序上升的事实。

Tip

通过 Alpha004 因子,我们不仅了解到 rank 与 ts_rank 的用法,也知道了横截面算子与时序算子的区别。此外,我们也了解到,为了方便计算 alpha101 因子,最佳的数据组织方式可能是将基础数据(比如 OHLC)都组织成一个个以日期为索引、asset 为列的宽表,以方便在两个方向上(横截面和时序)的计算。

ts_*

这一组算子中,除了之前已经介绍过的 ts_rank 之外,还有 ts_max, ts_argmax, ts_argmin, ts_min。这一些算子都有两个参数,首先时时间序列,比如 close 或者 open,然后是滑动窗口的长度。

注意这一类算子一定是在滑动窗口上进行的,只有这样,才不会引入未来数据。

除此之外,其它常用统计函数,比如 min, max, sum, product, stddev 等,尽管没有使用 ts_前缀,它们也是时序算子,而不是截面算子。考虑到我们已经通过 ts_rank 详细介绍了时序算子的用法,而这些算子的作用大家也都非常熟悉,这里就从略。

delay

在 Alpha101 中,delay 算子用来获取 n 天前的数据。比如,


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def delay(df, n):
    return df.shift(n)

data = {
    'date': pd.date_range(start='2023-01-01', periods=10),
    'close': [100, 101, 102, 103, 104, 105, 106, 107, 108, 109]
}
df = pd.DataFrame(data)

delay(df, 5)

如此一来,我们在计算第 5 天的因子时,使用的 close 数据就是 5 天前的,即原来索引为 0 处的 close。

correlation 和 covariance

correlation 就是两个时间序列在滑动窗口上的皮尔逊相关系数,这个算子可以实现为:

1
2
3
4
5
def correlation(x, y, window=10):
    return x.rolling(window).corr(y).fillna(0).replace([np.inf, -np.inf], 0)

def covariance(x, y, window=10):
    return x.rolling(window).cov(y)

注意在这里,尽管我们只对 x 调用了 rolling,但在计算相关系数时,经验证,y 也是按同样的窗口进行滑动的。

scale

按照 Alpha101 的解释,这个算子的作用,是将数组的元素进行缩放,使之满足 sum(abs(x)) = a,缺省情况下 a = 1。它可以实现为:

1
2
def scale(df, k=1):
    return df.mul(k).div(np.abs(df).sum())

decay_linear

这个算子的作用是将长度为 d 的时间序列中的元素进行线性加权衰减,使之总和为 1,且越往后的元素权重越大。

1
2
3
4
def decay_linear(df, period=10):
    weights = np.array(range(1, period+1))
    sum_weights = np.sum(weights)
    return df.rolling(period).apply(lambda x: np.sum(weights*x) / sum_weights)

delta

相当于 dataframe.diff()。

adv{d}

成交量的 d 天简单移动平均。

signedpower

signedpower(x, a) 相当于 x^a

Alpha 101 因子解读

此部分略

开源的Alpha101因子分析库

完整探索Alpha101中的定义的因子的最佳方案是,根据历史数据,计算出所有这些因子,并且通过Alphalens甚至backtrader对它们进行回测。popbo就实现了这样的功能。


运行该程序库需要安装alphalens, akshare,baostock以及jupyternotebook。在进行研究之前,需要先参照其README文件进行数据下载和因子计算。然后就可以打开research.ipynb,对每个因子的历年表现进行分析。

在我们的补充材料中,提供了该项目的全部源码并且可以在我们的课程环境中运行。


高效量化编程 - Mask Array and Find Runs

在很多量化场景下,我们都需要统计某个事件连续发生了多少次,比如,连续涨跌停、N连阳、计算Connor's RSI中的streaks等等。

比如,要判断下列收盘价中,最大的连续涨停次数是多少?最长的N连涨数是多少?应该如何计算呢?

1
2
a = [15.28, 16.81, 18.49, 20.34, 21.2, 20.5, 22.37, 24.61, 27.07, 29.78, 
     32.76, 36.04]

假设我们以10%的涨幅为限,则可以将上述数组转换为:

1
2
pct = np.diff(a) / a[:-1]
pct > 0.1

我们将得到以下数组:

1
flags = [True, False, True, False, False, False, True, False, True, True, True]

这仍然不能计算出最大连续涨停次数,但它是很多此类问题的一个基本数据结构,我们将原始的数据按条件转换成类似的数组之后,就可以使用下面的神器了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from numpy.typing import ArrayLike
from typing import Tuple
import numpy as np

def find_runs(x: ArrayLike) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
    """Find runs of consecutive items in an array.
    """

    # ensure array
    x = np.asanyarray(x)
    if x.ndim != 1:
        raise ValueError("only 1D array supported")
    n = x.shape[0]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    # handle empty array
    if n == 0:
        return np.array([]), np.array([]), np.array([])

    else:
        # find run starts
        loc_run_start = np.empty(n, dtype=bool)
        loc_run_start[0] = True
        np.not_equal(x[:-1], x[1:], out=loc_run_start[1:])
        run_starts = np.nonzero(loc_run_start)[0]

        # find run values
        run_values = x[loc_run_start]

        # find run lengths
        run_lengths = np.diff(np.append(run_starts, n))

        return run_values, run_starts, run_lengths


pct = np.diff(a) / a[:-1]
v,s,l = find_runs(pct > 0.099)
(v, s, l)

输出结果为:

1
(array([ True, False,  True]), array([0, 3, 6]), array([3, 3, 5]))

输出结果是一个由三个数组组成的元组,分别表示:

value: unique values start: start indices length: length of runs 在上面的输出中,v[0]为True,表示这是一系列涨停的开始,s[0]则是对应的起始位置,此时索引为0; l[0]则表示该连续的涨停次数为3次。同样,我们可以知道,原始数组中,最长连续涨停(v[2])次数为5(l[2]),从索引6(s[2])开始起。

所以,要找出原始序列中的最大连续涨停次数,只需要找到l中的最大值即可。但要解决这个问题依然有一点技巧,我们需要使用第4章中介绍的 mask array。

1
2
3
v_ma = np.ma.array(v, mask = ~v)
pos = np.argmax(v_ma * l)
print(f"最大连续涨停次数{l[pos]},从索引{s[pos]}:{a[s[pos]]}开始。")

在这里,mask array的作用是,既不让 v == False 的数据参与计算(后面的 v_ma * l),又保留这些元素的次序(索引)不变,以便后面我们调用 argmax 函数时,找到的索引跟v, s, l中的对应位置是一致的。

我们创建的v_ma是一个mask array,它的值为:

1
2
3
masked_array(data=[True, --, True],
            mask=[False,  True, False],
            fill_value=True)
当它与另一个整数数组相乘时,True就转化为数字1,这样相乘的结果也仍然是一个mask array:

1
2
3
masked_array(data=[3, --, 5],
             mask=[False,  True, False],
            fill_value=True)

当arg_max作用在mask array时,它会忽略掉mask为True的元素,但保留它们的位置,因此,最终pos的结果为2,对应的 v,s,l中的元素值分别为: True, 6, 5。

如果要统计最长N连涨呢?这是一个比寻找涨停更容易的任务。不过,这一次,我们将不使用mask array来实现:

1
2
3
4
v,s,l = find_runs(np.diff(a) > 0)

pos = np.argmax(v * l)
print(f"最长N连涨次数{l[pos]},从索引{s[pos]}:{a[s[pos]]}开始。")

输出结果是:最长N连涨次数6,从索引5:20.5开始。

这里的关键是,当Numpy执行乘法时,True会被当成数字1,而False会被当成数字0,于是,乘法结果自然消除了没有连续上涨的部分,从而不干扰argmax的计算。

当然,使用mask array可能在语义上更清楚一些,尽管mask array的速度会慢一点,但正确和易懂常常更重要。


计算 Connor's RSI中的streaks Connor's RSI(Connor's Relative Strength Index)是一种技术分析指标,它是由Nirvana Systems开发的一种改进版的相对强弱指数(RSI)。

Connor's RSI与传统RSI的主要区别在于它考虑了价格连续上涨或下跌的天数,也就是所谓的“连胜”(winning streaks)和“连败”(losing streaks)。这种考虑使得Connor's RSI能够更好地反映市场趋势的强度。

在前面介绍了find_runs函数之后,计算streaks就变得非常简单了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def streaks(close):
    result = []
    conds = [close[1:]>close[:-1], close[1:]<close[:-1]]

    flags = np.select(conds, [1, -1], 0)
    v, _, l = find_runs(flags)
    for i in range(len(v)):
        if v[i] == 0:
            result.extend([0] * l[i])
        else:
            result.extend([v[i] * x for x in range(1, (l[i] + 1))])

    return np.insert(result, 0, 0)

这段代码首先将股价序列划分为上涨、下跌和平盘三个子系列,然后对每个子系列计算连续上涨或下跌的天数,并将结果合并成一个新的数组。

在streaks中,连续上涨天数要用正数表示,连续下跌天数用负数表示,所以在第5行中,通过np.select将条件数组转换为[1, 0, -1]的序列,后面使用乘法就能得到正确的连续上涨(下跌)天数了。


样本测试之外,我们还有哪些过拟合检测方法?

在知乎上看到一个搞笑的贴子,说是有人为了卖策略,让回测结果好看,会在代码中植入大量的if 语句,判断当前时间是特定的日期,就不进行交易。但奥妙全在这些日期里,因为在这些日期时,交易全是亏损的。

内容的真实性值得怀疑。不过,这却是一个典型的过拟合例子。

过拟合和检测方法

过拟合是指模型与数据拟合得很好,以至于该模型不可泛化,从而不能在另一个数据集上工作。从交易角度来说,过拟合“设计”了一种策略,可以很好地交易历史数据,但在新数据上肯定会失败。

过拟合是我们在回测中的头号敌人。如何检测过拟合呢?

一个显而易见的检测方法是样本外测试。它是把整个数据集划分为互不重叠的训练集和测试集,在训练集上训练模型,在测试集上进行验证。如果模型在测试集上也表现良好,就认为该模型没有拟合。

在样本本身就不足的情况下,样本外测试就变得困难。于是,人们发明了一些拓展版本。

其中一种拓展版本是 k-fold cross-validation,这是在机器学习中常见的概念。

它是将数据集随机分成 K 个大小大致相等的子集,对于每一轮验证,选择一个子集作为验证集,其余 K-1 个子集作为训练集。模型在训练集上训练,在验证集上进行评估。这个过程重复 K 次,最终评估指标通常为 K 次验证结果的平均值。


这个过程可以简单地用下图表示:

k-fold cross validation,by sklearn

但在时间序列分析(证券分析是其中典型的一种)中,k-fold方法是不适合的,因为时间序列分析有严格的顺序性。因此,从k-fold cross-validation特化出来一个版本,称为 rolling forecasting。你可以把它看成顺序版本的k-fold cross-validation。

它可以简单地用下图表示:

rolling forecasting, by tsfresh


从k-fold cross-validation到rolling forecasting的两张图可以看出,它们的区别在于一个是无序的,另一个则强调时间顺序,训练集和验证集之间必须是连续的。

有时候,你也会看到 Walk-Forward Optimization这种说法。它与rolling forecasting没有本质区别。

不过,我最近从buildalpha网站上,了解到了一种新颖的方法,这就是噪声测试。

新尝试:噪声测试

buildalpha的噪声测试,是将一定比率的随机噪声叠加到回测数据上,然后再进行回测,并将基于噪声的回测与基于真实数据的回测进行比较。

L50

它的原理是,在我们进行回测时,历史数据只是可能发生的一种可能路径。如果时间重演,历史可能不会改变总的方向,但是偶然性会改变历史的步伐。而一个好的策略,应该是能对抗偶然性、把握历史总的方向的策略。因此,在一个时间序列加上一些巧妙的噪声,就可能会让过拟合的策略失效,而真正有效的策略仍然闪耀。

buildalpha是一个类似tradingview的平台。要进行噪声测试,可以通过图形界面进行配置。

通过这个对话框,buildalpha修改了20%左右的数据,并且对OHLC的修改幅度都控制在用ATR的20%以内。最下面的100表明我们将随机生成100组带噪声的数据。


我们对比下真实数据与叠加噪声的数据。

左图为真实数据,右图为叠加部分噪声的数据。叠加噪声后,在一些细节上,引入了随机性,但并没有改变股价走势(叠加是独立的)。如果股价走势被改变,那么这种方法就是无效的甚至有害的。

最后,在同一个策略上,对照回测的结果是:

75%

从结果上看,在历史的多条可能路径中,没有任何一条的回测结果能比真实数据好。


换句话说,真实回测的结果之所以这么好,纯粹是因为制定策略的人,是带着上帝视角,从未来穿越回去的。

参数平原与噪声测试

噪声测试是稍稍修改历史数据再进行圆滑。而参数平原则是另一种检测过拟合的方法,它是指稍微修改策略参数,看回测表现是否会发生剧烈的改变。如果没有发生剧烈的改变,那么策略参数就是鲁棒的。

Build Alpha以可视化的方式,提供了参数平原检测。

在这个3D图中,参数选择为 X= 9和Y=4,如黑色简单所示。显然,这一区域靠近敏感区域,在其周围,策略的性能下降非常厉害。按照传统的推荐,我们应该选择参数 X=8和Y=8,这一区域图形更为平坦。

在很多时候,参数平原的提示是对的 -- 因为我们选择的参数,其实价格变化的函数;但它毕竟不是价格变化。最直接的方法是,当价格发生轻微变化时,策略的性能如果仍然处在一个平坦的表面,就更能说明策略是鲁棒的。

不过,这种图很难绘制,所以,Build Alpha绘制的仍然是以参数为n维空间的坐标、策略性能为其取值的三维图,但它不再是基于单个历史数据,而是基于一组历史数据:真实历史数据和增加了噪声的数据。

高效量化编程: Mask Array应用和find_runs

在很多量化场景下,我们都需要统计某个事件连续发生了多少次,比如,连续涨跌停、N连阳、计算Connor's RSI中的streaks等等。

比如,要判断下列收盘价中,最大的连续涨停次数是多少?最长的N连涨数是多少?应该如何计算呢?

1
2
a = [15.28, 16.81, 18.49, 20.34, 21.2, 20.5, 22.37, 24.61, 27.07, 29.78, 
     32.76, 36.04]

假设我们以10%的涨幅为限,则可以将上述数组转换为:

1
2
pct = np.diff(a) / a[:-1]
pct > 0.1

我们将得到以下数组:

1
flags = [True, False, True, False, False, False, True, False, True, True, True]

这仍然不能计算出最大连续涨停次数,但它是很多此类问题的一个基本数据结构,我们将原始的数据按条件转换成类似的数组之后,就可以使用下面的神器了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
from numpy.typing import ArrayLike
from typing import Tuple
import numpy as np

def find_runs(x: ArrayLike) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
    """Find runs of consecutive items in an array.

    Args:
        x: the sequence to find runs in

    Returns:
        A tuple of unique values, start indices, and length of runs
    """

    # ensure array
    x = np.asanyarray(x)
    if x.ndim != 1:
        raise ValueError("only 1D array supported")
    n = x.shape[0]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    # handle empty array
    if n == 0:
        return np.array([]), np.array([]), np.array([])

    else:
        # find run starts
        loc_run_start = np.empty(n, dtype=bool)
        loc_run_start[0] = True
        np.not_equal(x[:-1], x[1:], out=loc_run_start[1:])
        run_starts = np.nonzero(loc_run_start)[0]

        # find run values
        run_values = x[loc_run_start]

        # find run lengths
        run_lengths = np.diff(np.append(run_starts, n))

        return run_values, run_starts, run_lengths


pct = np.diff(a) / a[:-1]
v,s,l = find_runs(pct > 0.099)
(v, s, l)

输出结果为:

1
(array([ True, False,  True]), array([0, 3, 6]), array([3, 3, 5]))

输出结果是一个由三个数组组成的元组,分别表示:

value: unique values start: start indices length: length of runs 在上面的输出中,v[0]为True,表示这是一系列涨停的开始,s[0]则是对应的起始位置,此时索引为0; l[0]则表示该连续的涨停次数为3次。同样,我们可以知道,原始数组中,最长连续涨停(v[2])次数为5(l[2]),从索引6(s[2])开始起。

所以,要找出原始序列中的最大连续涨停次数,只需要找到l中的最大值即可。但要解决这个问题依然有一点技巧,我们需要使用第4章中介绍的 mask array。

1
2
3
v_ma = np.ma.array(v, mask = ~v)
pos = np.argmax(v_ma * l)
print(f"最大连续涨停次数{l[pos]},从索引{s[pos]}:{a[s[pos]]}开始。")

在这里,mask array的作用是,既不让 v == False 的数据参与计算(后面的 v_ma * l),又保留这些元素的次序(索引)不变,以便后面我们调用 argmax 函数时,找到的索引跟v, s, l中的对应位置是一致的。

我们创建的v_ma是一个mask array,它的值为:

1
2
3
masked_array(data=[True, --, True],
            mask=[False,  True, False],
            fill_value=True)
当它与另一个整数数组相乘时,True就转化为数字1,这样相乘的结果也仍然是一个mask array:

1
2
3
masked_array(data=[3, --, 5],
             mask=[False,  True, False],
            fill_value=True)

当arg_max作用在mask array时,它会忽略掉mask为True的元素,但保留它们的位置,因此,最终pos的结果为2,对应的 v,s,l中的元素值分别为: True, 6, 5。

如果要统计最长N连涨呢?这是一个比寻找涨停更容易的任务。不过,这一次,我们将不使用mask array来实现:

1
2
3
4
v,s,l = find_runs(np.diff(a) > 0)

pos = np.argmax(v * l)
print(f"最长N连涨次数{l[pos]},从索引{s[pos]}:{a[s[pos]]}开始。")

输出结果是:最长N连涨次数6,从索引5:20.5开始。

这里的关键是,当Numpy执行乘法时,True会被当成数字1,而False会被当成数字0,于是,乘法结果自然消除了没有连续上涨的部分,从而不干扰argmax的计算。

当然,使用mask array可能在语义上更清楚一些,尽管mask array的速度会慢一点,但正确和易懂常常更重要。


计算 Connor's RSI中的streaks Connor's RSI(Connor's Relative Strength Index)是一种技术分析指标,它是由Nirvana Systems开发的一种改进版的相对强弱指数(RSI)。

Connor's RSI与传统RSI的主要区别在于它考虑了价格连续上涨或下跌的天数,也就是所谓的“连胜”(winning streaks)和“连败”(losing streaks)。这种考虑使得Connor's RSI能够更好地反映市场趋势的强度。

在前面介绍了find_runs函数之后,计算streaks就变得非常简单了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def streaks(close):
    result = []
    conds = [close[1:]>close[:-1], close[1:]<close[:-1]]

    flags = np.select(conds, [1, -1], 0)
    v, _, l = find_runs(flags)
    for i in range(len(v)):
        if v[i] == 0:
            result.extend([0] * l[i])
        else:
            result.extend([v[i] * x for x in range(1, (l[i] + 1))])

    return np.insert(result, 0, 0)

这段代码首先将股价序列划分为上涨、下跌和平盘三个子系列,然后对每个子系列计算连续上涨或下跌的天数,并将结果合并成一个新的数组。

在streaks中,连续上涨天数要用正数表示,连续下跌天数用负数表示,所以在第5行中,通过np.select将条件数组转换为[1, 0, -1]的序列,后面使用乘法就能得到正确的连续上涨(下跌)天数了。

高效量化编程: Pandas 的多级索引

题图: 普林斯顿大学。普林斯顿大学在量化金融领域有着非常强的研究实力,并且拥有一些著名的学者,比如马克·布伦纳迈尔,范剑青教授(华裔统计学家,普林斯顿大学金融教授,复旦大学大数据学院院长)等。

Pandas 的多级索引(也称为分层索引或 MultiIndex)是一种强大的特性。当我们进行因子分析、组合管理时,常常会遇到多级索引,甚至是不可或缺。比如,Alphalens在进行因子分析时,要求的输入数据格式就是由date和asset索引的。同样的数据结构,也会用在回测中。比如,如果我们回测中的unverse是由多个asset组成,要给策略传递行情数据,我们可以通过一个字典传递,也可以通过这里提到的多级索引的DataFrame传递。

在这篇文章里,我们将介绍多级索引的增删改查操作。

创建一个有多级索引的DataFrame

让我们先从一个最普通的行情数据集开始。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import pandas as pd

dates = pd.date_range('2023-01-01', '2023-01-05').repeat(2)
df = pd.DataFrame(
    {
        "date": dates,
        "asset": ["000001", "000002"] * 5,
        "close": (1 + np.random.normal(0, scale=0.03,size=10)).cumprod() * 10,
        "open": (1 + np.random.normal(0, scale=0.03,size=10)).cumprod() * 10,
        "high": (1 + np.random.normal(0, scale=0.03,size=10)).cumprod() * 10,
        "low": (1 + np.random.normal(0, scale=0.03,size=10)).cumprod() * 10
    }
)
df.tail()

生成的数据集如下:

我们可以通过set_index方法来将索引设置为date:

1
2
df1 = df.set_index('date')
df1

这样,我们就得到了一个只有date索引的DataFrame。

如果我们在调用set_index时,指定一个数组,就会得到一个多级索引:

1
df.set_index(['date', 'asset'])

这样就生成了一个有两级索引的DataFrame。

set_index语法非常灵活,可以用来设置全新的索引(之前的索引被删除),也可以增加列作为索引:

1
df1.set_index('asset', append=True)

这样得到的结果会跟上图完全一样。但如果你觉得索引的顺序不对,比如,我们希望asset排在date前面,可以这样操作:

1
2
df2 = df1.set_index('asset', append=True)
df2.swaplevel(0,1)

我们通过swaplevel方法交换了索引的顺序。但如果我们的索引字段不止两个字段,那么, 我们就要使用reorder_levels()这个方法了。

重命名索引

当数据在不同的Python库之间传递时,往往就需要改变数据格式(列名、索引等),以适配不同的库。如果需要重命名索引,我们可以使用以下几种方法之一:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

df2.rename_axis(index={"date":"new_date"}) # 使用列表,可部分传参
df2.rename_axis(["asset", "date"]) # 使用数组,一一对应

_ = df.index.rename('ord', inplace=True) # 单索引
df

_ = df2.index.rename(["new_date", "new_asset"], inplace=True)
df2

与之区别的是,我们常常使用df.rename来给列重命名,而这个方法也有给索引重命名的选项,但是,含义却大不相同:

1
df.rename(index={"date": "rename_date"}) # 不生效

没有任何事情发生。这究竟是怎么一回事?为什么它没能将index重新命名呢? 实际上它涉及到DataFrame的一个深层机制, Axes和axis。

axes, axis

你可能很少接触到这个概念,但是,你可以自己验证一下:

1
2
df = pd.DataFrame([(0, 1, 2), (2, 3, 4)], columns=list("ABC"))
df.axes

我们会看到如下输出:

1
2
3
4
[
    RangeIndex(start=0, stop=2, step=1), 
    Index(['A', 'B', 'C'], dtype='object')
]

这两个元素都称为Axis,其中第一个是行索引,我们在调用 Pandas函数时,可能会用 axis = 0,或者axis = 'index'来引用它;第二个是列索引,我们在调用Pandas函数时,可能会用axis = 1,或者axis = 'columns'来引用它。

到目前为止,这两个索引都只有一级(level=0),并且都没有名字。当我们说列A,列B并且给列改名字时,我们实际上是在改axis=1中的某些元素的值。

现在,我们应该可以理解了,当我们调用df.rename({"date": "rename_date"})时,它作用的对象并不是axis = 0本身,而是要作用于axis=0中的元素。然而,在index中并不存在"date"这个元素(df中的索引都是日期类型),因此,这个重命名就不起作用。

现在,我们明白了,为什么给索引改名字,可以使用df.index.rename。同样地,我们就想到了,可以用df.columns.rename来改列名。

1
2
3
4
df = pd.DataFrame([(0, 1, 2), (2, 3, 4)], columns=list("ABC"))
df.columns.rename("Fantastic Columns", inplace=True)
df.index.rename("Fantastic Rows", inplace=True)
df

这样显示出来的DataFrame,会在左上角多出行索引和列索引的名字。

同样地,我们也可以猜到,既然行存在多级索引,那么列也应该有多级索引。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import pandas as pd
import numpy as np

# 创建多级列索引
columns = pd.MultiIndex.from_tuples([
    ('stock', 'price'),
    ('stock', 'volume'),
    ('bond', 'price'),
    ('bond', 'volume')
])


data = np.random.rand(5, 4) 
df = pd.DataFrame(data, columns=columns)

df

左上角一片空白,因此,这个DataFrame的行索引和列索引都还没有命名(乍一看挺反直觉的,难道列索引不是stock, bond吗)!

总之,如果我们要给行索引或者列索引命名,请使用"正规"方法,即rename_axis。我们绕了一大圈,就是为了说明为什么rename_axis才应该是用来重命名行索引和索引的正规方法。

下面的例子显示了如何给多级索引的column索引重命名:

1
df.rename_axis(["type", "column"], axis=1)

这非常像一个Excel工作表中,发生标题单元格合并的情况。

访问索引的值

有时候我们需要查看索引的值,也许是为了troubleshooting,也许是为了传递给其它模块。比如,在因子检验中,我们可能特别想知道某一天筛选出来的表现最好的是哪些asset,而这个asset的值就在多级索引中。

如果只有一级索引,我们就用index或者columns来引用它们的值。如果是多级索引呢?Pandas引入了level这个概念。我们仍以df2这个DataFrame为例。此时它应该是由new_date, new_asset为索引的DataFrame。

此时,new_date是level=0的行索引,new_asset是level=1的行索引。要取这两个索引的值,我们可以用df.index.get_level_values方法:

1
2
3
df2.index.get_level_values(0)
df2.index.get_level_values(level=1)
df2.index.get_level_values(level='new_asset')

当索引没有命名时,我们就要使用整数来索引。否则,就可以像第三行那样,使用它的名字来索引。

按索引查找记录

当存在多级索引时,检索索引等于某个值的全部记录非常简单,要使用xs这个函数。让我们回到df2这个DataFrame上。此时它应该是由new_date, new_asset为索引的DataFrame。

现在,我们要取asset等于000001的全部行情:

1
df2.xs('000001', level='asset')

我们将得到一个只有一级索引,包含了全部000001记录的DataFrame。

样本外测试之外,我们还有哪些过拟合检测方法?

在知乎上看到一个搞笑的贴子,说是有人为了卖策略,让回测结果好看,会在代码中植入大量的if 语句,判断当前时间是特定的日期,就不进行交易。但奥妙全在这些日期里,因为在这些日期时,交易全是亏损的。

内容的真实性值得怀疑。不过,这却是一个典型的过拟合例子。

过拟合和检测方法

过拟合是指模型与数据拟合得很好,以至于该模型不可泛化,从而不能在另一个数据集上工作。从交易角度来说,过拟合“设计”了一种策略,可以很好地交易历史数据,但在新数据上肯定会失败。

过拟合是我们在回测中的头号敌人。如何检测过拟合呢?

一个显而易见的检测方法是样本外测试。它是把整个数据集划分为互不重叠的训练集和测试集,在训练集上训练模型,在测试集上进行验证。如果模型在测试集上也表现良好,就认为该模型没有拟合。

在样本本身就不足的情况下,样本外测试就变得困难。于是,人们发明了一些拓展版本。

其中一种拓展版本是 k-fold cross-validation,这是在机器学习中常见的概念。

它是将数据集随机分成 K 个大小大致相等的子集,对于每一轮验证,选择一个子集作为验证集,其余 K-1 个子集作为训练集。模型在训练集上训练,在验证集上进行评估。这个过程重复 K 次,最终评估指标通常为 K 次验证结果的平均值。

这个过程可以简单地用下图表示:

k-fold cross validation,by sklearn

但在时间序列分析(证券分析是其中典型的一种)中,k-fold方法是不适合的,因为时间序列分析有严格的顺序性。因此,从k-fold cross-validation特化出来一个版本,称为 rolling forecasting。你可以把它看成顺序版本的k-fold cross-validation。

它可以简单地用下图表示:

rolling forecasting, by tsfresh

从k-fold cross-validation到rolling forecasting的两张图可以看出,它们的区别在于一个是无序的,另一个则强调时间顺序,训练集和验证集之间必须是连续的。

有时候,你也会看到 Walk-Forward Optimization这种说法。它与rolling forecasting没有本质区别。

不过,我最近从buildalpha网站上,了解到了一种新颖的方法,这就是噪声测试。

新尝试:噪声测试

buildalpha的噪声测试,是将一定比率的随机噪声叠加到回测数据上,然后再进行回测,并将基于噪声的回测与基于真实数据的回测进行比较。

它的原理是,在我们进行回测时,历史数据只是可能发生的一种可能路径。如果时间重演,历史可能不会改变总的方向,但是偶然性会改变历史的步伐。而一个好的策略,应该是能对抗偶然性、把握历史总的方向的策略。因此,在一个时间序列加上一些巧妙的噪声,就可能会让过拟合的策略失效,而真正有效的策略仍然闪耀。

buildalpha是一个类似tradingview的平台。要进行噪声测试,可以通过图形界面进行配置。

噪声测试设置, by buildalpha

通过这个对话框,buildalpha修改了20%左右的数据,并且对OHLC的修改幅度都控制在用ATR的20%以内。最下面的100表明我们将随机生成100组带噪声的数据。

我们对比下真实数据与叠加噪声的数据。

左图为真实数据,右图为叠加部分噪声的数据。叠加噪声后,在一些细节上,引入了随机性,但并没有改变股价走势(叠加是独立的)。如果股价走势被改变,那么这种方法就是无效的甚至有害的。

最后,在同一个策略上,对照回测的结果是:

噪声测试结果, by buildalpha

从结果上看,在历史的多条可能路径中,没有任何一条的回测结果能比真实数据好。换句话说,真实回测的结果之所以这么好,纯粹是因为制定策略的人,是带着上帝视角,从未来穿越回去的。

参数平原与噪声测试

噪声测试是稍稍修改历史数据再进行圆滑。而参数平原则是另一种检测过拟合的方法,它是指稍微修改策略参数,看回测表现是否会发生剧烈的改变。如果没有发生剧烈的改变,那么策略参数就是鲁棒的。

Build Alpha以可视化的方式,提供了参数平原检测。

在这个3D图中,参数选择为 X= 9和Y=4,如黑色简单所示。显然,这一区域靠近敏感区域,在其周围,策略的性能下降非常厉害。按照传统的推荐,我们应该选择参数 X=8和Y=8,这一区域图形更为平坦。

在很多时候,参数平原的提示是对的 -- 因为我们选择的参数,其实价格变化的函数;但它毕竟不是价格变化。最直接的方法是,当价格发生轻微变化时,策略的性能如果仍然处在一个平坦的表面,就更能说明策略是鲁棒的。

不过,这种图很难绘制,所以,Build Alpha绘制的仍然是以参数为n维空间的坐标、策略性能为其取值的三维图,但它不再是基于单个历史数据,而是基于一组历史数据:真实历史数据和增加了噪声的数据。在这种情况下,我们基于参数平原选择的最优参数将更为可靠。

本文参考了Build Alpha网站上的两篇文章,噪声测试参数优化噪声测试,并得到了 Nelson 网友的帮助,特此鸣谢!

[0818] QuanTide Weekly

本周要闻

  • 全球猴痘病例超1.56万,相关美股 GeoVax Labs收涨110.75%
  • 央行发布重要数据,7月M2同比增长6.3%,M1同比下降6.6%
  • 7月美国CPI同比上涨2.9%,零售销售额环比增长1%
  • 证券时报:国企可转债的刚兑信仰该放下了

下周看点

  • 周三,ETF期权交割日。以往数据表明,ETF期权交割日波动较大。
  • 周三和周四,分别有3692亿和5777亿巨量逆回购到期。央行此前宣传将于到期日续作。
  • 周二,上交所和中证指数公司发布科创板200指数
  • 游戏《黑神话:悟空》正式发售,多家媒体给出高分

本周精选

  • OpenBB实战!如何轻松获得海外市场数据?
  • 贝莱德推出 HybridRAG,用于从财务文档中提取信息,正确率达100%!

本周要闻详情

  • 世界卫生组织14日宣布,猴痘疫情构成“国际关注的突发公共卫生事件”。今年以来报告猴痘病例数超过1.56万例,已超过去年病例总数,其中死亡病例达537例。海关总署15日发布公告,要求来自疫情发生地人员主动进行入境申报并接受检测。相关话题登上东方财富热榜,载止发稿,达到107万阅读量,远超第二名(AI眼镜)的63万阅读量(新华网、东方财富)
  • 央行13日发布重要数据,显示7月末M2余额303.31万亿元,同比增长6.3%;M1余额同比下降6.6%,M0同比增长12%。其中M1连续4个月负增长,表明企业活期存款下降,有些还在逐步向理财转化(第一财经)
  • 今年7月美国CPI同比上涨2.9%,环比上涨0.2%,核心CPI同比上涨3.2%,同比涨幅为2021年4月以来最低值,但仍高于美联储设定的2%长期通胀目标。(新华网)
  • 美国7月零售销售额环比增长1% 高于市场预期,是自2023年1月以来的最高值,汽车、电子产品、食品等销售额均增长。经济学家认为,美国经济将实现“软着陆”,在通胀“降温”的同时而不进入衰退。(中国新闻网)
  • 纳斯达克指数一周上涨5.29%,道指一周上涨2.94%,再涨2%将创出历史新高。
  • 证券时报:近日,某国企公告其发行的可转债到期违约,无法兑付本息,成为全国首例国企可转债违约,国企刚兑的信仰被打破。在一个成熟市场,相关主体真实的经营和财务状况,决定了其金融产品的风险等级。投资者在进行资产配置时,应该重点考量发行人的经营和财务状况,而不是因为对方是某种身份,就进行“拔高”或“歧视”。首例国企可转债违约的案例出现了,这是可转债市场的一小步,更是让市场之手发挥作用、让金融支持实体经济高质量发展的一大步。长期以来,可转债是非常适合个人和中小机构配置的一种标的。下有债性托底,上有股性空间,也是网格交易法的备选标的之一。

OpenBB实战!

你有没有这样的经历?常常看到一些外文的论文或者博文,研究方法很好,结论也很吸引人,忍不住就想复现一下。

但是,这些文章用的数据往往都是海外市场的。我们怎么才能获得免费的海外市场数据呢?

之前有 yfinance,但是从 2021 年 11 月起,就对内陆地区不再提供服务了。我们今天就介绍这样一个工具,OpenBB。它提供了一个数据标准,通过它可以聚合许多免费和付费的数据源。

在海外市场上,Bloomberg 毫无疑问是数据供应商的老大,产品和服务包括财经新闻、市场数据、分析工具,在全球金融市场中具有极高的影响力,是许多金融机构、交易员、分析师和决策者不可或缺的信息来源。不过 Bloomberg 的数据也是真的贵。如果我们只是个人研究,或者偶尔使用一下海外数据,显然,还得寻找更有性价比的数据源。

于是,OpenBB 就这样杀入了市场。从这名字看,他们是想做一个一个 Open Source 的 Bloomberg。

安装 openbb

通过以下命令安装 openbb:

1
pip install openbb[all]

Tip

openbb 要求的 Python 版本是 3.11 以上。你最好单独为它创建一个虚拟环境。


安装后,我们有多种方式可以使用它。

使用命令行

安装后,我们可以在命令行下启动 openbb。

R50

然后就可以按照提示,输入命令。比如,如果我们要获得行情数据,就可以一路输入命令 equity > price, 再输入 historical --symbol LUV --start_date '2024-01-01' --end_date '2024-08-01',就可以得到这支股票的行情数据。

openbb 会在此时弹出一个窗口,以表格的形式展示行情数据,并且允许你在此导出数据。

效果有点出人意料,哈哈。


比较有趣的是,他们把命令设计成为 unix 路径的模式。所以,在执行完刚才的命令之后,我们可以输入以根目录为起点的其它命令,比如:

1
/economy/gdp
我们就可以查询全球 GDP 数据。

使用 Python

我们通过 notebook 来演示一下它的使用。

1
2
3
from openbb import obb

obb

这个 obb 对象,就是我们使用 openbb 的入口。当我们直接在单元格中输入 obb 时,就会提示我们它的属性和方法:

在这里,openbb 保持了接口的一致性。我们看到的内容和在 cli 中看到的差不多。

现在,我们演示一些具体的功能。首先,通过名字来查找股票代码:


1
2
3
from openbb import obb

obb.equity.search("JPMorgan", provider="nasdaq").to_df().head(3)

输出结果为:

作为一个外国人,表示要搞清楚股票代码与数据提供商的关系,有点困难。不过,如果是每天都研究它,花点时间也是应该的。

我们从刚才的结果中,得知小摩(我常常记不清 JPMorgan 是大摩还是小摩。但实际上很好记。一个叫摩根士丹利,另一个叫摩根大通。大通是小摩)的股票代码是 AMJB(名字是 JPMorgan Chase 的那一个),于是我们想查一下它的历史行情数据。如果能顺利取得它的行情数据,我们的教程就可以结束了。

但是,当我们调用以下代码时:

1
obb.equity.price.historical("AMJB")

出错了!提示 No result found.

使用免费、但需要注册的数据源

真实原因是 OpenBB 中,只有一个开箱即用的免费数据源 -- CBOE,但免费的 CBOE 数据源里没有这个股票。我们要选择另外一个数据源,比如 FMP。但是,需要先注册 FMP 账号(免费),再将 FMP 账号的 API key 添加到 OpenBB hub 中。


FMP 是 Financial Modeling Prep (FMP) 数据提供商,它提供免费(每限 250 次调用)和收费服务,数据涵盖非常广泛,包括了美国股市、加密货币、外汇和详细的公司财务数据。免费数据可以回调 5 年的历史数据。

Tip

OpenBB 支持许多数据源。这些数据源往往都提供了一些免费使用次数。通过 OpenBB 的聚合,你就可以免费使用尽可能多的数据。

注册 FMP 只需要有一个邮箱即可,所以,如果 250 次不够用,看起来也很容易加量。注册完成后,就可以在 dashboard 中看到你的 API key:

75%

然后注册 Openbb Hub 账号,将这个 API key 添加到 OpenBB hub 中。

50%

现在,我们将数据源改为 FMP,再运行刚才的代码,就可以得到我们想要的结果了。


1
obb.equity.price.historical("AMJB", provider="fmp").to_df().tail()

我们将得到如下结果:

换一支股票,apple 的,我们也是先通过 search 命令,拿到它的代码'AAPL'(我常常记作 APPL),再代入上面的代码,也能拿到数据了。

需要做一点基本面研究,比如,想知道 apple 历年的现金流数据?

1
obb.equity.fundamental.cash("AAPL", provider='fmp').to_df().tail()

任何时候,交易日历、复权信息和成份股列表都是回测中不可或缺的(在 A 股,还必须有 ST 列表和涨跌停历史价格)。我们来看看如何获取股标列表和成份股列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 获取所有股票列表
all_companies = obb.equity.search("", provider="sec")

print(len(all_companies.results))
print(all_companies.to_df().head(10))

# 获取指数列表
indices = obb.index.available(provider="fmp").to_df()
print(indices)

# 获取指数成份股,DOWJONES, NASDAQ, SP500(无权限)
obb.index.constituents("dowjones", provider='fmp').to_df()

好了。尝试一个新的库很花时间。而且往往只有花过时间之后,你才能决定是否要继续使用它。如果最终不想使用它,那么前面的探索时间就白花了。

于是,我们就构建了一个计算环境,在其中安装了 OpenBB,并且注册了 fmp 数据源,提供了示例 notebook,供大家练习 openbb。

这个环境是免费提供给大家使用的。登录地址在这里,口令是: ope@db5d


贝莱德推出 HybridRAG

大模型已经无处不在,程序员已经发现,使用大模型来生成代码非常好用 -- 但这有一个关键,就是程序员知道自己在干什么,并且大模型生成的代码是否正确。

但涉及到金融领域,事情就变得复杂起来。没有人知道大模型生成的答案是否正确,而且也不可能像程序员那样进行验证 -- 这种验证要么让你错过时机,要么让你损失money。

从非结构化文本,如收益电话会议记录和财务报告中,提取相关见解的能力对于做出影响市场预测和投资策略的明智决策至关重要,这一直是贝莱德全球最大的资产管理公司的核心观点之一。

最近他们的研究人员与英伟达一起,推出了一种称为 HybridRAG 的新颖方法。该方法集成了 VectorRAG 和基于知识图的 RAG (GraphRAG) 的优点,创建了一个更强大的系统,用于从财务文档中提取信息。

HybridRAG 通过复杂的两层方法运作。最初,VectorRAG 基于文本相似性检索上下文,这涉及将文档划分为较小的块并将它们转换为存储在向量数据库中的向量嵌入。然后,系统在该数据库中执行相似性搜索,以识别最相关的块并对其进行排名。同时,GraphRAG 使用知识图来提取结构化信息,表示财务文档中的实体及其关系。通过合并这两种上下文,HybridRAG 可确保语言模型生成上下文准确且细节丰富的响应。

HybridRAG 的有效性通过使用 Nifty 50 指数上市公司的财报电话会议记录数据集进行的广泛实验得到了证明。该数据集涵盖基础设施、医疗保健和金融服务等各个领域,为评估系统性能提供了多样化的基础。研究人员比较了 HybridRAG、VectorRAG 和 GraphRAG,重点关注忠实度、答案相关性、上下文精确度和上下文召回等关键指标。


分析结果表明,HybridRAG 在多个指标上均优于 VectorRAG 和 GraphRAG。 HybridRAG 的忠实度得分为 0.96,表明生成的答案与提供的上下文相符。关于答案相关性,HybridRAG 得分为 0.96,优于 VectorRAG (0.91) 和 GraphRAG (0.89)。

GraphRAG 在上下文精确度方面表现出色,得分为 0.96,而 HybridRAG 在上下文召回方面保持了强劲的表现,与 VectorRAG 一起获得了 1.0 的满分。这些结果强调了 HybridRAG 在提供准确、上下文相关的响应,同时平衡基于矢量和基于图形的检索方法的优势方面的优势。

论文链接

里程碑!DuckDB 发布 1.0

有一个数据库项目,每月下载次数高达数百万,仅扩展的下载流量每天就超过 4 TB 。在 GitHub 和社交媒体平台上,该数据库拥有数以万计的 Stars 和粉丝,这是数据库类的产品难以企及的天花板。最近,这个极具人气的数据库迎来了自己的第一个大版本。

这个项目,就是 DuckDB。Duckdb 是列存储的方式,非常适合个人用户用作行情数据的存储。这也是我们关注它的原因。

Duckdb 这次发布 1.0 的主要准则是,它的数据存储格式已经稳定(并且目前来看最优化)了,不仅完全向后兼容,也提供了一定程度上的向前兼容。也就是说,达到这个版本之后,后面发布的更新,一般情况下将不会出现破坏式更新 -- 即不会出现必须手动处理迁移数据的情况。

从 1.0 发布以来,duckdb 的似乎受到了更大的欢迎:

在这次发布之后, duckdb 还发布了历年来 duckdb 性能上的提升:

当然在性能的横向比较上,duckdb 仍然是位居榜首的。这是 groupby 查询的比较:

Duckdb,Clickhouse 和 Polars 位居前三。Dask 会出 out-of-memory 错误,也是出人意料。这还做什么大数据、分布式啊。Pandas 虽然用了接近 20 分钟,但最终还是给出了结果,而 Modin 还不知道在几条街之后,你这要如何无缝替换 pandas?

这个是 50GB, 1B 行数据的 join 操作,直接让一众兄弟们都翻了车:

所以,Polars 还是很优秀啊。Clickhouse 有点出丑,直接出了异常。

不过,Clickhouse 之所以被拉进来测试,主要是因为它的性能很强悍,所以应该被拉来比划。但是,它跟 Duckdb 在功能上有很大的差异,或者说领先,比如分布式存储,并发读写(Duckdb 只支持一个读写,或者同时多个只读),此外还有作为服务器必不可少的账号角色管理等。另外,Duckdb 能管理的数据容量在 1TB 以下。更多的数据,还得使用 Clickhouse。

Duckdb 在资本市场上也很受欢迎。基于 DuckDB 的初创公司, MotherDuck 开发了 DuckDB 的无服务器版本,目前已经筹集了 5000 多万美元资金,估值达到 4 亿美元。在 AI 时代,能拿到这么高估值的传统软件公司非常罕见。作为对比,AI 教母李飞飞创办的 World Labs 目前估值也才 10 亿左右。

不过,Duckdb 也不是没有竞争者。除了 Polars 之外,直接使用 Clickhouse 引擎的 chDB 最近风头也很强劲,在 clickhouse 的官方 benchmark 比拼中,紧追 Duckdb。性能上尽管略微弱一点,但 chDB 已经支持 clickhouse 作为后端数据源,这一点上可能会吸引需要存储和分析更大体量数据的用户。

OpenBB实战!轻松获取海外市场数据

你有没有这样的经历?常常看到一些外文的论文或者博文,研究方法很好,结论也很吸引人,忍不住就想复现一下。

但是,这些文章用的数据往往都是海外市场的。我们怎么才能获得免费的海外市场数据呢?

之前有 yfinance,但是从 2021 年 11 月起,就对内陆地区不再提供服务了。我们今天就介绍这样一个工具,OpenBB。它提供了一个数据标准,通过它可以聚合许多免费和付费的数据源。

在海外市场上,Bloomberg 毫无疑问是数据供应商的老大,产品和服务包括财经新闻、市场数据、分析工具,在全球金融市场中具有极高的影响力,是许多金融机构、交易员、分析师和决策者不可或缺的信息来源。不过 Bloomberg 的数据也是真的贵。如果我们只是个人研究,或者偶尔使用一下海外数据,显然,还得寻找更有性价比的数据源。

于是,OpenBB 就这样杀入了市场。从这名字看,它就是一个 Open Source 的 Bloomberg。

OpenBB 有点纠结。一方面,它是开源的,另一方面,它又有自己的收费服务。当然,在金融领域做纯开源其实也没有什么意义,指着人家免费,自己白嫖赚钱,这事也说不过去。大家都是冲着赚钱来的,付费服务不寒碜人。

Info

感谢这些开源的产品,让所有人都有机会,From Zero To Hero! 金融一向被视为高端游戏,主要依赖性和血液来传播。开源撕开了一条口子,让普通人也能窥见幕后的戏法。
如果使用过 OpenBB,而它也确实达成了它的承诺,建议你前往 Github,为它点一个赞。
开源项目不需要我们用金钱来支持,但如果我们都不愿意给它一个免费的拥抱,最后大家就只能使用付费产品了。

安装 openbb

通过以下命令安装 openbb:

1
pip install openbb[all]

Tip

openbb 要求的 Python 版本是 3.11 以上。你最好单独为它创建一个虚拟环境。

安装后,我们有多种方式可以使用它。

使用命令行

安装后,我们可以在命令行下启动 openbb。

然后就可以按照提示,输入命令。比如,如果我们要获得行情数据,就可以一路输入命令 equity > price, 再输入 historical --symbol LUV --start_date '2024-01-01' --end_date '2024-08-01',就可以得到这支股票的行情数据。

openbb 会在此时弹出一个窗口,以表格的形式展示行情数据,并且允许你在此导出数据。

效果有点出人意料,哈哈。

比较有趣的是,他们把命令设计成为 unix 路径的模式。所以,在执行完刚才的命令之后,我们可以输入以根目录为起点的其它命令,比如:

1
/economy/gdp
我们就可以查询全球 GDP 数据。

使用 Python

我们通过 notebook 来演示一下它的使用。

1
2
3
from openbb import obb

obb

这个 obb 对象,就是我们使用 openbb 的入口。当我们直接在单元格中输入 obb 时,就会提示我们它的属性和方法:

在这里,openbb 保持了接口的一致性。我们看到的内容和在 cli 中看到的差不多。

现在,我们演示一些具体的功能。首先,通过名字来查找股票代码:

1
2
3
from openbb import obb

obb.equity.search("JPMorgan", provider="nasdaq").to_df().head(3)

输出结果为:

作为一个外国人,表示要搞清楚股票代码与数据提供商的关系,有点困难。不过,如果是每天都研究它,花点时间也是应该的。

我们从刚才的结果中,得知小摩(我常常记不清 JPMorgan 是大摩还是小摩。但实际上很好记。一个叫摩根士丹利,另一个叫摩根大通。大通是小摩)的股票代码是 AMJB(名字是 JPMorgan Chase 的那一个),于是我们想查一下它的历史行情数据。如果能顺利取得它的行情数据,我们的教程就可以结束了。

但是,当我们调用以下代码时:

1
obb.equity.price.historical("AMJB")

出错了!提示 No result found.

使用免费、但需要注册的数据源

真实原因是 OpenBB 中,只有一个开箱即用的免费数据源 -- CBOE,但免费的 CBOE 数据源里没有这个股票。我们要选择另外一个数据源,比如 FMP。但是,需要先注册 FMP 账号(免费),再将 FMP 账号的 API key 添加到 OpenBB hub 中。

FMP 是 Financial Modeling Prep (FMP) 数据提供商,它提供免费(每限 250 次调用)和收费服务,数据涵盖非常广泛,包括了美国股市、加密货币、外汇和详细的公司财务数据。免费数据可以回调 5 年的历史数据。

Tip

OpenBB 支持许多数据源。这些数据源往往都提供了一些免费使用次数。通过 OpenBB 的聚合,你就可以免费使用尽可能多的数据。

注册 FMP 只需要有一个邮箱即可,所以,如果 250 次不够用,看起来也很容易加量。注册完成后,就可以在 dashboard 中看到你的 API key:

然后注册 Openbb Hub 账号,将这个 API key 添加到 OpenBB hub 中。

现在,我们将数据源改为 FMP,再运行刚才的代码,就可以得到我们想要的结果了。

1
obb.equity.price.historical("AMJB", provider="fmp").to_df().tail()

我们将得到如下结果:

换一支股票,apple 的,我们也是先通过 search 命令,拿到它的代码'AAPL'(我常常记作 APPL),再代入上面的代码,也能拿到数据了。

需要做一点基本面研究,比如,想知道 apple 历年的现金流数据?

1
obb.equity.fundamental.cash("AAPL", provider='fmp').to_df().tail()

任何时候,交易日历、复权信息和成份股列表都是回测中不可或缺的(在 A 股,还必须有 ST 列表和涨跌停历史价格)。我们来看看如何获取股标列表和成份股列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 获取所有股票列表
all_companies = obb.equity.search("", provider="sec")

print(len(all_companies.results))
print(all_companies.to_df().head(10))

# 获取指数列表
indices = obb.index.available(provider="fmp").to_df()
print(indices)

# 获取指数成份股,DOWJONES, NASDAQ, SP500(无权限)
obb.index.constituents("dowjones", provider='fmp').to_df()

好了。尝试一个新的库很花时间。而且往往只有花过时间之后,你才能决定是否要继续使用它。如果最终不想使用它,那么前面的探索时间就白花了。

于是,我们就构建了一个计算环境,在其中安装了 OpenBB,并且注册了免费使用的 fmp 数据源,提供了示例 notebook,供大家练习 openbb。

这个环境是免费提供给大家使用的。如果你也想免安装立即试试 OpenBB,那么就进群看公告,领取登陆地址吧!

Datathon-我的Citadel量化岗之路!附历年比赛资料

Citadel是一家顶级的全球性对冲基金管理公司,由肯尼斯.格里芬(Kenneth Griffin)创建于1990年,是许多量化人的梦中情司。

Citadel为应届生提供多个岗位,但往往需要先进行一段实习。

Citadel的实习资格也是比较有难度,这篇文章就介绍一些通关技巧。

我们会对主要的三类岗位,即投资量化研究软件研发都进行一些介绍,但是重点会在量化研究岗位上。

我们将介绍获得量化研究职位的一条捷径,并提供一些重要的准备资料。

投资类岗位

投资类岗位可能是最难的,但待遇十分优渥。

2025年的本科或者硕士实习生将拿到5300美元的周薪,并且头一周都是住在四季酒店里,方便新人快速适应自己的同伴和进行社交。

在应聘条件上面,现在的专业和学校背景并不重要,你甚至可以不是经济学或者金融专业的,但需要对股票的估值感兴趣。

但他们的要求是“非凡(Extraordianry)” -- 这个非凡的标准实际上比哈佛的录取标准还要高一些。Citadel自称录用比是1%,哈佛是4%。如果要对非凡举个例子的话,Citadel会介绍说,他们招过NASA宇航员。

所以,关于这个岗位,我很难给出建议,但是Citadel在面试筛选上,是和Wonderlic合作的,如果你确实很想申这个岗位,建议先报一个Wonderlic的培训,大约$50左右。Wonderlic Select会有认知、心理、文化和逻辑方面的测试,参加此类培训,将帮助你刷掉一批没有准备的人。

11 weeks of extraordinary growth program

一旦入选为实习生,Citadel将提供一个11周的在岗实训,实训内容可以帮助你快速成长。通过实训后,留用的概率很大。

软件研发类

这个岗位比较容易投递,Citadel有一个基本的筛选,很快就会邀请你参加 HackerRank测试。由于HackerRank是自动化的,所以,几乎只要申请,都会得到邀请。

HackerRank有可能遇到LeetCode上困难级的题目,但也有人反映会意外地遇到Easy级别的题目。总的来说,平时多刷Leetcode是会有帮助的。并且准备越充分,胜出的机会就越大。

你可以在glassdoor或者一亩三分地(1point3acres)上看到Citadel泄漏出来的面试题,不过,多数信息需要付费。

量化研究和Datathon

参加Datathon并争取好的名次,是获得量化研究岗位实习的捷径。从历年比赛数据来看,竞争人数并不会很多(从有效提交编号分析),一年有两次机会。

这个比赛是由correlation one承办的。c1是由原对冲基金经理Rasheed Sabar发起的,专注于通过培训解决方案帮助企业和发展人才。

它的合作对象有DoD, Amazon, Citadel, Point 72等著名公司。可能正是因为创始人之前的职业人脉,所以它拿到了为Citadel, Point 72举办竞赛和招募的机会。在它的网站上有一些培训和招募项目,也可以看一看。

Correlation One

Datathon只针对在校生举办,你得使用学校邮箱来申请。通过官网在线报名后,你需要先进行一个90分钟的在线评估。这个评估有心理和价值观的、也有部分技术的。

评估结果会在比赛前三天通知。然后进入一个社交网络阶段(network session),在此阶段,你需要组队,或者加入别人的队伍。Datathon是协作性项目,一般要求4人一队参赛。

正式开始后,你会收到举办方发来的问题和数据集(我们已搜集历年测试问题、数据集及参赛团队提交的答案到网站,地址见文末),需要从中选择一个问题进行研究,并在7天内提交一个报告,阐明你们所进行的研究。

这个过程可能对内地的学生来讲生疏一些,但对海外留学生来讲,类似的协作和作为团队进行presentation是很平常的任务了。所以,内地的学生如果想参加的话,更需要这方面的练习,不然,会觉得7天时间不够用。

女生福利

女生除可以参加普通的Datathon之外,还有专属的Women's Datathon,最近的一次是明年的1月10日,现在开始准备正是好时机。

不过,这次Women's Datathon是线下的,仅限美国和加拿大在读学生参加。

Datathon通关技巧

Datathon看起来比赛的是数据分析能力,是硬技巧,但实际上,熟悉它的规则,做好团队协作也非常重要。而且从公司文化上讲,Citadel很注重协作。

  1. 组队时,一定要确保团队成员使用相同的编程语言,否则工作结果是没有办法聚合的。
  2. 尽管Citadel没有限制编程语言和工具软件,但最终提供的报告必须是PDF, PPT或者HTML。并且,如果你提交的PDF包含公式的话,还必须提供latex源码。考虑到比赛只有7天,所以你平时就必须很熟悉这些工具软件。或者,当你组队时,就需要考虑,团队中必须包含一个有类似技能的人。
  3. Datathon是在线的虚拟竞赛,所以,并没有现场的presentation环境。因此,一定要完全熟悉和遵循它的提交规范。
  4. 也正是因为上一条,Report一定要条理清晰,一定要从局外人的身份多读几次,看看项目之外的人读了这份报告,能否得到清晰的印象。
  5. 尽可能熟悉Jupyter Notebook和pandas(如果你使用Python的话)。这也是官方推荐,通过Notebook可以快速浏览竞赛所提供的数据集。
  6. 补充数据是有益的,这能反映你跳出框架自己解决问题的能力。所以平常要多熟悉一些数据集。如果一些数据要现爬的话,那需要非常熟悉爬虫。因为爬虫与后面的数据分析是串行的。在数据拿下来之前,其它工作都只能等待。
  7. Visualization非常重要。如果你习惯使用Python,平时可以多练习matplotlib和seaborn这两个库。

我们已经搜集了2017年以来所有的竞赛题目,包括数据、问题,以及一些团队提交的报告和代码。如果你需要准备Datathon,这会是一个非常好的参考。

在这里,我们对2024年夏的Datathon做一个简单介绍。

2024年 Summer Datathon

Problem Statement of 2024 Summer Datathon

2024年的Datathon于8月5日刚刚结束。这次的题目是关于垃圾食品的,要求从提供的数据集中,得出关于美国食品加工的一些结论。除了指定数据集之外,也允许根据需要自行添加新的数据集。不过,这些数据集也提交给评委,并且不得超过2G。

论题可以从以下三个之中选择,也可以自行拟定:

  1. 能够从肉类生产来预测餐馆的股价吗?
  2. 糖的价格会影响年青人对含糖饮料的消费吗?如果存在影响,这种影响会有地区差异吗?
  3. 肉制品生产低谷与失业人数相关吗?

有的团队已经将竞赛数据集、问题及他们的答案上传到github,下表是我们搜集的部分repo列表。其中包含了一些当年夺得过名次的solution,非常值得研究。如果你在练习中能达到此标准,那么就有较大概率在自己的比赛中取得名次。

year rank files 说明
2024 summer NA data, code, report 两个团队的报告,可运行
2024 spring NA data, code, report 目录清晰,报告质量高
2023 NA report, code
2022 3rd report, code 报告质量高,可视化效果
2021 summer 1st data, src, report 包含airbnb数据
2021 spring NA data,code,report
[2020] 3rd report
2018 1st data,code,report 两个团队的报告
[2017] NA report,code,data

这些竞赛的资料也都上传到了我们的课程环境。

Datathon 历年资料

如果你想立即开始练习,可以申请使用我们的课程环境,这样可以节省你下载数据、安装环境的时间。我们已帮你调通了2024年夏季比赛的代码,可以边运行边学习他人的代码。

[0811] QuanTide Weekly

本周要闻

  • 央行表示,将在公开市场操作中增加国债买卖。坚决防范汇率超调风险。
  • 统计局:七月 CPI 同比上涨 0.5%,PPI 同比下降 0.8%
  • 美最新初请失业金人数明显下降,市场对经济衰退的担忧稍解,美股震荡回升

下周看点

  • 周二晚美国PPI,周三晚美国核心CPI和周四零售销售数据
  • 周五(8 月 16 日)股指期货交割日
  • 周一马斯克连线特朗普

本周精选

  • Datathon-我的 Citadel 量化岗之路!附历年比赛资料
  • 视频通话也不能相信了! Deep-Live-Cam 一夜爆火,伪造直播只要一张照片!
  • 介绍一个量化库之 tsfresh

上期本周要闻中提到巴斯夫爆炸,维生素价格飙涨。本周维生素板块上涨 3.6%,最高上涨 6.7%。

  • 周末央行密集发声,完善住房租赁金融支持体系,支持存量商品房去库存。研究适度收窄利率走廊宽度。做好跨境资金流动的监测分析,防止形成单边一致性预期并自我强化,坚决防范汇率超调风险。利率走廊是指中央银行设定的短期资金市场利率波动范围。它通常由三个利率构成:
    政策利率:通常是央行的基准利率,如再贴现率或存款准备金利率。
    超额准备金利率(上限):银行存放在央行的超额准备金所获得的利息。
    隔夜拆借利率(下限):银行间市场的最低借贷成本。
  • 7 月 CPI 同比上涨 0.5%,前值 0.3%。其中猪肉上涨 20.4%,影响 CPI 上涨 0.24%。畜肉类价格上涨 4.9%,影响 CPI 上涨约 0.14%。受市场需求不足及部分国际大宗商品价格下行等因素影响,PPI 下降 0.8%,环比下降 0.2%。
    猪肉以一己之力拉升 CPI 涨幅近一半。猪肉短期上涨过快,涨势恐难持续,将对下月 CPI 环比构成压力。
  • 此前因 7 月失业率上升、巴菲特大幅减仓、日央行加息等多重导致全球股市巨幅震荡,美股经历2024年以来市场波动最大的一周。但在 8 日,美公布上周初请失业救济人数为 23.3 万,前值 25 万,预期 24 万。数据大幅好于前值和预期之后,市场担忧减弱,美股、日经等上周先跌后涨,基本收复失地。这一事件表明,近期市场对数据报告格外敏感。

  • 周五消息,嘉实基金董事长赵学军因个人问题配合有关部门调查。方正证券研究所所长刘章明被调整至副所长,不再担任研究所行政负责人。刘今年一月因违规荐股被出具警示函。

下周看点

  • 下周二、周三和周四,事关美联储降息的几大重要数据,如PPI, CPI和零售销售数据都将出炉。美联储鹰派人物表示,通胀率仍远高于委员会2%的目标,且存在上行风险。财报方面,家得宝、沃尔玛值得关注,身处商品供应链末端的大卖场们可能对于通胀在加速还是降速有更切身的体会。
  • 周五(8 月 16 日)股指期货交割日。今年以来,股指期货交割日上证指数表现比较平稳,甚至以上涨为主。

根据财联社、东方财富、证券时报等资讯汇编


DATATHON-我的 CITADEL 量化岗之路!附历年比赛资料

Kenneth Griffin Speak to Interns

Citadel 是一家顶级的全球性对冲基金管理公司,由肯尼斯. 格里芬 (Kenneth Griffin) 创建于 1990 年,是许多量化人的梦中情司。

Citadel 为应届生提供多个岗位,但往往需要先进行一段实习。

Citadel 的实习资格也是比较有难度,这篇文章就介绍一些通关技巧。


我们会对主要的三类岗位,即投资量化研究软件研发都进行一些介绍,但是重点会在量化研究岗位上。

我们将介绍获得量化研究职位的一条捷径,并提供一些重要的准备资料。


投资类岗位

投资类岗位可能是最难申请的,但待遇十分优渥。

2025 年的本科或者硕士实习生将拿到 5300 美元的周薪,并且头一周都是住在四季酒店里,方便新人快速适应自己的同伴和进行社交。

在应聘条件上面,现在的专业和学校背景并不重要,你甚至可以不是经济学或者金融专业的,但需要对股票的估值感兴趣。

但他们的要求是“非凡 (Extraordianry)” -- 这个非凡的标准实际上比哈佛的录取标准还要高一些。Citadel 自称录用比是 1%,哈佛是 4%。如果要对非凡举个例子的话,Citadel 会介绍说,他们招过 NASA 宇航员。

所以,关于这个岗位,我很难给出建议,但是 Citadel 在面试筛选上,是和 Wonderlic 合作的,如果你确实很想申这个岗位,建议先报一个 Wonderlic 的培训,大约$50 左右。Wonderlic Select 会有认知、心理、文化和逻辑方面的测试,参加此类培训,将帮你刷掉一批没有准备的人。

11 weeks of extraordinary growth program


一旦入选为实习生,Citadel 将提供一个 11 周的在岗实训,实训内容可以帮助你快速成长。通过实训后,留用的概率很大。

软件研发类

这个岗位比较容易投递,Citadel 在做一个很基本的筛选后,很快就会邀请你参加 HackerRank 测试。由于 HackerRank 是自动化的,所以,几乎只要申请,都会得到邀请。

HackerRank 有可能遇到 LeetCode 上困难级的题目,但也有人反映会意外地遇到 Easy 级别的题目。总的来说,平时多刷 Leetcode 是会有帮助的。并且准备越充分,胜出的机会就越大。

你可以在 glassdoor 或者一亩三分地(1point3acres)上看到 Citadel 泄漏出来的面试题,不过,多数信息需要付费。

量化研究和 Datathon

参加 Datathon 并争取好的名次,是获得量化研究岗位实习的捷径。从历年比赛数据来看,竞争人数并不会很多(从有效提交编号分析),一年有两次机会。

这个比赛是由 correlation one 承办的。c1 是由原对冲基金经理 Rasheed Sabar 发起的,专注于通过培训解决方案帮助企业和发展人才。

它的合作对象有 DoD, Amazon, Citadel, Point 72 等著名公司。可能正是因为创始人之前的职业人脉,所以它拿到了为 Citadel, Point 72 举办竞赛和招募的机会。在它的网站上有一些培训和招募项目,也可以看一看。


75% Correlation One

Datathon 只针对在校生举办,你得使用学校邮箱来申请。通过官网在线报名后,你需要先进行一个90 分钟的在线评估。这个评估有心理和价值观的、也有部分技术的。

评估结果会在比赛前三天通知。然后进入一个社交网络阶段(network session),在此阶段,你需要组队,或者加入别人的队伍。Datathon 是协作性项目,一般要求4 人一队参赛。

正式开始后,你会收到举办方发来的问题和数据集(我们已搜集历年测试问题、数据集及参赛团队提交的答案到网站,地址见文末),需要从中选择一个问题进行研究,并在 7 天内提交一个报告,阐明你们所进行的研究。

这个过程可能对内地的学生来讲生疏一些,但对海外留学生来讲,类似的协作和作为团队进行 presentation 是很平常的任务了。所以,内地的学生如果想参加的话,更需要这方面的练习,不然,会觉得 7 天时间太赶。


女生福利

女生除可以参加普通的 Datathon 之外,还有专属的 Women's Datathon,最近的一次是明年的 1 月 10 日,现在开始准备正是好时机。

不过,这次 Women's Datathon 是线下的,仅限美国和加拿大在读学生参加。

Datathon 通关技巧

Datathon 看起来比赛的是数据分析能力,是硬技巧,但实际上,熟悉它的规则,做好团队协作也非常重要。而且从公司文化上讲,Citadel 很注重协作。

  1. 组队时,一定要确保团队成员使用相同的编程语言,否则工作结果是没有办法聚合的。
  2. 尽管 Citadel 没有限制编程语言和工具软件,但最终提供的报告必须是 PDF, PPT 或者 HTML。并且,如果你提交的 PDF 包含公式的话,还必须提供 latex 源码。考虑到比赛只有 7 天,所以你平时就必须很熟悉这些工具软件。或者,当你组队时,就需要考虑,团队中必须包含一个有类似技能的人。
  3. Datathon 是在线的虚拟竞赛,所以,并没有现场的 presentation 环境。因此,一定要完全熟悉和遵循它的提交规范。
  4. 也正是因为上一条,Report 一定要条理清晰,一定要从局外人的身份多读几次,看看项目之外的人读了这份报告,能否得到清晰的印象。
  5. 尽可能熟悉 Jupyter Notebook 和 pandas(如果你使用 Python 的话)。这也是官方推荐,通过 Notebook 可以快速浏览竞赛所提供的数据集。

  1. 补充数据是有益的,这能反映你跳出框架自己解决问题的能力。所以平常要多熟悉一些数据集。如果一些数据要现爬的话,那需要非常熟悉爬虫。因为爬虫与后面的数据分析是串行的。在数据拿下来之前,其它工作都只能等待。
  2. Visualization 非常重要。如果你习惯使用 Python,平时可以多练习 matplotlib 和 seaborn 这两个库。

我们已经搜集了 2017 年以来所有的竞赛题目,包括数据、问题,以及一些团队提交的报告和代码。如果你需要准备 Datathon,这会是一个非常好的参考。

在这里,我们对 2024 年夏的 Datathon 做一个简单介绍。

2024 年 Summer Datathon

Problem Statement of 2024 Summer Datathon

2024 年的 Datathon 于 8 月 5 日刚刚结束。这次的题目是关于垃圾食品的,要求从提供的数据集中,得出关于美国食品加工的一些结论。除了指定数据集之外,也允许根据需要自行添加新的数据集。不过,这些数据集也提交给评委,并且不得超过 2G。

论题可以从以下三个之中选择,也可以自行拟定:


  1. 能够从肉类生产来预测餐馆的股价吗?
  2. 糖的价格会影响年青人对含糖饮料的消费吗?如果存在影响,这种影响会有地区差异吗?
  3. 肉制品生产低谷与失业人数相关吗?

有的团队已经将竞赛数据集、问题及他们的答案上传到 github,下表是我们搜集的部分 repo 列表。其中包含了一些当年夺得过名次的 solution,非常值得研究。如果你在练习中能达到此标准,那么就有较大概率在自己的比赛中取得名次。

year rank files 说明
2024 summer NA data, code, report 两个团队的报告,可运行
2024 spring NA data, code, report 目录清晰,报告质量高
2023 NA report, code
2022 3rd report, code 报告质量高,可视化效果
2021 summer 1st data, src, report 包含 airbnb 数据
2021 spring NA data,code,report
[2020] 3rd report
2018 1st data,code,report 两个团队的报告
[2017] NA report,code,data

这些竞赛的资料也都上传到了我们的 Jupyter Lab 服务器,只需要付很小的费用就可以使用。无须下载和安装,你就可以运行和调试其他人提交的答案。

75% Datathon 历年资料


如果你想立即开始练习,可以申请使用我们的课程环境,这样可以节省你下载数据、安装环境的时间。我们已帮你调通了 2024 年夏季比赛的代码,可以边运行边学习他人的代码。


视频通话也不能相信了! DEEP-LIVE-CAM 一夜爆火,伪造直播只要一张照片!

L50

让马斯克为你带货!

AI 换脸已不是什么大新闻,视频换脸也早就被实现,最早出现的就是 Deep Fake。但是,如果说直播和视频通话也能被实时换脸呢?

发布数月之久的 Deep Live Cam 最近一夜爆火,很多人注意到它伪造直播只要一张照片。

最近,博主 MatthewBerman 进行了一次测试。 他正戴着眼镜在镜头前直播,当给模型一张马斯克的照片之后,直播流立马换脸成了马斯克!就连眼镜也几乎很好地还原了!

他还测试了暗光条件和点光源的条件——常规情况下较难处理的场景,但是 Deep-Live-Cam 的表现都非常丝滑,暗光条件下的甚至更像马斯克了!

这个项目已在 Github 上开源,目前星标接近 8k。对硬件要求不高,只用 CPU 也可以运行。


快快提醒家里的老人,如果接到孩子的电话,特别是要钱的,一定要先问密码验证问题。如果老人记不住密码验证问题,也可以教老人使用先挂断,再主动拨回去的方法。

这个版本支持 Windows 和 MacOS。需要使用 Python 3.10, git,visual studio 2022 运行时(Windows)或者 onnxruntimes-silicon(MacOS Arm)和 ffmpeg。第一次运行会下载一些模型,大约 300M。


介绍一个量化库之 TSFRESH

TsFresh 是一个 Python 库,用于识别时间序列数据中的模式。tsfresh 这个词来自于 Time Series Feature extraction based on scalable hypothesis tests"。

50%

为什么要使用 tsfresh 呢?

实际上,tsfresh 并不专门为量化设计的。但由于 k 线数据具有时间序列特征,因此可以利用 tsfresh 进行一部分特征提取。在量化场景下使用 tsfresh,主要收益有:

  1. 在机器学习场景下,可能需要大量的 feature。如果这些 featrue 都通过手工来构造,不光时间成本很高,正确性也难以保证。从其文档来看,tsfresh 在算法和代码质量上应该是很优秀的。它的算法有专门的白皮书进行描述,代码也有单元测试来覆盖。所以,即使一个算法自己能实现,我也愿意依赖 tsfresh(当然要多读文档和源码)。毕竟,在 feature 阶段出了错,策略一定失败并且无处查起。
  2. 如果要提取时间序列的多个特征,手工提取就很难避免串行化执行,从而导致速度很慢。而 tsfresh 已经实现了并行化。

当然,我们也要认识到,尽管很多 awesome-quant list 列入了 tsfresh,但 tsfresh 并不是特别适合量化场景,因为金融时间序列充满了噪声。数据不是没有规律,而是这些规律隐藏在大量的随机信号中,而很多时间序列特征库,都是基于时间序列是有比较强的规律这一事实设计出来的。

所以,tsfresh 中许多 feature,实际上并没有 ta-lib 中的特征来得有效。

但如果你要运用机器学习方法,并且有大量标注数据的话(这实际上是比较有难度、很花钱的一件事),那么可以参考下面的示例,快速上手 tsfresh 加机器学习。

Medium 上有一篇文章,介绍了如何使用 tsfresh 提取特征,并使用 ARDRegression (sklearn 中的一种线性回归)来预测加密货币价格。文章附有 代码,正在探索加密货币的读者可以尝试一下。

如果你愿意看视频的话,Nils Braun在 PyCon 2017 上以tsfresh进行股票预测为例做了一次 presentation,也很有趣,注意看到最后的Q&A session。

50%