4. 盘口策略对手续费与返佣结构的压力测试

费率结构建模:把手续费与返佣拆成可扫描的参数

盘口策略的净收益对费率极其敏感,因为单笔毛利往往只有 1~2 个最小报价单位。压力测试的第一步不是跑策略,而是把"手续费 + 返佣"抽象成一组可独立扫描的参数,并明确它们作用在成交的哪一侧。

本例采用如下费率模型(示例参数,非实测):

  • taker 费率 f_t:主动吃单成交时按成交金额比例收取,基准 0.02%(2 bp)。
  • maker 费率 f_m:被动挂单成交时收取,基准 0.00%,即零费。
  • maker 返佣 r_m:被动成交后按成交金额返还,基准 +0.01%(1 bp),即净收 1 bp。
  • 单笔净费率:主动腿 -f_t,被动腿 r_m - f_m。一次完整往返(一主一被)的净费率 = r_m - f_m - f_t。

基准情景下往返净费率为 0.01% - 0% - 0.02% = -0.01%,即每往返一次净付出 1 bp。这个数字必须与策略的平均单笔毛利对比,才能判断生存空间。

往返成交的费率构成(基准情景) 主动腿(taker) 成交金额 × f_t f_t = 0.02% 支出 −2 bp 被动腿(maker) 返佣 r_m = +0.01% 手续费 f_m = 0.00% 收入 +1 bp 往返净费率 r_m − f_m − f_t = +1 − 0 − 2 −1 bp / 往返 压力扫描的三个维度 维度 A:taker 费率上调 0.02% → 0.03% → 0.05% 直接压缩主动腿毛利 影响:往返净费率 −2 → −4 bp 维度 B:返佣取消/下调 +1 bp → 0 → −0.5 bp 被动腿由收入变成本 影响:往返净费率 −1 → −3 bp 维度 C:组合情景 taker 上调 + 返佣取消 最不利组合 影响:往返净费率 −5 bp

策略侧的关键耦合:换手频率决定费率侵蚀总量

单笔净费率只是"单价",真正决定净收益的是费率 × 换手次数。同一策略在费率上调后,除了单笔成本上升,还可能因为信号阈值相对变差而改变成交节奏,因此必须同时记录换手频率,否则会误判策略的生存能力。

本例用一个最小可运行的盘口策略回放框架来量化这一点。策略逻辑固定:当买一价与卖一价之差(点差)大于阈值时,在买一价挂被动买单、在卖一价挂被动卖单;成交后立即用主动单平掉库存。这样每完成一次"被动开仓 + 主动平仓"就构成一次往返,便于把费率直接映射到每笔盈亏。

import numpy as np

# ---------- 费率情景定义(示例参数) ----------
SCENARIOS = {
    "基准":        {"f_t": 0.0002, "f_m": 0.0,    "r_m": 0.0001},
    "taker上调":   {"f_t": 0.0003, "f_m": 0.0,    "r_m": 0.0001},
    "taker大幅上调":{"f_t": 0.0005, "f_m": 0.0,    "r_m": 0.0001},
    "返佣取消":    {"f_t": 0.0002, "f_m": 0.0,    "r_m": 0.0},
    "返佣转负":    {"f_t": 0.0002, "f_m": 0.0,    "r_m": -0.00005},
    "最不利组合":  {"f_t": 0.0005, "f_m": 0.0,    "r_m": 0.0},
}

def run_strategy(snapshots, fee, spread_threshold=0.02, max_rounds=200):
    """snapshots: list of dict(bid, ask, mid)
    返回:往返次数、毛收益、净收益、平均单笔毛利"""
    gross = 0.0
    rounds = 0
    for s in snapshots:
        if rounds >= max_rounds:
            break
        bid, ask = s["bid"], s["ask"]
        spread = ask - bid
        if spread <= spread_threshold:
            continue
        # 被动腿:买一价买入,卖一价卖出(各成交一半库存,简化为一次往返)
        # 毛收益 = 点差(以中间价为基准的比例)
        mid = (bid + ask) / 2
        gross_pnl = spread / mid          # 单次往返毛收益率
        # 净费率:被动腿收入 r_m - f_m,主动腿支出 f_t
        net_fee = (fee["r_m"] - fee["f_m"]) - fee["f_t"]
        net_pnl = gross_pnl + net_fee
        gross += gross_pnl
        rounds += 1
        # 记录单笔净收益用于后续统计
        s["_net"] = net_pnl
    return rounds, gross, gross + rounds * ((fee["r_m"] - fee["f_m"]) - fee["f_t"])

