下面这道题可以抽象为跨交易所一价套利(跨所买入→另一所卖出),且每个交易所的一档挂单都带有按成交额计提的费率。思路分三步:
解题思路(算法)
1)费用模型与判定条件
-
若在交易所 \(A\) 以卖一价(ask)买入、费率 \(f_A\),单位成本
\(\text{cost}_A=\text{ask}_A\cdot(1+f_A)\)。 -
在交易所 \(B\) 以买一价(bid)卖出、费率 \(f_B\),单位到手收入
\(\text{rev}_B=\text{bid}_B\cdot(1-f_B)\)。 -
只有当
\[\text{rev}_B-\text{cost}_A>0 \]才存在扣费后的真实套利空间。把 \(\text{ppu}=\text{rev}_B-\text{cost}_A\) 叫做单位净差(per-unit profit)。
2)撮合与更新
- 对所有 \(A\neq B\) 的组合计算 \(\text{ppu}\),挑选单位净差最大的组合优先成交(贪心)。
- 成交量取两边可成交量的最小值 \(q=\min(q^{\text{bid}}_B,q^{\text{ask}}_A)\)。
- 成交后把两边的可成交量减去 \(q\),若一侧清零则从候选集合移除。
- 继续寻找下一笔 \(\text{ppu}>0\) 的组合,直到没有正利润的组合为止。
这样做满足你的第 2 点要求:“某一交易所套利完成后,减少挂单量,继续匹配下一个交易所,直到挂单量清 0”。
3)时间复杂度
- 共有 \(m\) 个卖出所(bids)与 \(n\) 个买入所(asks),每轮最多评估 \(m\times n\) 个组合,配合堆/排序维护最大 \(\text{ppu}\) 即可,整体规模很小。
代入你给的具体数据
原始一档(价格,数量)与费率(从 key 中拆出):
-
Bids(卖出侧)
ex1-0.00024: (0.95, 10)
ex2-0.0005 : (0.98, 10)
ex3-0.0002 : (1.00, 5)
ex4-0.00025: (1.02, 4)
ex5-0 : (0.94, 11) -
Asks(买入侧)
ex1-0.00024: (0.96, 50)
ex2-0.0005 : (1.03, 8)
ex3-0.0002 : (1.01, 2)
ex4-0.00025: (1.04, 5)
ex5-0 : (0.96, 3)
按照上面的规则计算、并按单位净差最大顺序撮合,得到 4 笔可盈利交易(价格单位同原始数据;费率为小数):
步骤 | 卖出所(Bid) | 卖价 | 卖费 | 买入所(Ask) | 买价 | 买费 | 成交量 | 单位净差 ppu | 本笔利润 |
---|---|---|---|---|---|---|---|---|---|
1 | ex4 | 1.02 | 0.00025 | ex5 | 0.96 | 0.0 | 3 | 0.0597450 | 0.1792350 |
2 | ex4 | 1.02 | 0.00025 | ex1 | 0.96 | 0.00024 | 1 | 0.0595146 | 0.0595146 |
3 | ex3 | 1.00 | 0.00020 | ex1 | 0.96 | 0.00024 | 5 | 0.0395696 | 0.1978480 |
4 | ex2 | 0.98 | 0.00050 | ex1 | 0.96 | 0.00024 | 10 | 0.0192796 | 0.1927960 |
- 总利润:\(0.1792350+0.0595146+0.1978480+0.1927960=\mathbf{0.6293936}\)。
撮合后的剩余挂单量
- Bids:ex1:10,ex2:0,ex3:0,ex4:0,ex5:11
- Asks:ex1:34,ex2:8,ex3:2,ex4:5,ex5:0
此时市场再也没有正的 \(\text{ppu}\):
\(\max \text{rev}_B = 0.95\cdot(1-0.00024)=0.949772\)(来自 ex1-bid);
\(\min \text{cost}_A = 0.96\cdot(1+0.00024)=0.9602304\)(来自 ex1-ask)。
因为 \(0.949772<0.9602304\),套利结束。
可直接复用的 Python 参考实现
# 输入:两字典 bids, asks;key="交易所-费率",value=[价格, 数量]
def parse_fee(k): return float(k.split('-')[1])
def to_side(d, side):out=[]for k,(p,q) in d.items():ex, fee = k.split('-')[0], parse_fee(k)out.append({'ex': ex, 'fee': fee, 'px': p, 'qty': q, 'side': side})return outdef unit_rev(bi): # 卖出侧到手return bi['px'] * (1 - bi['fee'])
def unit_cost(aj): # 买入侧成本return aj['px'] * (1 + aj['fee'])def best_pairs(bids, asks):cand=[]for bi in bids:if bi['qty']<=0: continuefor aj in asks:if aj['qty']<=0 or bi['ex']==aj['ex']: continueppu = unit_rev(bi) - unit_cost(aj)if ppu > 0:cand.append((ppu, min(bi['qty'], aj['qty']), bi, aj))cand.sort(key=lambda x:(x[0], x[1]), reverse=True)return canddef arbitrage(bids_dict, asks_dict):B, A = to_side(bids_dict, 'bid'), to_side(asks_dict, 'ask')trades, total = [], 0.0while True:pairs = best_pairs(B, A)if not pairs: breakppu, q, bi, aj = pairs[0]trades.append(dict(sell_ex=bi['ex'], sell_bid=bi['px'], sell_fee=bi['fee'],buy_ex=aj['ex'], buy_ask=aj['px'], buy_fee=aj['fee'],qty=q, ppu=ppu, profit=ppu*q))total += ppu*qbi['qty'] -= qaj['qty'] -= qreturn trades, total, B, A
把你题面给出的 bids
、asks
直接传给 arbitrage
即可得到上面同样的 4 笔成交与总利润 0.6293936,并且每一笔成交后数量都会按规则减少、直到无利可图为止。
备注与扩展
- 这里费率按双边各一次计提,且忽略充提/跨所转账时间与资金占用风险;真实交易需再考虑滑点、穿仓、撮合延迟、撤单失败、转账成本等。
- 若希望稳健而非贪心,可以把阈值设为 \(\text{ppu}>\epsilon\)(例如覆盖潜在滑点),或先以“单位净差/对手量”的综合指标排序。
- 若要支持多档深度,思路相同:把每档拆成一条记录,统一进入上述撮合循环即可。