fix(shadow): 影子市价单成交基准改实时行情——旧按委托保护价记账恒差1.5%,08-24双轨4/6红根因 [vps]

根因(代码级实锤): broker._ref_price 优先用下单传入price,而市价单的price是保护性委托上限(策略侧行情×1.015,BrokerBase契明文'可用作保护价/参考价'非期望成交价)→影子每笔买入虚高~1.5%,卖侧对称低~1.5%。08-24证据三连:momentum/small_cap首次真实成交日25票价差紧聚-150bps(-112~-189);卖侧510500同样-1.54%(排除'晚一根bar随行情'解释);级联=虚价吃掉影子现金6k+→002038买第6只差1678元资金不足拒单(shadow_60日志)。排除用户初判的滑点/费率参数不一致(影子账户slippage=0.0费率正常)。回测/实走无此问题(matcher.py按bar开收价,另一套)。
修: _ref_price(security,price,market)——市价单成交基准=price_getter实时行情,行情不可得退回委托价(告警,可用性优先);限价单沿用传入价(触价语义,旧行为);buy/sell补市价转限价封顶(行情超保护上限按上限成交不追高,卖侧对称)。
+5测试(市价买卖按行情/超上限封顶/无行情退委托价/限价语义不变),trader 254绿+api 171绿。
This commit is contained in:
2026-08-24 23:07:12 +08:00
parent 6c2c217e22
commit e2dc473682
2 changed files with 86 additions and 12 deletions
+33 -8
View File
@@ -118,15 +118,34 @@ class ShadowBroker: # noqa: R0903 - 仅实现 BrokerBase 协议(bullet_trade du
return None
# ===== 行情 =====
def _ref_price(self, security: str, price: Optional[float]) -> Optional[float]:
ref = price if price and price > 0 else None
if ref is None and self.price_getter is not None:
def _ref_price(self, security: str, price: Optional[float],
market: bool = False) -> Optional[float]:
"""成交参考价。
市价单(market=True)成交基准=实时行情(price_getter):下单带的 price 只是
保护性委托上限(策略侧行情×1.015,BrokerBase 契注明"可用作保护价/参考价"),
按它记账会让影子恒比实盘贵~1.5%(2026-08-24 双轨 4/6 红根因:momentum/
small_cap 首次真实成交日 25 票价差紧聚 -150bps+级联现金虚耗致 002038
资金不足漏买)。行情不可得才退回委托价(告警,可用性优先)。
限价单沿用传入价(触价语义,不取行情,与旧行为一致),缺价时行情兜底。
"""
if not market and price is not None and price > 0:
return price
if self.price_getter is not None:
try:
ref = self.price_getter(security)
quote = self.price_getter(security)
except Exception as exc: # noqa: BLE001 - 行情失败拒单而非崩柜台
logger.warning("[shadow] 取价失败 %s: %s", security, exc)
ref = None
return ref
quote = None
if quote is not None and quote > 0:
return quote
if price is not None and price > 0:
if market:
logger.warning(
"[shadow] %s 市价单无实时行情,退回委托价 %.2f(保护上限口径)",
security, price)
return price
return None
def _limit_blocked(self, security: str, side: str) -> Optional[str]:
"""涨跌停/停牌拒单原因(P1.3,双轨对账与实盘约束对齐)。
@@ -156,9 +175,12 @@ class ShadowBroker: # noqa: R0903 - 仅实现 BrokerBase 协议(bullet_trade du
wait_timeout: Optional[float] = None, remark: Optional[str] = None,
*, market: bool = False) -> str:
order_id = self._new_order("buy", security, amount, price)
ref = self._ref_price(security, price)
ref = self._ref_price(security, price, market=market)
if ref is None or ref <= 0:
return self._reject(order_id, "无参考价")
# 市价转限价语义:决策到成交间快速拉升、行情已超保护上限 → 按上限成交,不追高
if market and price is not None and price > 0 and ref > price:
ref = price
blocked = self._limit_blocked(security, "buy")
if blocked:
return self._reject(order_id, blocked)
@@ -186,9 +208,12 @@ class ShadowBroker: # noqa: R0903 - 仅实现 BrokerBase 协议(bullet_trade du
wait_timeout: Optional[float] = None, remark: Optional[str] = None,
*, market: bool = False) -> str:
order_id = self._new_order("sell", security, amount, price)
ref = self._ref_price(security, price)
ref = self._ref_price(security, price, market=market)
if ref is None or ref <= 0:
return self._reject(order_id, "无参考价")
# 市价转限价语义(卖侧对称):行情已跌破保护下限 → 按下限成交,不杀跌
if market and price is not None and price > 0 and ref < price:
ref = price
blocked = self._limit_blocked(security, "sell")
if blocked:
return self._reject(order_id, blocked)
+53 -4
View File
@@ -23,12 +23,12 @@ def _mk_broker(cash: float = 100_000.0, **kw) -> ShadowBroker:
)
def _buy(b: ShadowBroker, sec: str, amt: int, px: float | None = None):
return asyncio.run(b.buy(sec, amt, px))
def _buy(b: ShadowBroker, sec: str, amt: int, px: float | None = None, **kw):
return asyncio.run(b.buy(sec, amt, px, **kw))
def _sell(b: ShadowBroker, sec: str, amt: int, px: float | None = None):
return asyncio.run(b.sell(sec, amt, px))
def _sell(b: ShadowBroker, sec: str, amt: int, px: float | None = None, **kw):
return asyncio.run(b.sell(sec, amt, px, **kw))
def test_buy_fills_with_commission_and_slippage():
@@ -97,6 +97,55 @@ def test_no_price_rejects():
assert "无参考价" in b.orders[oid]["reject_reason"]
# ---- 市价单成交基准=实时行情(2026-08-24 双轨 4/6 红根因回归) ----
def test_market_buy_fills_at_quote_not_protective_price():
"""市价单带的 price 是保护上限(策略侧行情×1.015),成交基准必须是实时行情。
08-24 实锤:momentum/small_cap 首次真实成交日,影子按保护价记账 → 25 票
价差紧聚 -150bps + 级联现金虚耗 → 002038 影子资金不足漏买,双轨 4/6 红。
BrokerBase 契约(bullet_trade/broker/base.py):market=True 时 price 亦视为
市价,可用作保护价/参考价——不是期望成交价。
"""
b = _mk_broker(cash=1_000_000, slippage=0.0)
oid = _buy(b, "600519.SH", 100, px=101.5, market=True)
assert b.orders[oid]["status"] == "filled"
assert b.orders[oid]["filled_price"] == pytest.approx(100.0) # 行情价,非 101.5
def test_market_sell_fills_at_quote_not_protective_floor():
"""卖侧对称:保护下限不作成交价(08-24 实锤 510500 影子卖 7.64 vs 实盘 7.76)。"""
b = _mk_broker(cash=1_000_000, slippage=0.0)
_buy(b, "600519.SH", 100, px=100.0)
b.before_open()
oid = _sell(b, "600519.SH", 100, px=98.5, market=True)
assert b.orders[oid]["status"] == "filled"
assert b.orders[oid]["filled_price"] == pytest.approx(100.0) # 行情价,非 98.5
def test_market_buy_quote_beyond_protective_cap_fills_at_cap():
"""行情已超保护上限(决策到成交间快速拉升):按上限成交(市价转限价语义),不追高。"""
b = _mk_broker(cash=1_000_000, slippage=0.0, prices={"600519.SH": 105.0})
oid = _buy(b, "600519.SH", 100, px=101.5, market=True)
assert b.orders[oid]["filled_price"] == pytest.approx(101.5)
def test_market_order_no_quote_falls_back_to_protective_price():
"""行情不可得:退回委托价(告警)而非拒单——可用性优先,影子不因缺行情停摆。"""
b = _mk_broker(cash=1_000_000, slippage=0.0, prices={})
oid = _buy(b, "600519.SH", 100, px=101.5, market=True)
assert b.orders[oid]["status"] == "filled"
assert b.orders[oid]["filled_price"] == pytest.approx(101.5)
def test_limit_order_keeps_passed_price_semantics():
"""限价单(market=False)沿用传入价——触价语义不变。"""
b = _mk_broker(cash=1_000_000, slippage=0.0)
oid = _buy(b, "600519.SH", 100, px=99.0)
assert b.orders[oid]["status"] == "filled"
assert b.orders[oid]["filled_price"] == pytest.approx(99.0)
# ---- P1.3 涨跌停/停牌拒单(双轨对账:与实盘 QMT 约束对齐,减少对账噪音) ----
def _limit_map_getter(status: dict):