# ---------- 构造示例行情切片(非实测,仅用于演示) ----------
rng = np.random.default_rng(42)
snapshots = []
for _ in range(500):
    mid = 100.0 + rng.normal(0, 0.05)
    half_spread = abs(rng.normal(0.015, 0.008))
    snapshots.append({"bid": mid - half_spread, "ask": mid + half_spread, "mid": mid})

# ---------- 扫描 ----------
print(f"{'情景':<12}{'往返次数':>8}{'毛收益(bp)':>12}{'净收益(bp)':>12}{'单笔净(bp)':>12}")
for name, fee in SCENARIOS.items():
    rounds, gross, net = run_strategy(snapshots, fee)
    per = (net / rounds * 1e4) if rounds else 0
    print(f"{name:<12}{rounds:>8}{gross*1e4:>12.2f}{net*1e4:>12.2f}{per:>12.2f}")

运行这段代码会得到一张按情景排列的对照表。关键判读点有三个:

  1. 往返次数是否变化:本例策略的入场条件只依赖点差,不依赖费率,所以往返次数在各情景下应保持一致。如果实际策略的入场阈值包含费率或预期收益,往返次数会随费率上升而下降——这本身就是"策略自我收缩"的信号,必须单独记录。
  2. 单笔净收益是否转负:当 单笔净(bp) < 0 时,策略在该费率下每做一笔都在亏钱,属于"结构性死亡",不是靠提高换手能救回来的。
  3. 净收益对费率的弹性:比较"基准"与"最不利组合"的净收益差额,除以费率差额,得到每 1 bp 费率变化对应的净收益变化,用于判断策略的费率敏感度。
把 spread_threshold 从 0.02 调到 0.03 再跑一遍,可以观察"提高入场门槛"能否抵消费率上调。如果提高门槛后往返次数骤降、净收益反而更差,说明该策略的毛利来源高度依赖高频小额点差,费率上调对其是致命的。

生存能力判定:用盈亏平衡费率反推安全边际

比"跑几个情景看结果"更有决策价值的是:直接求出盈亏平衡的 taker 费率,即令单笔净收益为零时的 f_t。由 gross_pnl + (r_m − f_m) − f_t = 0 可得:

f_t* = gross_pnl + r_m − f_m

把基准情景下的平均单笔毛利代入,就能得到策略能承受的 taker 费率上限。当前费率与 f_t* 之间的差距就是安全边际。若安全边际小于 1 bp,说明费率只要微调就会击穿策略,属于高风险暴露。

情景taker 费率返佣往返净费率单笔净收益(示例)判定
基准0.02%+0.01%−1 bp正可生存
taker 上调0.03%+0.01%−2 bp接近零边际
taker 大幅上调0.05%+0.01%−4 bp负不可生存
返佣取消0.02%0−2 bp接近零边际
返佣转负0.02%−0.005%−2.5 bp负不可生存
最不利组合0.05%0−5 bp显著负不可生存
表中"单笔净收益"列的具体数值取决于示例行情切片的点差分布,属于演示性结果,不能当作真实回测结论。实际使用时必须替换为真实盘口快照,并确认快照的时间戳与撮合规则一致,否则费率结论会被脏数据放大或掩盖。

交付物:费率压力测试记录表

为了让结论可复核、可对比,建议每次压力测试都填写一张固定字段的记录表。字段只保留与费率决策直接相关的项,避免引入无关的管理指标。

字段填写说明示例值
行情切片来源数据文件、时间范围、快照频率示例切片,500 条,1s 快照
策略版本策略逻辑标识,确保各情景用同一版本spread_maker_v1
入场阈值点差阈值,单位与行情一致0.02
费率情景f_t / f_m / r_m 三元组0.0005 / 0 / 0
往返次数该情景下实际完成的往返数187
单笔净收益净收益 / 往返次数,单位 bp−3.2 bp
盈亏平衡 f_t*由平均单笔毛利反推0.028%
安全边际f_t* − 当前 f_t−0.022%(已击穿)
判定可生存 / 边际 / 不可生存不可生存

完成这张表后,策略在费率上调情景下的生存能力就有了可比较的量化结论:不是"感觉费率高了会亏",而是"当 taker 费率超过 0.028% 时,该策略的单笔净收益转负,且提高入场阈值无法挽回"。这个结论可以直接用于决定是否调整策略的成交方式(例如减少主动腿、改为双边被动成交),或是否需要在费率谈判中争取返佣。