跳转至




tools

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个。

量化人如何用好 Jupyter?(二)

当我们使用 Jupyter 时,很显然我们的主要目的是探索数据。这篇文章将介绍如何利用 JupySQL 来进行数据查询--甚至代替你正在使用的 Navicat, dbeaver 或者 pgAdmin。此外,我们还将介绍如何更敏捷地探索数据,相信这些工具,可以帮你省下 90%的 coding 时间。


JupySQL - 替换你的数据库查询工具

JupySQL 是一个运行在 Jupyter 中的 sql 查询工具。它支持传统关系型数据库(PostgreSQL, MySQL, SQL server)、列数据库(ClickHouse),数据仓库 (Snowflake, BigQuery, Redshift, etc) 和嵌入式数据库 (SQLite, DuckDB) 的查询。

之前我们不得不为每一种数据库寻找合适的查询工具,找到开源、免费又好用的其实并不容易。有一些工具,设置还比较复杂,比如像 Tabix,这是 ClickHouse 有一款开源查询工具,基于 web 界面的。尽管它看起来简单到甚至无须安装,但实际上这种新的概念,导致一开始会引起一定的认知困难。在有了 JupySQL 之后,我们就可以仅仅利用我们已知的概念,比如数据库连接串,SQL 语句来操作这一切。


除了查询支持之外,JupySQL 的另一特色,就是自带部分可视化功能。这对我们快速探索数据特性提供了方便。

安装 JupySQL

现在,打开一个 notebook,执行以下命令,安装 JupySQL:

1
%pip install jupysql duckdb-engine --quiet

之前你可能是这样使用 pip:

1
! pip install jupysql

在前一篇我们学习了 Jupyter 魔法之后,现在你知道了,%pip 是一个 line magic。

显然,JupySQL 要连接某种数据库,就必须有该数据库的驱动。接下来的例子要使用 DuckDB,所以,我们安装了 duckdb-engine。

Info

DuckDB 是一个性能极其强悍、有着现代 SQL 语法特色的嵌入式数据库。从测试上看,它可以轻松管理 500GB 以内的数据,并提供与任何商业数据库同样的性能。

在安装完成后,需要重启该 kernel。


JupySQL 是作为一个扩展出现的。要使用它,我们要先用 Jupyter 魔法把它加载进来,然后通过%sql 魔法来执行 sql 语句:

1
2
3
4
5
6
7
%load_ext sql

# 连接 DUCKDB。下面的连接串表明我们将使用内存数据库
%sql duckdb://

# 这一行的输出结果为 1,表明 JUPYSQL 正常工作了
%sql select 1

数据查询 (DDL 和 DML)

不过,我们来点有料的。我们从 baostock.com 上下载一个 A 股历史估值的示例文件。这个文件是 Excel 格式,我们使用 pandas 来将其读入为 DataFrame,然后进行查询:

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

df = pd.read_excel("/data/.common/valuation.xlsx")
%load_ext sql

# 创建一个内存数据库实例
%sql duckdb://

# 我们将这个 DATAFRAME 存入到 DUCKDB 中
%sql --persist df

现在,我们来看看,数据库里有哪些表,表里都有哪些字段:

1
2
3
4
5
# 列出数据库中有哪些表
%sqlcmd tables

# 列出表'DF'有哪些列
%sqlcmd columns -t df

最后一行命令将输出以下结果:

name type nullable default autoincrement comment
index BIGINT True None False None
date VARCHAR True None False None
code VARCHAR True None False None
close DOUBLE PRECISION True None False None
peTTM DOUBLE PRECISION True None False None
pbMRQ DOUBLE PRECISION True None False None
psTTM DOUBLE PRECISION True None False None
pcfNcfTTM DOUBLE PRECISION True None False None

作为数据分析师,或者量化研究员,这些命令基本上满足了我们常用的 DDL 功能需求。在使用 pgAdmin 的过程中,要找到一个表格,需要沿着 servers > server > databases > database > Schema > public > Tables 这条路径,一路展开所有的结点才能列出我们想要查询的表格,不免有些烦琐。JupySQL 的命令简单多了。


现在,我们预览一下这张表格:

1
%sql select * from df limit 5

我们将得到如下输出:

index date code close peTTM pbMRQ psTTM pcfNcfTTM
0 2022-09-01 sh.600000 7.23 3.978631 0.370617 1.103792 1.103792
1 2022-09-02 sh.600000 7.21 3.967625 0.369592 1.100739 1.100739
2 2022-09-05 sh.600000 7.26 3.99514 0.372155 1.108372 1.108372
3 2022-09-06 sh.600000 7.26 3.99514 0.372155 1.108372 1.108372
4 2022-09-07 sh.600000 7.22 3.973128 0.370105 1.102266 1.102266

