137S 策略 · 平仓尾部风险改进说明
文件:scripts/run_pyramid_live.py · 改动日期:2026-04-23 · 版本:CLOSE_ALL v2
一、背景与风险识别
当策略权益规模达到 $20M(2000万) 量级时,BTC 持仓可能超过 100 BTC。
在此规模下,原始 CLOSE_ALL 实现存在三项系统性缺陷,可能导致止损完全失效。
1.1 原始代码(改前)
# run_pyramid_live.py — 原 CLOSE_ALL 分支(已废弃)
elif action == PyramidAction.CLOSE_ALL:
# 问题①:滑点探盘固定用 1.0 BTC,无论实际持仓多大
limit_px, best_px, actual_slip = _get_order_price(
info, "BTC", is_buy=False, size=1.0, ← 实际可能 100 BTC
slippage_buffer=slippage, max_slippage=max_slippage,
)
# 问题②:sz=0.0 表示单笔全平,IOC 未成交则全部留仓
exchange.order("BTC", is_buy=False, sz=0.0, limit_px=limit_px,
order_type={"limit": {"tif": "Ioc"}}, reduce_only=True)
# 问题③:插针时 SlippageTooHighError → 直接返回 ok=False,仓位原地不动
except SlippageTooHighError as exc:
return ExecResult(ok=False, detail=f"slippage_too_high_close: {exc}")
1.2 三项核心缺陷
| # | 缺陷 | 在 $20M 下的实际后果 |
| ① |
滑点按 1 BTC 评估 |
100 BTC 的真实市场冲击被严重低估;实际挂单价远离市场,IOC 必定大部分未成交 |
| ② |
单笔 sz=0.0 全平 |
一次性挂出 100 BTC 卖单,流动性不足时绝大部分取消,止损形同虚设 |
| ③ |
插针直接放弃 |
插针(价格急跌)正是最需要平仓的时刻,代码却返回失败、仓位悬空,损失持续扩大 |
极端场景测算:权益 $20M、10x 杠杆、持仓 ~100 BTC。BTC 单日下跌 15% 时,
爆仓线约在 −10%(即 $54K→$49K)。插针可能在单根 4H K线内触穿止损位,交易所强平先于策略响应。
此时若 CLOSE_ALL 再失败,损失可能超出止损预期数倍。
二、改进方案:自适应拆单循环 NEW v2
2.1 设计原则
| 原则 | 实现方式 |
| 按实际仓位拆单 | 每轮先调用 info.user_state() 查询交易所真实持仓,再计算 clip 大小 |
| 滑点评估匹配实际大小 | _get_order_price(size=clip),用真实 clip BTC 数探盘 |
| 插针不等待、直接市价 | SlippageTooHighError → 立即 market_close(sz=clip),无任何等待 |
| 多层兜底 | 连续 3 次市价兜底 → 强制全清;超 10 轮 → 最终 market_close() |
2.2 关键参数
| 常量 | 值 | 说明 |
CLIP_FRAC | 25% | 每轮平掉当前剩余仓位的 1/4 |
CLIP_MAX_BTC | 10 BTC | 单笔上限,控制单次市场冲击 |
CLIP_MIN_BTC | 0.01 BTC | 低于此视为已清仓,退出循环 |
MAX_ROUNDS | 10 | 最大尝试轮数(4 轮理论上可清 68%,10 轮可清 94%) |
INTER_SLEEP | 2 秒 | 正常 clip 间隔;插针路径跳过等待 |
MKT_THRESH | 3 次 | 连续 N 次市价兜底后触发强制全清 |
WIDE_SLIP | max(slippage×3, 1.5%) | 市价兜底宽容滑点,确保成交优先于价格 |
2.3 新版逻辑流程
# run_pyramid_live.py — CLOSE_ALL v2(自适应拆单)
for round_i in range(10): # MAX_ROUNDS = 10
# Step 1:查询交易所实际持仓
szi = _ca_query_pos() # info.user_state() → assetPositions[BTC].szi
remaining = abs(szi)
if remaining < 0.01: break # 已清仓,退出
# Step 2:按实际仓位计算 clip
clip = min(remaining * 0.25, 10.0) # 25%,最多 10 BTC
is_buy = szi < 0 # 空头→买入平仓,多头→卖出平仓
# Step 3:尝试 IOC 限价单(滑点按 clip 实际大小评估)
try:
limit_px, _, _ = _get_order_price(size=clip, ...) ← 用真实 clip,非 1 BTC
exchange.order("BTC", sz=clip, tif="Ioc", reduce_only=True)
except SlippageTooHighError: # ← 插针保护
# 不等待 30 秒!立即降级为市价单
exchange.market_close("BTC", sz=clip, slippage=0.015)
consecutive_mkt += 1
# Step 4:连续 3 次市价兜底 → 强制全清
if consecutive_mkt >= 3:
exchange.market_close("BTC", slippage=0.015) # 全部剩余
_notify("插针强平 | N次市价兜底")
break
time.sleep(2)
else: # 10 轮耗尽
exchange.market_close("BTC", slippage=0.015) # 最终兜底
_notify("平仓超限 | 10轮未清仓")
return ExecResult(ok=False, ...)
三、改前 vs 改后对比
| 场景 | 改前行为 | 改后行为 |
| $20M 大仓位正常平仓 |
sz=0.0 单笔挂出 100 BTC,IOC 部分成交,剩余留仓 |
分 4~10 轮,每轮 ≤10 BTC,每轮前查实际仓位 |
| 滑点评估 |
固定用 1 BTC 探盘,100 BTC 实际冲击被忽略 |
每 clip 用真实大小探盘,评估准确 |
| 插针(价格急跌) |
SlippageTooHighError → ok=False,仓位原地不动 |
立即市价单出局,零等待 |
| IOC 未成交 |
无补救,止损失效 |
下一轮重查仓位、继续尝试 |
| 连续失败 |
没有重试机制 |
连续 3 次降级 → 强制市价清仓 + 邮件告警 |
| 程序卡死 / 极端行情 |
无保底 |
10 轮上限触发兜底 market_close() + 告警 |
| 普通下单的 30s 等待 |
CLOSE_ALL 中也有 30s 等待 |
30s 等待仅保留在普通限价路径;CLOSE_ALL 全程无等待 |
四、告警通知
| 触发条件 | 邮件主题 | 含义 |
| 连续 3 次市价兜底 |
插针强平 | N次市价兜底 | reason |
插针期间多次滑点超限,已强制市价全清剩余仓位 |
| 10 轮未能清仓 |
平仓超限 | 10轮未清仓 | reason |
极端行情下循环耗尽,已触发最终兜底市价单,需人工核查 |
五、日志示例
# 正常情况(4 轮平掉 $20M 仓位)
close_all reason=hard_stop_limit
close_all[0] szi=102.43500 clip=10.00000 is_buy=False
best=84320.0 lim=84235.7 slip=0.0010%
close_all[1] szi=92.43500 clip=10.00000 is_buy=False
...
close_all[4] cleared (remaining=0.00000)
# 插针情况(自动降级市价)
close_all[2] szi=72.43500 clip=10.00000 is_buy=False
slip_too_high (slippage 0.8200% > max 0.5000%), market fallback immediately
→ market_close(sz=10.0, slippage=0.015) 执行
# 连续 3 次插针(强制全清)
close_all: 3 consec mkt orders, force market_close all
→ 邮件告警: 插针强平 | 3次市价兜底
六、遗留风险(未覆盖)
爆仓间隙风险:若 BTC 在单根 4H K线内下跌 >10%(触穿 10x 杠杆爆仓线),
交易所强平早于策略 60s 轮询触发。此为平台级风险,非程序层面可完全防御。
建议在权益超过 $5M 后将 base_leverage 降至 7x 或启用 adaptive_leverage。
API 断线:若 info.user_state() 持续超时,_ca_query_pos()
返回 0 → 循环提前退出。可在监控层增加独立的持仓健康检查(heartbeat)。