Files
sanguo_vnpy_v2/sanguo_trader/matcher.py
T
claude_dev 05d74fc2c1 fix(trader): M+L 接口校验 (listing_days/NaN/输入校验/类型注数) review
M1: PaperOrder+limit+matcher 加 listing_days(创业/科创/北交所前5日不锁,0=已过)
M3: is_locked_for_*_symbol cfg 注解 AccountConfig
M4: matcher NaN bar 拒单 bar_missing
L3: PositionLedger price/volume 正数校验
L5: PaperOrder __post_init__ volume 类型校验(拒 float/bool)
M5: current_close 契约 docstring + H3 残留注释修正(transfer_fee 双向)
79 tests passed.
2026-07-07 11:23:16 +08:00

126 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""A 股撮合纯函数(match_session / 费率 / 100 股 / 封板)。
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 (
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,
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. 限价单触价检查(含 H4:限价超涨停/跌停先拒)
4. 100 股取整(买入向下取整;卖出允许零股)
5. 滑点(买 += slippage,卖 -= slippageH2
6. 费用(佣金 min 5 元 / 印花税仅卖 / 过户费双向 H3)
"""
symbol = order.symbol
bar_date = str(match_bar.get("date", ""))
# 0. 停牌/缺 barNaN)拒单(review M4
_open = match_bar["open"]
if _open != _open: # NaN 检测(NaN != NaN
return PaperReject(order.strategy_id, symbol, "bar_missing", bar_date)
# 1. 涨跌停封板拒单(raw
if order.side == OrderSide.BUY and is_locked_for_buy_symbol(
match_bar, symbol, prev_close_raw, cfg, is_st, order.listing_days
):
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, order.listing_days
):
return PaperReject(order.strategy_id, symbol, "limit_down_locked", bar_date)
# 2. 成交价(按 match_session
if order.match_session == MatchSession.NEXT_OPEN:
fill_price = float(match_bar["open"])
elif order.match_session == MatchSession.CURRENT_CLOSE:
fill_price = float(match_bar["close"])
else:
return PaperReject(
order.strategy_id, symbol, "unsupported_match_session", bar_date
)
# 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:
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. 滑点(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 * 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,
)