%sql 是一种 line magic。我们还可以使用 cell magic,来构建更复杂的语句:

1
2
3
4
5
# EXAMPLE-1
%%sql --save agg_pe
select code, min(peTTM), max(peTTM), mean(peTTM)
from df
group by code

使用 cell magic 语法,整个单元格都会当成 sql 语句,这也使得我们构建复杂的查询语句时,可以更好地格式化它。这里在%%sql 之后,我们还使用了选项 --save agg_pe,目的是为了把这个较为复杂、但可能比较常用的查询语句保存起来,后面我们就可以再次使用它。


Tip

在 JupySQL 安装后,还会在工具栏出现一个 Format SQL 的按钮。如果一个单元格包含 sql 语句,点击它之后,它将对 sql 语句进行格式化,并且语法高亮显示。

我们通过 %sqlcmd snippets 来查询保存过的查询语句:

1
%sqlcmd snippets

这将列出我们保存过的所有查询语句,刚刚保存的 agg_pe 也在其中。接下来,我们就可以通过%sqlcmd 来使用这个片段:

1
2
3
4
5
6
7
query = %sqlcmd snippets agg_pe

# 这将打印出我们刚刚保存的查询片段
print(query)

# 这将执行我们保存的代码片段
%sql {{query}}

最终将输出与 example-1 一样的结果。很难说有哪一种数据库管理工具会比这里的操作来得更简单!

JupySQL 的可视化

JupySQL 还提供了一些简单的绘图,以帮助我们探索数据的分布特性。


1
%sqlplot histogram -t df -c peTTM pbMRQ

JupySQL 提供了 box, bar, pie 和 histogram。

超大杯的可视化工具

不过,JupyerSQL提供的可视化功能并不够强大,只能算是中杯。有一些专业工具,它们以pandas DataFrame为数据载体,集成了数据修改、筛选、分析和可视化功能。这一类工具有, Qgrid(来自 Quantpian),PandasGUI,D-Tale 和 mitosheet。其中D-Tale功能之全,岂止是趣大杯,甚至可以说是水桶杯。

我们首先探讨的是Qgrid,毕竟出自Quantpian之手,按理说他们可能会加入量化研究员最常用的一些分析功能。 他们在 Youtube 上提供了一个 presentation,介绍了如何使用 Qgrid 来探索数据的边界。不过,随着 QuantPian 关张大吉,所有这些工具都不再有人维护,因此我们也不重点介绍了。

PandasGUI 在 notebook 中启动,但它的界面是通过 Qt 来绘制的,因此,启动以后,它会有自己的专属界面,而且是以独立的 app 来运行。它似乎要求电脑是 Windows。

Mitosheet的界面非常美观。安装完成后,需要重启 jupyterlab/notebook server。仅仅重启 kernel 是不行的,因为为涉及到界面的修改。


重启后,在Notebook的工具条栏,会多出一个“New Mitosheet”的按钮,点击它,就会新增一个单元格,其内容为:

1
2
import mitosheet
mitosheet.sheet(analysis_to_replay="id-sjmynxdlon")

并且自动运行这个单元格,调出 mito 的界面。下面是 mitto 中可视化一例:

mitto 有免费版和专业版的区分,而且似乎它会把数据上传到服务器上进行分析,所以在国内使用起来,感觉不是特别流畅。

与上面介绍的工具相比,D-Tale 似乎没有这些工具有的这些短板。


我们在 notebook 中通过pip install dtale来安装 dtale。安装后,重启 kernel。然后执行:

1
2
3
import dtale

dtale.show(df)

这会显加载以下界面:

75%

在左上角有一个小三角箭头,点击它会显示菜单:

75%


我们点击describe菜单项看看,它的功能要比df.describe 强大不少。df.describe 只能给出均值、4 分位数值,方差,最大最小值,dtale 还能给出 diff, outlier, kurtosis, skew,绘制直方图,Q-Q 图(检查是否正态分布)。

注意我们可以导出进行这些计算所用的代码!这对数据分析的初学者确实很友好。

这是从中导出的绘制 qq 图的代码:

1
2
3
4
5
# DISCLAIMER: 'DF' REFERS TO THE DATA YOU PASSED IN WHEN CALLING 'DTALE.SHOW'

import numpy as np
import pandas as pd
import plotly.graph_objs as go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
if isinstance(df, (pd.DatetimeIndex, pd.MultiIndex)):
    df = df.to_frame(index=False)

