feat(trader): matcher.py A股撮合(match_session/费率/100股/封板) Issue#3
This commit is contained in:
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user