《自己动手做套利机器人》课程第 11 课。现在机器人知道该交易什么、交易多少。今天我们要检验所有计算背后隐藏的假设:计划能否按原样执行。
直接执行时的假设
到目前为止,机器人计算出的计划是:在 A 以 100.00 买入 1 个币,在 B 以 100.30 卖出 1 个币,扣手续费后利润为 0.0997(第 10 课)。最直接的执行方式是假设两笔订单都能立即按这些价格成交。这是第一版模拟通常采用的假设,也是为什么刚起步的机器人纸面表现出色,实盘却亏钱。本课接下来会逐一拆解这个假设。
五种会破坏计划的情况
- 延迟。 从你读取价格到订单抵达交易所,中间会经过你的程序、网络和交易所队列,可能耗费几十或几百毫秒。价格会在这段时间内变化。
- 两条交易腿,两个时刻。 你先买,再卖。第一笔成交后,第二笔的价格可能已经变化。在第二条交易腿完成前,你持有一个币,也承担着价格风险。这叫交易腿风险。
- 部分成交。 你下单 1 个币,结果只成交 0.6 个。另一条交易腿必须相应缩小,否则你会剩下未对冲的数量。
- 一条交易腿失败。 第二笔订单可能被拒绝:该交易所余额不足、触发价格限制、交易所宕机,或触及请求频率限制。现在你持有一个原本不想持有的币,只能按市场能给的价格卖掉。
- 其他机器人。 所有人都能看到这个价差。更快的机器人会在你之前拿走订单簿最优档。你的订单到达时,你依赖的档位已经不在了。通常只有价格朝不利方向变动时,你的订单才会成交,所以你的成交情况比平均水平更差。
可以运行的模拟
我们沿用第 10 课的计划,再加入两种影响。第一,卖出发生前,B 的买价会随机变动(服从正态分布,标准差以价格单位表示)。第二,卖出这条腿有一定概率失败,此时你会以 99.80 的价格在市场上抛出这个币。
import random
random.seed(11)
FEE = 0.001
ASK_A, BID_B = 100.00, 100.30 # 计划:在 A 买入 1 个币,在 B 卖出 1 个币
def one_trade(move_sigma, fail_chance, dump_price):
paid = ASK_A * (1 + FEE)
if random.random() < fail_chance: # 卖出这条腿失败:抛出这个币
return dump_price * (1 - FEE) - paid
bid_now = BID_B + random.gauss(0, move_sigma) # 买入期间买价发生了变动
return bid_now * (1 - FEE) - paid
def run(move_sigma, fail_chance, n=10000):
r = [one_trade(move_sigma, fail_chance, 99.80) for _ in range(n)]
return sum(r) / n, sum(x < 0 for x in r) / n
print("plan profit", round(BID_B * (1 - FEE) - ASK_A * (1 + FEE), 4))
for sig, fail in [(0, 0), (0.05, 0), (0.2, 0), (0.2, 0.1), (0.2, 0.3)]:
m, l = run(sig, fail)
print(f"move {sig:<5} fail {fail:<4} avg profit {m:+.4f} losing trades {l*100:.0f}%")
输出:
plan profit 0.0997
move 0 fail 0 avg profit +0.0997 losing trades 0%
move 0.05 fail 0 avg profit +0.0994 losing trades 2%
move 0.2 fail 0 avg profit +0.0994 losing trades 31%
move 0.2 fail 0.1 avg profit +0.0516 losing trades 38%
move 0.2 fail 0.3 avg profit -0.0512 losing trades 52%
逐行看看结果:
- 没有价格变动,也没有失败:计划利润是 +0.0997。
- 随机变动幅度为 0.05(利润的一半):平均利润几乎不变,但每 100 笔交易里已经有 2 笔亏钱。没有偏向的随机噪声平均后会抵消,这是最乐观的情况。
- 随机变动幅度为 0.2:平均利润一样,但每 100 笔里有 31 笔亏钱。每笔利润很小,因此看似无害的噪声也会让三成交易亏损。
- 第二条腿有 10% 的失败概率:平均利润减半,变成 +0.0516。一笔失败交易会损失约 0.40(花 100.10 买入,只收到 99.70),会抵消四笔盈利交易的利润。
- 失败概率为 30%:平均利润变成负数 −0.0512。即使每个单独计划看起来都能盈利,策略整体仍在亏钱。
手工核算失败成本:你花了 100.00 × 1.001 = 100.10。以 99.80 × 0.999 = 99.7002 的价格抛售。损失为 0.3998。失败概率为 30% 时,期望利润是 0.7 × 0.0997 − 0.3 × 0.3998 = −0.0501,与模拟结果 −0.0512 接近(差异来自 10 000 次试验中的随机波动)。
这个模拟刻意保持简单,但有一个让结果偏乐观的重要缺陷:价格变动没有方向偏差。真实市场里的成交并不是均匀抽样。价格朝有利方向变动时,其他机器人会抢先成交;价格对你不利时,你反而更容易成交。因此,真实结果会比“变动 0.2、失败 0”这一行更差。
机器人如何应对这些问题
- 延迟。 测量延迟,记录每一步的时间。让机器人靠近交易所运行(例如使用位于同一区域的服务器),并先做成本最低的检查。
- 交易腿风险。 尽量缩短暴露在风险中的时间:在两个交易所都预先持有余额,同时买入和卖出,不要先转币再交易。两笔订单一起发送,而不是一笔完成后再发送另一笔。
- 部分成交。 第一条腿成交后,读取实际成交数量,并让第二条腿按这个数量下单。
- 一条交易腿失败。 为剩下的币准备处理办法:带价格限制地按市价卖出,并把成本计入结果。像模拟中那样,把失败率加入利润估算。
- 其他机器人。 留出安全空间:只有预期利润明显大于零时才交易,而不是刚刚高于零。具体留多少,应根据你自己的测量结果设定。
让 AI 智能体编写并测试代码
一个含糊的请求:
把套利模拟做得更真实。
一个自带检查条件的请求:
根据第 11 课的代码编写 reality.py,包含 one_trade(move_sigma, fail_chance, dump_price) 和 run(...)。使用 random.seed(11)。第一行输出必须是 "plan profit 0.0997"。move 为 0.2、fail 为 0.3 的那一行必须显示平均利润为负数。添加一个参数 min_edge:计划利润低于 min_edge 时跳过交易。打印 min_edge 为 0.05 和 0.15 时每个计划交易的平均利润,并用两句话解释数字为什么不同。不要改动计划中的数字。
手工重新计算失败成本(0.3998),并与智能体输出的单笔失败交易成本对比。
练习
- 运行代码,确认输出与表格一致。
- 找出平均利润恰好为零的失败概率。手工计算:0.0997 × (1 − f) = 0.3998 × f,因此 f = 0.0997 / 0.4995,约为 0.20。用代码检查 fail 为 0.2 时的结果。
- 把抛售价格从 99.80 改成 99.95。盈亏平衡的失败概率如何变化?这说明迅速并接近市场价格卖掉剩余币有多大价值?
- 让价格变动只朝一个方向:每次从
bid_now减去 0.05(成交偏差)。重新运行并比较。这更接近真实市场。
接下来学什么
现在你知道计划只是起点,失败情况决定了平均结果。第 12 课会为机器人加上安全保护:用实时价格和虚拟资金进行纸面交易、为我们用到的每个数字准备配置文件,以及在糟糕的一小时造成损失前停止机器人的风险限制。