# REMOVE ANY PRE-EXISTING INDICES FOR EASE OF USE IN THE D-TALE CODE, BUT THIS IS NOT REQUIRED
df = df.reset_index().drop('index', axis=1, errors='ignore')
df.columns = [str(c) for c in df.columns]  # update columns to strings in case they are numbers

s = df[~pd.isnull(df['peTTM'])]['peTTM']

import scipy.stats as sts
import plotly.express as px

qq_x, qq_y = sts.probplot(s, dist="norm", fit=False)
chart = px.scatter(x=qq_x, y=qq_y, trendline='ols', trendline_color_override='red')
figure = go.Figure(data=chart, layout=go.Layout({
    'legend': {'orientation': 'h'}, 'title': {'text': 'peTTM QQ Plot'}
}))

有了这个功能,如果不知道如何通过 plotly 来绘制某一种图,那么就可以把数据加载到 dtale,用 dtale 绘制出来,再导出代码。作为量化人,可能最难绘制的图就是 K 线图了。这个功能,dtale 有。

最后,实际上dtale是自带服务器的。我们并不一定要在 notebook 中使用它。安装 dtale 之后,可以在命令行下运行dtale命令,然后再打开浏览器窗口就可以了。更详细的介绍,可以看这份 中文文档

量化人如何用好Jupyter环境?(一)

网上有很多jupyter的使用技巧。但我相信,这篇文章会让你全面涨姿势。很多用法,你应该没见过。

  • 显示多个对象值
  • 魔法:%precision %psource %lsmagic %quickref等
  • vscode中的interactive window

1. 魔法命令

几乎每一个使用过Jupyter Notebook的人,都会注意到它的魔法(magic)功能。具体来说,它是一些适用于整个单元格、或者某一行的魔术指令。

比如,我们常常会好奇,究竟是pandas的刀快,还是numpy的剑更利。在量化中,我们常常需要寻找一组数据的某个分位数。在numpy中,有percentile方法,quantile则是她的pandas堂姊妹。要不,我们就让这俩姐妹比一比身手好了。有一个叫timeit的魔法,就能完成这任务。

不过,我们先得确定她们是否真有可比性。

1
2
3
4
5
6
7
8
import numpy as np
import pandas as pd

array = np.random.normal(size=1_000_000)
series = pd.Series(array)

print(np.percentile(array, 95))
series.quantile(0.95)

两次输出的结果都是一样,说明这两个函数确实是有可比性的。

在上面的示例中,要显示两个对象的值,我们只对前一个使用了print函数,后一个则省略掉了。这是notebook的一个功能,它会默认地显示单元格最后输出的对象值。这个功能很不错,要是把这个语法扩展到所有的行就更好了。


不用对神灯许愿,这个功能已经有了!只要进行下面的设置:

1
2
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"

在一个单独的单元格里,运行上面的代码,之后,我们就可以省掉print:

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

array = np.random.normal(size=1_000_000)
series = pd.Series(array)

# 这一行会输出一个浮点数
np.percentile(array, 95)

# 这一行也会输出一个浮点数
series.quantile(0.95)

这将显示出两行一样的数值。这是今天的第一个魔法。

现在,我们就来看看,在百万数据中探囊取物,谁的身手更快一点?

1
2
3
4
5
6
7
8
import numpy as np
import pandas as pd

array = np.random.normal(size=1_000_000)
series = pd.Series(array)

%timeit np.percentile(array, 95)
%timeit series.quantile(0.95)

我们使用%timeit来测量函数的运行时间。其输出结果是:

1
2
26.7 ms ± 5.67 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
21.6 ms ± 837 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

看起来pandas更快啊。而且它的性能表现上更稳定,标准差只有numpy的1/7。mean±std什么的,量化人最熟悉是什么意思了。

这里的timeit,就是jupyter支持的魔法函数之一。又比如,在上面打印出来的分位数,有16位小数之多,真是看不过来啊。能不能只显示3位呢?当然有很多种方法做到这一点,比如,我们可以用f-str语法:

1
f"{np.percentile(array, 95):.3f}"

啰哩啰嗦的,说好要Pythonic的呢?不如试试这个魔法吧:

1
2
%precision 3
np.percentile(array, 95)

之后每一次输出浮点数,都只有3位小数了,是不是很赞?

如果我们在使用一个第三方的库,看了文档,觉得它还没说明白,想看它的源码,怎么办?可以用psource魔法:

1
2
3
from omicron import tf

%psource tf.int2time

