模擬交易、設定與風險限額

套利課程第 12 課。讓機器人以即時價格和虛擬資金運作,把每個數字移入設定檔,並加入風險限額,在糟糕的一小時造成損失前停止機器人。附可核對輸出的簡短 Python 程式碼。

"打造你自己的套利機器人"課程第 12 課。這也為世界 3 收尾。你的機器人已經有掃描器、數量計算器和現實檢驗。今天要讓它可以安全運作。

最直接的做法

最直接的開始方式:把金鑰放進腳本,在函式裡硬編碼所有數字,手續費 0.001、數量 1 等,然後以真錢執行來 "看看會發生什麼事"。它會一直正常,直到第一次意外出現。接著三個問題都沒有好答案:我改了哪個數字?我虧了多少?它為什麼持續交易?

本課以三個工具逐一解決這三點。

工具 1:模擬交易

模擬交易是指整個機器人以即時價格運作、做出決策,並把訂單送到一個只記錄原本會發生什麼事的虛擬帳戶。資金不會移動。你會知道機器人如何運作,每個錯誤都不用花錢。

讓它有用的關鍵是:虛擬帳戶和真實交易所用戶端有相同的方法,這裡叫作 trade。機器人不知道正在和哪一個互動。之後切換到真實用戶端時,其他任何東西都不用改。

模擬帳戶也會稍微說謊,第 11 課已展示過:它會以你傳入的委託簿價格成交,沒有延遲,也沒有其他機器人。把它的利潤看作上限。真實結果會更低。

工具 2:設定檔

所有可能需要更改的數字都放在一個位置,於啟動時讀取。下面的程式碼把它寫成程式內的 JSON 文字,因此可以直接執行。在你的機器人中,它會是一個 config.json 檔案。

  • 不碰邏輯也能修改數字。
  • 可以在每次執行旁儲存設定,之後知道哪套設定得到哪個結果。
  • 數字錯誤只要在一行中找出。

金鑰和機密絕不放進版本庫中的設定檔。請放在環境變數。

工具 3:風險限額

風險限額是一條規則,當某件事看來不對時會停止機器人,不管機器人相信什麼。四種基本限額:

  • 單筆最大數量。要求 1000 枚而不是 1 枚的錯誤會在此處被攔住。
  • 最小 edge:計畫利潤低於安全邊際時不交易,原因見第 11 課。
  • 一個期間內的最多交易次數。失控迴圈會撞上它。
  • 每日最大損失。當損失越過限額時,機器人停止,只有人可以重新啟動。它就是緊急停止開關。

程式碼

import json

CONFIG = json.loads("""{
  "fee": 0.001,
  "min_edge": 0.05,
  "max_size": 1.0,
  "max_trades": 20,
  "max_daily_loss": 0.50
}""")

def walk(levels, amount):
    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

def plan_profit(asks, bids, q, fee):
    """Profit after fees for size q, or None if a book is too thin."""
    paid, got_a = walk(asks, q)
    received, got_b = walk(bids, q)
    if got_a < q or got_b < q:
        return None
    return received * (1 - fee) - paid * (1 + fee)

class PaperAccount:
    """A fake account. A real exchange client would have the same trade() method."""
    def __init__(self):
        self.pnl, self.trades = 0.0, 0
    def trade(self, asks, bids, q, fee):
        profit = plan_profit(asks, bids, q, fee)
        self.pnl += profit
        self.trades += 1
        return profit

class Guard:
    def __init__(self, cfg):
        self.cfg, self.stopped = cfg, False
    def check(self, account, size, edge):
        if self.stopped:
            return "stopped"
        if account.pnl <= -self.cfg["max_daily_loss"]:
            self.stopped = True
            return "daily loss limit"
        if account.trades >= self.cfg["max_trades"]:
            return "trade limit"
        if size > self.cfg["max_size"]:
            return "size above limit"
        if edge is None or edge < self.cfg["min_edge"]:
            return "edge below minimum"
        return "ok"

asks = [(100.00, 2), (100.20, 3)]
bids = [(100.30, 1), (99.90, 5)]
account, guard = PaperAccount(), Guard(CONFIG)
for size in [1.0, 1.0, 1.5, 0.2]:
    edge = plan_profit(asks, bids, size, CONFIG["fee"])
    verdict = guard.check(account, size, edge)
    profit = account.trade(asks, bids, size, CONFIG["fee"]) if verdict == "ok" else None
    print(f"size {size:<4} edge {edge:+.4f} -> {verdict:20} profit {profit if profit is None else round(profit, 4)}")
