課程「建立自己的套利機器人」第 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 和公開儲存庫,你會把它展示給其他人。