这会显示tf.int2time函数的源代码:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
    @classmethod
    def int2time(cls, tm: int) -> datetime.datetime:
        """将整数表示的时间转换为`datetime`类型表示

        examples:
            >>> TimeFrame.int2time(202005011500)
            datetime.datetime(2020, 5, 1, 15, 0)

        Args:
            tm: time in YYYYMMDDHHmm format

        Returns:
            转换后的时间
        """
        s = str(tm)
        # its 8 times faster than arrow.get()
        return datetime.datetime(
            int(s[:4]), int(s[4:6]), int(s[6:8]), int(s[8:10]), int(s[10:12])
        )

看起来Zillionare-omicron的代码,文档还是写得很不错的。能和numpy一样,在代码中包括示例,并且示例能通过doctest的量化库,应该不多。

Jupyter的魔法很多,记不住怎么办?这里有两个魔法可以用。一是%lsmagic:

1
%lsmagic

这会显示为:


确实太多魔法了!不过,很多命令是操作系统命令的一部分。另一个同样性质的魔法指令是%quickref,它的输出大致如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
IPython -- An enhanced Interactive Python - Quick Reference Card
================================================================

obj?, obj??      : Get help, or more help for object (also works as
                   ?obj, ??obj).
?foo.*abc*       : List names in 'foo' containing 'abc' in them.
%magic           : Information about IPython's 'magic' % functions.

Magic functions are prefixed by % or %%, and typically take their arguments
without parentheses, quotes or even commas for convenience.  Line magics take a
single % and cell magics are prefixed with two %%.

Example magic function calls:
...

输出内容大约有几百行,一点也不quick!

2. 在vscode中使用jupyter

R50

如果有可能,我们应该尽可能地利用vscode的jupyter notebook。vscode中的jupyter可能在界面元素的安排上弱于浏览器(即原生Jupyter),比如,单元格之间的间距太大,无法有效利用屏幕空间,菜单命令少于原生jupyter等等。但仍然vscode中的jupyter仍然有一些我们难于拒绝的功能。

首先是代码提示。浏览器中的jupyter是BS架构,它的代码提示响应速度比较慢,因此,只有在你按tab键之后,jupyter才会给出提示。在vscode中,代码提示的功能在使用体验上与原生的python开发是完全一样的。

其次,vscode中的jupyter的代码调试功能更好。原生的Jupyter中进行调试可以用%pdb或者%debug这样的magic,但体验上无法与IDE媲美。上图就是在vscode中调试notebook的样子,跟调试普通Python工程一模一样地强大。

还有一点功能是原生Jupyter无法做到的,就是最后编辑位置导航。


如果我们有一个很长的notebook,在第100行调用第10行写的一个函数时,发现这个函数实现上有一些问题。于是跳转到第10行进行修改,修改完成后,再回到第100行继续编辑,这在原生jupyter中是通过快捷键跳转的。

通常我们只能在这些地方,插入markdown cell,然后利用标题来进行快速导航,但仍然无法准确定位到具体的行。但这个功能在IDE里是必备功能。我们在vscode中编辑notebook,这个功能仍然具备。

notebook适于探索。但如果最终我们要将其工程化,我们还必须将其转换成为python文件。vscode提供了非常好的notebook转python文件功能。下面是本文的notebook版本转换成python时的样子:


转换后的notebook中,原先的markdown cell,转换成注释,并且以# %% [markdown]起头;而原生的python cell,则是以# %%起头。

vscode编辑器会把这些标记当成分隔符。每个分隔符,引起一个新的单元格,直到遇到下一个分隔符为止。这些单元格仍然是可以执行的。由于配置的原因,在我的工作区里,隐藏了这些toolbar,实际上它们看起来像下图这样。

66%

这个特性被称为Python Interactive Window,可以在vscode的文档vscode中查看。

我们把notebook转换成python文件,但它仍然可以像notebook一样,按单元格执行,这就像是个俄罗斯套娃。宇宙第一的IDE,vs code实至名归。

量化研究员如何写一手好代码

即使是Quant Research, 写一手高质量的代码也是非常重要的。再好的思路,如果不能正确地实现,都是没有意义的。只有正确实现了、通过回测检验过了,才能算是真正做出来了策略。

写一手高质量的代码的意义,对Quant developer来讲就更是自不待言了。这篇notebook笔记就介绍一些python best practice。

2024年,免费博客赚钱方案

几年前,我就推荐过用 Markdown 写作静态博客。静态博客几乎是零托管成本,比较适合个人博客起步。Markdown 便于本地搜索,也可当作是个人知识库方案。

现在有了新的进展。我不仅构建了一个视觉上相当不错的个人网站,还美化了 github、构建了个人出版系统 -- 将文章导出为排版精美的图片和 pdf 的能力。