feat(trader): matcher.py A股撮合(match_session/费率/100股/封板) Issue#3

This commit is contained in:
2026-07-07 10:05:49 +08:00
parent 84bf00ea8d
commit 67ea7763cd
2 changed files with 326 additions and 0 deletions
+92
View File
@@ -0,0 +1,92 @@
"""A 股撮合纯函数(match_session / 费率 / 100 股 / 封板)。
match_bar 必须是 raw 价格(spec §3.3 / §6.2)。所有费率来自 AccountConfig。
拒单返回 PaperReject;成交返回 PaperTrade。
资金检查由 Account 在 apply 前负责(matcher 不看资金)。
"""
import pandas as pd
from .limit import is_locked_for_buy_symbol, is_locked_for_sell_symbol
from .models import (
AccountConfig,
MatchSession,
OrderSide,
PaperOrder,
PaperReject,
PaperTrade,
)
MIN_LOT = 100
def cross_order(
order: PaperOrder,
match_bar: pd.Series,
prev_close_raw: float,
cfg: AccountConfig,
is_st: bool = False,
) -> PaperTrade | PaperReject:
"""单笔订单撮合。
步骤:
1. 涨跌停封板拒单(raw,按板块幅度)
2. 成交价(NEXT_OPEN=bar.open / CURRENT_CLOSE=bar.close
3. 限价单触价检查
4. 100 股取整(买入向下取整;卖出允许零股)
5. 费用(佣金 min 5 元 / 印花税仅卖 / 过户费单边)
"""
symbol = order.symbol
bar_date = str(match_bar.get("date", ""))
# 1. 涨跌停封板拒单(raw
if order.side == OrderSide.BUY and is_locked_for_buy_symbol(
match_bar, symbol, prev_close_raw, cfg, is_st
):
return PaperReject(order.strategy_id, symbol, "limit_up_locked", bar_date)
if order.side == OrderSide.SELL and is_locked_for_sell_symbol(
match_bar, symbol, prev_close_raw, cfg, is_st
):
return PaperReject(order.strategy_id, symbol, "limit_down_locked", bar_date)
# 2. 成交价(按 match_session
if order.match_session == MatchSession.NEXT_OPEN:
fill_price = match_bar["open"]
elif order.match_session == MatchSession.CURRENT_CLOSE:
fill_price = match_bar["close"]
else:
return PaperReject(
order.strategy_id, symbol, "unsupported_match_session", bar_date
)
# 3. 限价单触价
if not order.is_market:
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:
return PaperReject(order.strategy_id, symbol, "limit_not_touched", bar_date)
# 4. 100 股取整(买入向下取整;卖出不取整,允许零股退出)
volume = order.volume
if order.side == OrderSide.BUY:
volume = (volume // MIN_LOT) * MIN_LOT
if volume < MIN_LOT:
return PaperReject(order.strategy_id, symbol, "volume_below_min_lot", bar_date)
# 5. 费用
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
return PaperTrade(
strategy_id=order.strategy_id,
symbol=symbol,
side=order.side,
price=fill_price,
volume=volume,
commission=commission,
stamp_duty=stamp_duty,
transfer_fee=transfer_fee,
bar_date=bar_date,
match_session=order.match_session,
)