Files
claude_dev ab703e93ba feat(matcher): 集合竞价CALL_AUCTION撮合(分期项)—开盘价(最大成交量原则→open)
CALL_AUCTION枚举原拒单(unsupported), 现撮合用开盘价(open, 集合竞价确定开盘价).
当前定价同NEXT_OPEN(均为open); 未来区分开盘/尾盘集合竞价需扩枚举.
test: call_auction fills@open(原rejected用例改).
2026-07-08 07:02:51 +08:00

138 lines
5.5 KiB
Python
Raw Permalink 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·200 股 / 封板)。
match_bar 必须是 raw 价格(spec §3.3 / §6.2)。所有费率来自 AccountConfig。
拒单返回 PaperReject;成交返回 PaperTrade。
资金检查由 Account 在 apply 前负责(matcher 不看资金)。
费用口径(review H3 修正):
- transfer_fee 直接出**双向总额**(沪深买卖均收),Account 不再 ×2。
- stamp_duty 仅卖;commission 最低 5 元。
最小手数(分期项):科创板 200 股起、1 股递增(不整倍);
主板/创业/北交所 100 股整倍。卖出不取整(允许零股退出)。
"""
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,
lot_size_for,
)
from .models import (
AccountConfig,
MatchSession,
OrderSide,
PaperOrder,
PaperReject,
PaperTrade,
)
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. 买入最小手数(科创 200 起 1 股递增;其余 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"])
elif order.match_session == MatchSession.CALL_AUCTION:
# 集合竞价开盘价(最大成交量原则 → open)。当前同 NEXT_OPEN 定价;
# 未来区分开盘/尾盘集合竞价需扩枚举(CALL_AUCTION_OPEN/CLOSE)。
fill_price = float(match_bar["open"])
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. 买入最小手数(科创 200 起 1 股递增;其余 100 整倍;卖出不取整)
volume = order.volume
if order.side == OrderSide.BUY:
min_lot = lot_size_for(symbol)
if get_board(symbol) == "star":
# 科创板:≥200,1 股递增(不整倍)
if volume < min_lot:
return PaperReject(order.strategy_id, symbol, "volume_below_min_lot", bar_date)
else:
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,
)