fix(trader): matcher slippage/过户费双向/限价超涨停拒单 (H2/H3/H4)

H2: fill_price 应用 slippage(买+/卖-,默认0不影响)
H3: 过户费改双向(matcher 直接×2,Account 不再×2,删单边注释)
H4: 限价单超涨停价拒单(price_above_limit/price_below_limit)
69 tests passed.
This commit is contained in:
2026-07-07 11:15:19 +08:00
parent f940841a9a
commit b874be1d84
2 changed files with 112 additions and 11 deletions
+36 -8
View File
@@ -3,10 +3,21 @@
match_bar 必须是 raw 价格(spec §3.3 / §6.2)。所有费率来自 AccountConfig。
拒单返回 PaperReject;成交返回 PaperTrade。
资金检查由 Account 在 apply 前负责(matcher 不看资金)。
费用口径(review H3 修正):
- transfer_fee 直接出**双向总额**(沪深买卖均收),Account 不再 ×2。
- stamp_duty 仅卖;commission 最低 5 元。
"""
import pandas as pd
from .limit import is_locked_for_buy_symbol, is_locked_for_sell_symbol
from .limit import (
get_board,
is_locked_for_buy_symbol,
is_locked_for_sell_symbol,
limit_down_price,
limit_ratio,
limit_up_price,
)
from .models import (
AccountConfig,
MatchSession,
@@ -31,9 +42,10 @@ def cross_order(
步骤:
1. 涨跌停封板拒单(raw,按板块幅度)
2. 成交价(NEXT_OPEN=bar.open / CURRENT_CLOSE=bar.close
3. 限价单触价检查
3. 限价单触价检查(含 H4:限价超涨停/跌停先拒)
4. 100 股取整(买入向下取整;卖出允许零股)
5. 费用(佣金 min 5 元 / 印花税仅卖 / 过户费单边
5. 滑点(买 += slippage,卖 -= slippageH2
6. 费用(佣金 min 5 元 / 印花税仅卖 / 过户费双向 H3)
"""
symbol = order.symbol
bar_date = str(match_bar.get("date", ""))
@@ -50,16 +62,26 @@ def cross_order(
# 2. 成交价(按 match_session
if order.match_session == MatchSession.NEXT_OPEN:
fill_price = match_bar["open"]
fill_price = float(match_bar["open"])
elif order.match_session == MatchSession.CURRENT_CLOSE:
fill_price = match_bar["close"]
fill_price = float(match_bar["close"])
else:
return PaperReject(
order.strategy_id, symbol, "unsupported_match_session", bar_date
)
# 3. 限价单触价
# 3. 限价单:先查涨跌停边界(H4),再查触价
if not order.is_market:
board = get_board(symbol)
ratio = limit_ratio(board, is_st)
up = limit_up_price(prev_close_raw, ratio, cfg.pricetick)
down = limit_down_price(prev_close_raw, ratio, cfg.pricetick)
# H4:限价超涨停 / 跌停拒单(== 边界允许,> / < 才拒)
if order.side == OrderSide.BUY and order.price > up:
return PaperReject(order.strategy_id, symbol, "price_above_limit", bar_date)
if order.side == OrderSide.SELL and order.price < down:
return PaperReject(order.strategy_id, symbol, "price_below_limit", bar_date)
# 触价(用原始 fill_priceslippage 不影响触价判断)
if order.side == OrderSide.BUY and fill_price > order.price:
return PaperReject(order.strategy_id, symbol, "limit_not_touched", bar_date)
if order.side == OrderSide.SELL and fill_price < order.price:
@@ -72,11 +94,17 @@ def cross_order(
if volume < MIN_LOT:
return PaperReject(order.strategy_id, symbol, "volume_below_min_lot", bar_date)
# 5. 费用
# 5. 滑点(H2;默认 0 不影响)
if order.side == OrderSide.BUY:
fill_price += cfg.slippage
else:
fill_price -= cfg.slippage
# 6. 费用(transfer_fee 双向,H3
gross = volume * fill_price
commission = max(gross * cfg.rate, cfg.min_commission)
stamp_duty = gross * cfg.stamp_duty_rate if order.side == OrderSide.SELL else 0.0
transfer_fee = gross * cfg.transfer_fee_rate # 单边;Account 算双向 ×2
transfer_fee = gross * cfg.transfer_fee_rate * 2 # 沪深双向
return PaperTrade(
strategy_id=order.strategy_id,
+76 -3
View File
@@ -204,11 +204,11 @@ def test_stamp_duty_only_on_sell():
assert t_sell.stamp_duty == pytest.approx(0.5)
def test_transfer_fee_single_sided_in_trade():
def test_transfer_fee_double_sided_in_trade():
t = cross_order(buy(market=True), mkbar(10, 10, 10, 10), PREV, CFG)
assert isinstance(t, PaperTrade)
# 单边 100*10*0.00001 = 0.01trade 里存单边,Account 算 ×2
assert t.transfer_fee == pytest.approx(0.01)
# 双向 100*10*0.00001*2 = 0.02matcher 直接出双向总额
assert t.transfer_fee == pytest.approx(0.02)
def test_trade_carries_match_session():
@@ -232,3 +232,76 @@ def test_trade_carries_strategy_id_and_symbol():
assert isinstance(t, PaperTrade)
assert t.strategy_id == "my_strat"
assert t.symbol == "300750"
# ---- H4: 限价超涨停 / 跌停未拒 ----
def test_limit_buy_above_limit_up_rejected():
"""限价买单价格超涨停 → reject price_above_limit(触价检查前先查涨跌停)。"""
# 主板 600000 prev 10.0 → 涨停 11.0;限价 11.50 超涨停
o = PaperOrder("s1", "600000", OrderSide.BUY, 11.50, 100, is_market=False)
r = cross_order(o, mkbar(10.5, 11, 10.2, 10.8), PREV, CFG)
assert isinstance(r, PaperReject) and r.reason == "price_above_limit"
def test_limit_sell_below_limit_down_rejected():
"""限价卖单价格低于跌停 → reject price_below_limit。"""
# 主板 600000 prev 10.0 → 跌停 9.0;限价 8.50 跌破
o = PaperOrder("s1", "600000", OrderSide.SELL, 8.50, 100, is_market=False)
r = cross_order(o, mkbar(10.5, 11, 10.2, 10.8), PREV, CFG)
assert isinstance(r, PaperReject) and r.reason == "price_below_limit"
def test_limit_buy_at_limit_up_not_rejected():
"""限价 == 涨停价 不拒(严格 >,等号允许)。"""
o = PaperOrder("s1", "600000", OrderSide.BUY, 11.0, 100, is_market=False)
t = cross_order(o, mkbar(10.8, 11.0, 10.5, 10.9), PREV, CFG)
assert isinstance(t, PaperTrade) and t.price == 10.8
def test_limit_buy_above_gem_limit_up_rejected():
"""创业板 20% 涨停边界:300750 prev 10.0 → 涨停 12.0;限价 12.50 拒单。"""
o = PaperOrder("s1", "300750", OrderSide.BUY, 12.50, 100, is_market=False)
r = cross_order(o, mkbar(10.5, 11, 10.2, 10.8), 10.0, CFG)
assert isinstance(r, PaperReject) and r.reason == "price_above_limit"
# ---- H2: slippage 应用到 fill_price ----
def test_slippage_added_to_buy_fill():
"""买单 fill_price += slippage。"""
cfg = AccountConfig(initial_capital=1_000_000, slippage=0.05)
t = cross_order(buy(market=True), mkbar(10.0, 10, 10, 10), PREV, cfg)
assert isinstance(t, PaperTrade)
assert t.price == pytest.approx(10.05)
def test_slippage_subtracted_from_sell_fill():
"""卖单 fill_price -= slippage。"""
cfg = AccountConfig(initial_capital=1_000_000, slippage=0.05)
t = cross_order(sell(market=True), mkbar(10.0, 10, 10, 10), PREV, cfg)
assert isinstance(t, PaperTrade)
assert t.price == pytest.approx(9.95)
def test_zero_slippage_preserves_fill_price():
"""默认 slippage=0 → fill_price 不变(向后兼容)。"""
t = cross_order(buy(market=True), mkbar(10.0, 10, 10, 10), PREV, CFG)
assert isinstance(t, PaperTrade)
assert t.price == 10.0
def test_slippage_affects_gross_for_fees():
"""slippage 后的 fill_price 用于费用计算(更真实)。"""
cfg = AccountConfig(initial_capital=1_000_000, slippage=0.05)
t = cross_order(sell(market=True), mkbar(10.0, 10, 10, 10), PREV, cfg)
assert isinstance(t, PaperTrade)
# 卖单:fill=9.95gross=100*9.95=995.0
# stamp_duty = 995.0 * 0.0005 = 0.4975
assert t.stamp_duty == pytest.approx(995.0 * 0.0005)
# ---- H3: 过户费双向(已有 test_transfer_fee_double_sided_in_trade 重命名断言更新)----
def test_transfer_fee_double_sided_on_sell_too():
"""卖单过户费同样双向(沪深一致)。"""
t = cross_order(sell(market=True), mkbar(10, 10, 10, 10), PREV, CFG)
assert isinstance(t, PaperTrade)
assert t.transfer_fee == pytest.approx(0.02)