《自己动手做套利机器人》课程第 15 课。前几种策略寻找的是两个交易所之间的价差。这一种寻找的是两枚通常一起移动的币之间的价差。
狗和它的主人
一个人牵着狗在长牵引绳上散步。狗会跑到前面,又落到后面,再跑向另一边。但牵引绳很长,不是无限长,狗不会长时间远离主人。如果你看到狗远远跑在前面,可以赌一分钟后它会更靠近主人。你不知道这一对会走向哪里,只知道它们之间的距离会回到正常水平。
两枚币也可能像这样。想想同一板块的两枚相似币,或一枚币和包装它的代币。每枚币都会因自己的原因上下波动,但有某种力量把它们拴在一起,它们之间的距离会不断回来。交易这个距离的回归叫作配对交易。它是最简单的一种统计套利。
你并不押注任一枚币的方向。距离太大时,卖出跑在前面的币,买入落在后面的币。距离缩小时,同时平掉两边。如果整个市场下跌,空头赚钱,多头亏钱,两者大致抵消。
直接的方法:比较价格
直白的想法是拿两个价格比较,看哪个更高。币 A 价格为 50,币 B 价格为 100。B“更贵”。卖 B,买 A?
这什么也说明不了。价格取决于发行了多少枚币,而不取决于币是否昂贵。较低价格的币在任何有用的意义上都不是更便宜。下面的演示市场中,开始时 A 为 50,B 为 100,固定的一半比例对这组币而言是正常的。
值得看的是比率的变化,不是比率本身。价格像乘法那样变化,一枚币涨 1%,再涨 2%,所以我们用对数把它们变成加法。一个比率的对数就是两个对数之差,这个差叫作价差:
spread = log(price of A) - log(price of B)
如果两枚币都翻倍,价差不会移动,因为两个对数都上升同样的数量。只有一枚币跑得比另一枚快时,它才会移动。这就是牵引绳上的距离。
多远才算太远:z-score
“价差很大”需要一个数字。取价差最近的 100 个值,计算它们的平均值以及围绕平均值的典型波动,标准差。然后问:今天的值比平均值高出多少个典型波动?
z = (spread now - average of the last 100) / wobble of the last 100
这就是 z-score。如果 z 为 0,这组币处在平常的距离。如果 z 为 2,价差比正常高出两个典型波动。在平静的钟形分布世界中,这大约每 100 天有 2 或 3 天会发生,因此是值得行动的罕见事件。
规则:
- z 高于 +2:价差过高,A 跑在前面。做空 A,买入 B。
- z 低于 −2:A 落后。买入 A,做空 B。
- 当 z 回到接近 0 时平掉这组仓位,我们使用绝对值低于 0.5。
- 每笔交易的四条腿都要付手续费:开 A、开 B、平 A、平 B。
代码
演示市场是刻意构造的:B 是随机游走,A 跟随 B,再加上一个总会向零回归的噪声,这就是牵引绳。然后我们运行规则。
import math, random, statistics
random.seed(3)
n = 1500
log_b, noise = [math.log(100)], [0.0]
for t in range(n):
log_b.append(log_b[-1] + random.gauss(0, 0.01)) # coin B: a random walk
noise.append(noise[-1] * 0.9 + random.gauss(0, 0.01)) # a noise that always drifts back to 0
log_a = [log_b[t] + noise[t] + math.log(0.5) for t in range(n + 1)] # coin A follows B
price_a = [math.exp(x) for x in log_a]
price_b = [math.exp(x) for x in log_b]
print("prices at the start:", round(price_a[0], 2), round(price_b[0], 2))
spread = [log_a[t] - log_b[t] for t in range(n + 1)]
print("spread mean", round(statistics.mean(spread), 3), "stdev", round(statistics.pstdev(spread), 3))
def backtest(spread, window=100, entry=2.0, exit_=0.5, fee=0.001):
pos, entry_spread, pnl, trades = 0, 0.0, 0.0, 0
for t in range(window, len(spread)):
w = spread[t - window:t]
z = (spread[t] - statistics.mean(w)) / statistics.pstdev(w)
if pos == 0:
if z > entry: pos, entry_spread = -1, spread[t] # spread high: short A, long B
elif z < -entry: pos, entry_spread = +1, spread[t] # spread low: long A, short B
elif abs(z) < exit_:
pnl += pos * (spread[t] - entry_spread) - 4 * fee # 2 legs in, 2 legs out
trades += 1
pos = 0
return trades, pnl * 100
for fee in (0.001, 0.003, 0.006, 0.01):
trades, result = backtest(spread, fee=fee)
print(f"fee {fee*100:.1f}% per leg: trades {trades}, result {result:+.1f} %")
输出:
prices at the start: 50.0 100.0
spread mean -0.693 stdev 0.026
fee 0.1% per leg: trades 22, result +64.3 %
fee 0.3% per leg: trades 22, result +46.7 %
fee 0.6% per leg: trades 22, result +20.3 %
fee 1.0% per leg: trades 22, result -14.9 %
读一读这些结果。
- 平均价差是 −0.693,即 log(0.5):“正常”的一半比率。它的波动为 0.026,约 2.6%。你交易的就是这个波动。
- 规则在 1500 步中完成 22 次完整往返。一笔交易赚取价差回归的距离,这里是手续费前每笔约 3.3%,计算方法是第一行的 64.3 加上 22 笔交易 × 0.4 手续费,再除以 22 笔交易。
- 手续费决定一切。每条腿 0.1% 时,四条腿每笔花 0.4%,总结果为 +64.3%。每条腿 1% 时,一枚小币的买卖价差很宽,四条腿花 4%,超过赚到的 3.3%,所以每笔交易都亏损,总结果为 −14.9%。
- 结果是以一条腿的名义金额的百分比表示,并假设牵引绳确实存在。
这个玩具隐藏了什么
把 +64% 当作机制的示意图。数字之所以很大,是因为我们构建了一个牵引绳永不断裂的世界。在真实市场中:
- 关联可能断裂。 两枚一起移动了一年的币,可能因其中一枚的新闻而永久分开。原本“过高”的价差会继续上升,跑掉的币上的空头会持续亏损。真实机器人需要一条规则:止损。如果 z 超过 4,或这组仓位开得太久,就平仓并接受损失。
- 关联必须检查。 牵引绳的检验有个名字,协整。用通俗的话说:取价差,检查它是否真的会回到平均值,而不是不断走远。一个简单的初步检查是比较数据前半段和后半段价差的波动。
- 两条腿的比例不一定是一比一。 这里 A 与 B 完全同步移动。真实数据中,一枚币可能比另一枚移动强 1.3 倍,因此两条腿的大小应为 1 比 1.3。寻找这个数字,回归,是完整方法的下一步。
- 成本比表中更大。 要计入手续费、买卖价差,以及空头腿的借币成本。
让 AI 智能体编写并测试代码
一个含糊的请求:
编写一个配对交易机器人。
一个自带检查条件的请求:
按第 15 课的代码编写 pairs.py。用 random.seed(3) 创建相同的合成市场,并精确打印本课中的各行。手续费为 0.1% 的行必须显示 22 笔交易和接近 +64.3 的结果。然后添加止损:绝对 z-score 高于 4 时平掉这组仓位,单独统计被止损的交易,并打印结果如何变化。再添加一个函数,它接收两枚币的收盘价列表,返回价差及其前半段和后半段的标准差。暂时不要交易,也不要下载数据。
先将第一个输出逐行与本课比较,再让智能体添加任何内容。
练习
- 运行代码,核对打印出的全部六行。
- 将入场阈值从 2.0 改为 1.5,再改为 3.0。在每条腿手续费为 0.1% 时,交易数量和结果如何变化?较低阈值的代价是什么?
- 故意让牵引绳断裂。第 750 步后,每一步都给噪声加 0.004,这是持续漂移。结果还能保持在零以上吗?加入智能体请求中的止损,看看数字如何变化。
- 下载你认为有关联的两枚币的每日收盘价,例如使用公开交易所 API,如第 9 课所示,并计算它们的价差和波动。价差是平的,还是会游走?
接下来
你为机器人有了第三种交易。第 16 课是终点:机器人以纸面模式运行,有仪表盘、README 和公开仓库,你会把它展示给其他人。