3. 盘口策略对滑点参数的敏感性扫描
扫描对象与参数空间设计
本节固定策略逻辑不变:以买一/卖一中间价为锚,按固定价差挂双边限价单,持仓上限 1 手,成交后按对手价平仓。唯一变化的是成交模拟中的滑点模型。为了让扫描结果可比较,先定义三类滑点模型的参数化方式,并统一以“每笔成交的额外成本”计入净收益。
- 固定跳数模型:每笔成交额外扣除
k个最小变动价位(tick),k ∈ {0, 0.5, 1, 2, 3, 5}。适合刻画流动性稳定、冲击可忽略的场景。 - 比例模型:按成交名义金额的
r比例扣除,r ∈ {0, 0.5, 1, 2, 3, 5} bps。适合刻画成本随成交金额线性放大的场景。 - 冲击成本模型:
cost = α·(Q/V)^β,其中Q为本次成交量,V为对应档位可见量,α为冲击系数(bps),β为非线性指数。扫描α ∈ {1, 2, 5, 10, 20} bps,β ∈ {0.5, 1.0, 1.5}。
统一约定:tick 取 0.01 元,标的参考价 10.00 元,则 1 tick ≈ 10 bps。所有滑点均从每笔平仓盈亏中扣除,成交率定义为“实际成交笔数 / 挂单笔数”。
扫描实现:一次运行覆盖全部参数组合
下面代码在固定策略信号序列上,对三类模型分别做参数扫描。策略信号与成交判定(是否触及挂单价)在扫描前一次性生成,滑点只影响成本与净收益,从而保证不同参数下成交率差异仅来自滑点模型对“可成交性”的建模差异(冲击模型下当成本超过预期价差时视为放弃成交)。
import numpy as np
TICK = 0.01
MID = 10.00
POS_LIMIT = 1
# 固定策略:以中间价为锚,双边挂单,价差 1 tick
def build_orders(snapshots):
orders = []
for s in snapshots:
bid = round(s["mid"] - TICK, 2)
ask = round(s["mid"] + TICK, 2)
orders.append({"bid": bid, "ask": ask, "mid": s["mid"],
"bid_vol": s["bid_vol"], "ask_vol": s["ask_vol"]})
return orders
# 成交判定:下一快照最低价触及买单价 / 最高价触及卖单价
def match_fills(orders, snapshots):
fills = []
for i, o in enumerate(orders[:-1]):
nxt = snapshots[i + 1]
if nxt["low"] <= o["bid"]:
fills.append({"side": "buy", "px": o["bid"], "qty": 1,
"vis": o["bid_vol"], "mid": o["mid"]})
if nxt["high"] >= o["ask"]:
fills.append({"side": "sell", "px": o["ask"], "qty": 1,
"vis": o["ask_vol"], "mid": o["mid"]})
return fills
def slip_fixed(fill, k):
return k * TICK * fill["qty"]
def slip_ratio(fill, r_bps):
return fill["px"] * fill["qty"] * r_bps / 1e4
def slip_impact(fill, alpha_bps, beta):
ratio = fill["qty"] / max(fill["vis"], 1e-9)
return fill["px"] * fill["qty"] * alpha_bps / 1e4 * (ratio ** beta)
def run_scan(fills, n_orders):
rows = []
for k in [0, 0.5, 1, 2, 3, 5]:
cost = sum(slip_fixed(f, k) for f in fills)
rows.append(("fixed", f"k={k}", cost, len(fills) / n_orders))
for r in [0, 0.5, 1, 2, 3, 5]:
cost = sum(slip_ratio(f, r) for f in fills)
rows.append(("ratio", f"r={r}bps", cost, len(fills) / n_orders))
for a in [1, 2, 5, 10, 20]:
for b in [0.5, 1.0, 1.5]:
kept, cost = 0, 0.0
for f in fills:
c = slip_impact(f, a, b)
# 冲击成本超过 1 tick 预期价差则放弃成交
if c <= TICK * f["qty"]:
kept += 1
cost += c
rows.append(("impact", f"a={a},b={b}", cost, kept / n_orders))
return rows
运行后得到一张长表,字段为 model / param / total_cost / fill_rate。为便于识别脆弱区间,再计算每个参数点相对无滑点基准的净收益衰减率:decay = total_cost / gross_pnl,其中 gross_pnl 为未扣滑点的平仓盈亏合计。
结果判读:脆弱区间在哪里
下表为教学示例数据(假设 gross_pnl = 420 元、挂单 800 笔、基准成交 512 笔),用于说明判读方法,非真实回测结果。
| 模型 | 参数 | 滑点成本(元) | 成交率 | 收益衰减 | 判读 |
|---|---|---|---|---|---|
| 固定跳数 | k=0.5 | 2.6 | 64.0% | 0.6% | 稳健 |
| 固定跳数 | k=2 | 10.2 | 64.0% | 2.4% | 稳健 |
| 固定跳数 | k=5 | 25.6 | 64.0% | 6.1% | 可接受 |
| 比例 | r=2bps | 10.2 | 64.0% | 2.4% | 稳健 |
| 比例 | r=5bps | 25.6 | 64.0% | 6.1% | 可接受 |
| 冲击 | a=5,b=1.0 | 18.4 | 58.3% | 4.4% | 成交率开始下滑 |
| 冲击 | a=10,b=1.5 | 41.7 | 41.2% | 9.9% | 脆弱区间 |
| 冲击 | a=20,b=1.5 | 63.5 | 27.6% | 15.1% | 失效区间 |
判读要点:固定跳数与比例模型下成交率不变,说明策略对“线性成本”不敏感,脆弱性主要来自冲击模型的非线性项。当 α·(Q/V)^β 超过 1 tick 预期价差时,成交被主动放弃,成交率骤降,收益衰减加速。因此脆弱区间的识别阈值可定为:成交率相对基准下降超过 20% 或收益衰减超过 8%,即进入需重点关注的参数区域。
注意:冲击模型中的“放弃成交”规则会改变成交率,这与固定/比例模型仅改变成本不同。比较三类模型时应分别看“成本维度”和“成交率维度”,不要只用一个综合指标掩盖差异。
可执行检查清单
- 扫描前确认策略信号与成交判定已冻结,滑点只作用于成本项,避免参数间相互污染。
- 固定跳数与比例模型下成交率应保持不变;若发生变化,说明成交判定被滑点逻辑误改。
- 冲击模型需明确“放弃成交”阈值,并记录被放弃的笔数,否则成交率下降无法归因。
- 以基准 gross_pnl 为分母计算衰减率,避免不同参数下分母漂移导致误判。
- 脆弱区间判定同时看成交率降幅与收益衰减,任一超阈值即标记为需关注参数点。