課程「建立自己的套利機器人」第 9 課。世界 3 從這裡開始:機器人的第 2 階段,成交模擬。到目前為止,每一側我們只用一個價格。今天你會學到為什麼一個價格不夠。
市場攤位
想像一個賣蘋果的市場攤位。賣家有幾箱蘋果,每箱都有自己的價格。第一箱有 2 公斤,每公斤 100 枚幣。第二箱有 3 公斤,每公斤 100.20 枚幣。如果你要 2 公斤,每公斤付 100。如果你要 5 公斤,前 2 公斤每公斤 100,接下來 3 公斤每公斤 100.20。你的平均價格會高於第一個價格。
委託簿就是某種幣的這種攤位。它有兩側。
- 賣方(ask)列出賣家。每一列表示:「我以這個價格賣這個數量」。各列從最低價(最佳賣價)往上排序。你買入時,就從頂端依序吃掉這些列。
- 買方(bid)列出買家。每一列表示:「我以這個價格買這個數量」。各列從最高價(最佳買價)往下排序。你賣出時,就從頂端依序吃掉這些列。
一列稱為一個價位。真實委託簿有數千個價位,但最上方幾個最重要。
一個小型委託簿:
| 賣價 | 賣方數量 |
|---|---|
| 100.20 | 3 |
| 100.00 | 2 |
| 買價 | 買方數量 |
|---|---|
| 99.90 | 5 |
| 99.80 | 4 |
(賣方依照許多交易所的顯示方式,把最差的價格寫在上方、最佳價格寫在下方,因此最佳賣價和最佳買價會在中間相鄰。)
最佳賣價 100.00,最佳買價 99.90,價差 0.10。
正面做法:一個價格乘以數量
直接的方法是把最佳價格乘以數量:3 枚幣以 100.00 計算,成本是 300.00。這是第 1 到第 4 課的掃描器暗中假設的做法。下面的真實成本是 300.20,所以直接方法在 3 枚幣的交易上差了 0.20,而且誤差會隨數量增加。對 0.10 的套利空間來說,這就是獲利與虧損的差別。這個直接數字只能用於快速初步查看,絕不能用來做決策。
走訪委託簿
走訪委託簿的意思是:要找出一筆交易的成本,就從最佳價位開始,拿走該價位有多少(或你還需要多少),再移到下一個價位,一直繼續到拿到全部所需數量。
例子。你在上面的委託簿(賣方)買入 3 枚幣。
- 價位 1:100.00,數量 2。買 2 枚幣。成本 2 × 100.00 = 200.00。你還需要 1 枚。
- 價位 2:100.20,數量 3。買 1 枚幣。成本 1 × 100.20 = 100.20。
- 總成本:300.20。平均價格 300.20 / 3 = 100.0667。
你本來想以 100.00 交易,但你的平均價格是 100.0667。每枚幣多出的 0.0667 成本稱為滑價。它會隨交易數量增加。
在買方賣出也一樣。如果你把 3 枚幣賣給買方:100.30 有 1 枚幣、99.90 有 5 枚幣:1 × 100.30 + 2 × 99.90 = 300.10。平均為 100.0333。
消失的套利
第 3 課中,畫面看見交易所 A 的賣價 100.00 和交易所 B 的買價 100.30,價差是 0.30。為 3 枚幣走訪兩邊委託簿:你在 A 付 300.20,在 B 收到 300.10。淨額 −0.10。階段 1 回報的價差,對 3 枚幣交易並不存在。它對 1 枚幣存在,因為 B 的最上方價位數量是 1:在 A 買 1 枚幣成本為 100.00,在 B 賣 1 枚幣可得 100.30,利潤是 0.30。所以這個價差對 1 枚幣是真的,對 3 枚幣卻是假的。
因此,階段 2 的問題不是「有沒有價差?」而是「哪個數量的利潤為正,以及最佳數量是多少?」
程式碼
def walk(levels, amount):
"""levels: [(price, size), ...], best price first.
Returns (money, filled): money spent or received, and how much was actually filled."""
money, left = 0.0, amount
for price, size in levels:
take = min(size, left)
money += take * price
left -= take
if left <= 0:
break
return money, amount - left
asks_A = [(100.00, 2), (100.20, 3)] # 在這裡買入
bids_B = [(100.30, 1), (99.90, 5)] # 在這裡賣出
paid, got_a = walk(asks_A, 3)
received, got_b = walk(bids_B, 3)
print("paid", paid, "for", got_a, "coins")
print("received", received, "for", got_b, "coins")
print("net", round(received - paid, 2))
print("average buy price", round(paid / got_a, 4), "average sell price", round(received / got_b, 4))
print(walk(asks_A, 10))
輸出:
paid 300.2 for 3 coins
received 300.1 for 3 coins
net -0.1
average buy price 100.0667 average sell price 100.0333
(500.6, 5)
最後一行顯示委託簿太薄時會發生什麼:你要求 10 枚幣,但委託簿只有 5 枚,所以得到 (500.6, 5):用 500.60 買到 5 枚幣。務必檢查 filled 是否等於你要的數量。
真實委託簿
你可以下載真實的委託簿。以下程式讀取一家公開交易所中比特幣的前五個價位。不需要帳戶或金鑰:
import json, urllib.request
req = urllib.request.Request("https://api.binance.com/api/v3/depth?symbol=BTCUSDT&limit=5",
headers={"User-Agent": "course"})
d = json.load(urllib.request.urlopen(req, timeout=10))
asks = [(float(p), float(q)) for p, q in d["asks"]]
bids = [(float(p), float(q)) for p, q in d["bids"]]
print("asks", asks[:3])
print("bids", bids[:3])
一次測試執行時,賣方開頭為 (75694.8, 4.63912),買方開頭為 (75694.79, 3.97826)。注意兩件事。價差是一美分,非常小。而最上方價位約有 4 枚幣,接下來的價位則遠少於 1 枚幣。交易 10 枚幣會吃到價格更差的價位。你的數字會不同。
兩個誠實提醒
- 委託簿是快照。等你的訂單抵達時,其他人可能已經拿走最上方價位。第 11 課會談這件事。
- 各交易所傳送委託簿的格式不同:價格可能是文字,數量可能以幣或合約表示。如同第 4 課,再次檢查單位。
讓 AI 代理人寫它並測試
一個模糊的要求:
加入委託簿支援。
一個內建檢查方式的要求:
撰寫 book.py,加入 walk(levels, amount),依最佳價格優先走訪各價位,回傳 (money, filled)。測試 1:賣方 [(100.00, 2), (100.20, 3)]、數量 3 必須回傳 (300.2, 3)。測試 2:買方 [(100.30, 1), (99.90, 5)]、數量 3 必須回傳 (300.1, 3)。測試 3:賣方數量 10 必須回傳 (500.6, 5)。接著加入一個函式,從公開 Binance depth 端點下載前 20 個價位,並以 0.1、1 和 10 BTC 的數量走訪,印出各自的平均價格。不要進行任何交易。
用計算機檢查三個測試數字。只有你重新計算過,代理人的程式碼才算正確。
你的練習
- 執行程式碼,確認第一段輸出和上面完全相同。
- 手動建立一個你自選的三個賣方價位委託簿。計算買入會用到三個價位的某個數量所需成本和平均價格,再用函式檢查。
- 以 0.1、1 和 10 BTC 執行真實委託簿函式。隨數量增加,平均價格上升多少?
- 把你的程式碼另存為新版本。
接下來
你現在能計算單一本委託簿上一筆交易的真實成本。一個循環有好幾個交易腿,每一腿都有自己的委託簿,也各有手續費。第 10 課會把它們結合起來,找出利潤最高的數量。