ab703e93ba
CALL_AUCTION枚举原拒单(unsupported), 现撮合用开盘价(open, 集合竞价确定开盘价). 当前定价同NEXT_OPEN(均为open); 未来区分开盘/尾盘集合竞价需扩枚举. test: call_auction fills@open(原rejected用例改).
138 lines
5.5 KiB
Python
138 lines
5.5 KiB
Python
"""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,卖 -= slippage;H2)
|
||
6. 费用(佣金 min 5 元 / 印花税仅卖 / 过户费双向 H3)
|
||
"""
|
||
symbol = order.symbol
|
||
bar_date = str(match_bar.get("date", ""))
|
||
|
||
# 0. 停牌/缺 bar(NaN)拒单(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_price,slippage 不影响触价判断)
|
||
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,
|
||
)
|