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:
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user