限价单信号价格约束优化设计方案
限价单信号价格约束优化设计方案
1. 背景与问题
当前系统的限价单价格完全基于下单时刻的 L2 订单簿计算(_calculate_limit_price()),未参考信号生成时的价格。在信号生成到实际下单的间隔内,市场价格可能不利移动:
- 买入场景:信号价 $100,下单时 bid 涨到 $102 → 限价 ~$102,比信号多付 2%
- 卖出场景:信号价 $100,下单时 ask 跌到 $98 → 限价 ~$98,比信号少收 2%
目标:挂单价格不能比信号价格更差,确保开仓成本不劣于信号时刻的市场价。
2. 当前限价定价流程
信号生成 (orchestrator.on_entry_signal)
│ latest_alt_price = 信号时刻的市场价
│ l2_snapshot = 信号时刻的 L2 订单簿快照
↓
开仓决策 (position_manager.open_position)
↓
限价开仓 (executor.limit_open)
├─ Leg B 预检查: _calculate_limit_price(coin, is_buy, l2_snapshot)
└─ Leg A 下单: _place_limit_order(coin, is_buy, size, l2_snapshot)
└─ _calculate_limit_price(coin, is_buy, l2_snapshot)
└─ 买入: best_bid * (1 + offset)
└─ 卖出: best_ask * (1 - offset)
_calculate_limit_price() 当前逻辑 (executor.py:466-514):
def _calculate_limit_price(self, coin, is_buy, l2_snapshot=None):
# 1. 从 L2 获取 best_bid, best_ask
# 2. effective_offset = min(spread_pct, limit_price_offset_pct)
# 3. 买入: price = best_bid * (1 + effective_offset)
# 卖出: price = best_ask * (1 - effective_offset)
return self.round_price(coin, price)
问题在于:L2 快照虽然是信号时刻缓存的,但如果快照无效会降级到 API 实时查询,且即便使用快照,价格也可能在极短时间内变化。更关键的是,当前没有任何机制保证最终挂单价格不劣于信号价格。
3. 设计方案
3.1 核心思路
在 _calculate_limit_price() 中新增信号价格约束(Signal Price Clamp):
- 买入:
price = min(l2_price, signal_price)— 不比信号时更贵 - 卖出:
price = max(l2_price, signal_price)— 不比信号时更便宜
3.2 适用范围
| 场景 | 是否约束 | 原因 |
|---|---|---|
| 开仓 (limit_open) | 是 | 信号价格 latest_alt_price 可用,保护开仓成本 |
| 平仓 (limit_close) | 否 | 平仓优先成交速度,已有超时转市价兜底 |
| 市价单 (market_open/close) | 否 | 市价单由 SDK slippage 参数控制 |
3.3 新增配置项
| 配置字段 | 环境变量 | 默认值 | 说明 |
|---|---|---|---|
signal_price_clamp_enabled |
TRADING_SIGNAL_PRICE_CLAMP_ENABLED |
true |
是否启用信号价格约束 |
signal_price_max_deviation_pct |
TRADING_SIGNAL_PRICE_MAX_DEVIATION_PCT |
0.03 |
L2 价格与信号价格偏差超过此阈值时输出 WARNING |
4. 修改文件清单
4.1 src/trading/config.py — 新增配置字段
在限价单配置区域(limit_order_timeout 之后)新增:
# 信号价格约束
signal_price_clamp_enabled: bool = True # 开仓限价不劣于信号价格
signal_price_max_deviation_pct: float = 0.03 # 偏差超过 3% 输出告警
在 load_trading_config() 中新增环境变量加载:
signal_price_clamp_enabled=os.getenv("TRADING_SIGNAL_PRICE_CLAMP_ENABLED", "true").lower() == "true",
signal_price_max_deviation_pct=_env_float("TRADING_SIGNAL_PRICE_MAX_DEVIATION_PCT", "0.03"),
4.2 src/trading/executor.py — 3 处修改
(a) _calculate_limit_price() — 新增 signal_price 参数 + clamp 逻辑
方法签名变更:
def _calculate_limit_price(
self,
coin: str,
is_buy: bool,
l2_snapshot: dict | None = None,
signal_price: float | None = None, # 新增:信号价格约束
) -> float:
在 return self.round_price(coin, price) 前插入约束逻辑:
# 信号价格约束: 限价不能比信号生成时的价格更差
if (signal_price is not None and signal_price > 0
and self._config.signal_price_clamp_enabled):
original_price = price
if is_buy:
price = min(price, signal_price) # 买入不超过信号价
else:
price = max(price, signal_price) # 卖出不低于信号价
if price != original_price:
deviation_pct = abs(original_price - signal_price) / signal_price
logger.info(
f"信号价格约束生效: {coin} {'买入' if is_buy else '卖出'} | "
f"L2=${original_price:.6f} → 约束后=${price:.6f} | "
f"信号价=${signal_price:.6f} | 偏差={deviation_pct:.2%}"
)
if deviation_pct > self._config.signal_price_max_deviation_pct:
logger.warning(
f"信号价格偏差过大: {coin} | 偏差={deviation_pct:.2%} > "
f"阈值={self._config.signal_price_max_deviation_pct:.2%}"
)
(b) _place_limit_order() — 透传 signal_price
方法签名新增 signal_price: float | None = None,并传递给 _calculate_limit_price():
def _place_limit_order(
self, coin: str, is_buy: bool, size: float, reduce_only: bool = False,
l2_snapshot: dict | None = None,
signal_price: float | None = None, # 新增
) -> OrderResult:
...
price = self._calculate_limit_price(coin, is_buy, l2_snapshot, signal_price)
...
(c) limit_open() — 传入 signal.latest_alt_price
两处调用传入 signal_price:
# Leg B 预检查估价
alt_limit_price = self._calculate_limit_price(
alt_coin, alt_is_buy, l2_snapshot,
signal_price=signal.latest_alt_price,
)
# Leg A 下单
result.leg_a = self._place_limit_order(
alt_coin, alt_is_buy, alt_size, l2_snapshot=l2_snapshot,
signal_price=signal.latest_alt_price,
)
5. 优化后的调用链
limit_open(signal)
├─ _calculate_limit_price(coin, is_buy, l2, signal_price=latest_alt_price) # Leg B 预检
│ └─ clamp: min(l2_price, signal_price) 或 max(l2_price, signal_price)
└─ _place_limit_order(coin, is_buy, size, l2, signal_price=latest_alt_price) # Leg A
└─ _calculate_limit_price(coin, is_buy, l2, signal_price)
└─ clamp 生效 → round_price()
limit_close(position) ← 不受影响,signal_price 默认 None,跳过 clamp
6. 边界情况处理
| 场景 | 行为 |
|---|---|
latest_alt_price 为 None 或 0 |
跳过 clamp,纯 L2 定价(完全向后兼容) |
| L2 价格已优于信号价格 | min/max 自动保留 L2 价格,无额外日志 |
| L2 价格与信号价格相同 | price != original_price 为 False,无日志输出 |
| 偏差 > 3%(默认阈值) | 仍然 clamp,但输出 WARNING 日志供人工关注 |
配置关闭 clamp_enabled=false |
完全跳过 clamp 逻辑,恢复原有行为 |
clamp 后 round_price() 精度 |
Python round() 四舍五入,对买/卖方向的影响 ≤0.01%,可忽略 |
| L2 返回 0.0(无订单簿) | 在 clamp 之前已 return 0.0,不会触达 clamp 代码 |
7. 风险评估
风险低:
- 所有新增参数都有默认值
None,完全向后兼容 limit_close()、market_open()、market_close()不受影响- 可通过环境变量
TRADING_SIGNAL_PRICE_CLAMP_ENABLED=false随时关闭
潜在影响:
- 如果市场在信号后不利移动较大,clamp 后的限价会远离当前市场(如:信号价 $100,当前 bid $105 → 限价 $100),导致限价单更难成交。但这正是期望行为 — 宁可不成交也不用更差的价格开仓。已有的 10 分钟超时 → 撤单机制会处理未成交情况。
round_price()在 clamp 之后调用,存在极小的精度偏差(<0.01%),实际无影响。
8. 验证方式
- 配置
TRADING_OPEN_ORDER_TYPE=limit,观察日志中信号价格约束生效输出 - 对比约束前后的限价:日志中显示
L2=$X → 约束后=$Y - 检查 Leg B 预检查的估算值是否使用了 clamped 价格(通过估算值日志确认)
- 通过
TRADING_SIGNAL_PRICE_CLAMP_ENABLED=false确认可关闭,恢复纯 L2 定价 - 制造高偏差场景,确认 WARNING 日志
信号价格偏差过大正常输出