"""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, )