print("pnl", round(account.pnl, 4), "trades", account.trades)

print("loss streak:")
account, guard = PaperAccount(), Guard(CONFIG)
for i in range(6):
    account.pnl -= 0.20
    print(i, round(account.pnl, 2), guard.check(account, 1.0, 0.1))

輸出:

size 1.0  edge +0.0997 -> ok                   profit 0.0997
size 1.0  edge +0.0997 -> ok                   profit 0.0997
size 1.5  edge -0.0502 -> size above limit     profit None
size 0.2  edge +0.0199 -> edge below minimum   profit None
pnl 0.1994 trades 2
loss streak:
0 -0.2 ok
1 -0.4 ok
2 -0.6 daily loss limit
3 -0.8 stopped
4 -1.0 stopped
5 -1.2 stopped

看看輸出。

  • 前兩筆交易通過所有檢查並被記錄:每筆 0.0997,合計 0.1994。可以手動核算 0.0997,參見第 10 課。
  • 數量 1.5 被數量限額阻止,它也會虧錢:-0.0502。
  • 數量 0.2 的 edge 為正,0.0199,但低於最小 edge 0.05,因此機器人跳過它。微小利潤不值得承擔第 11 課裡的風險。
  • 在連續虧損中,機器人每步損失 0.20。保護機制在第三步觸發,此時累計為 -0.6。限額是 0.50,損失多出它 0.10,因為檢查在每筆交易之前執行,只看到已經產生的損失。限額可以止損,但不能阻止最後一筆交易。把它設得低於你實際承受得起的損失。
  • 在 "daily loss limit" 之後,保護機制會永遠回答 "stopped"。只有人工重新啟動可以清除它。這是刻意設計的。

開始真實交易之前

合理的順序,規則是只有前一步已經變得乏味時,才往下一步走:

  1. 至少一週的模擬交易,保留日誌。把它聲稱的利潤和第 11 課的估算相比。
  2. 真實交易所、極小數量,交易所的最低值,幾美元的損失限額。目標是測試連線、訂單和手續費,不是獲利。
  3. 只有真實結果持續與模擬結果一致時,才慢慢提高數量。

如果真實結果遠差於模擬結果,差異就是你的真實成本:延遲、失敗的一腿、其他機器人。提高數量前,找出是哪一項。

讓 AI 代理撰寫並測試它

模糊的請求:

為我的機器人加入安全措施。

有檢查的請求:

撰寫 safe.py,包含第 12 課的 plan_profit、PaperAccount 和 Guard,從 config.json 讀取設定(fee 0.001, min_edge 0.05, max_size 1.0, max_trades 20, max_daily_loss 0.50)。以 asks [(100.00, 2), (100.20, 3)] 和 bids [(100.30, 1), (99.90, 5)] 測試:數量 1.0、1.0、1.5、0.2 必須得到 ok、ok、size above limit、edge below minimum,最終 pnl 必須為 0.1994。測試 2:每步損失 0.20 時,必須在第 2 步印出 "daily loss limit",之後印出 "stopped"。只從環境變數讀取 API 金鑰,絕不印出它們。不得連線到任何交易所。

接受代理的成果前,先用計算機核算 pnl 0.1994。

你的練習

  1. 執行程式碼並與上面的輸出比較。
  2. 把設定移到真正的 config.json 檔案並用 json.load 讀取。將 min_edge 改為 0.10,看看哪些交易被跳過。
  3. 修改保護機制,讓它在每筆交易後檢查損失,以便它在第一筆越過限額的交易後停止。超出的部分消失了嗎?不完全是:最後一筆交易仍然發生。
  4. 新增 "最大未平倉部位" 限額:如果機器人在一次失敗的一腿後同時持有超過 2 枚幣,它就停止。

下一步

世界 3 已完成:你擁有一個完整的機器人,它能發現機會、計算數量、模擬並保護。世界 4 讓它更快、範圍更廣:第 13 課從速度開始,解釋 Python 為什麼變慢、可以怎麼處理,以及 Rust 如何加入其中。