feat(trader): 科创板200股最小手数(分期项切片)—lot_size_for+matcher板块取整
科创板(688/689)最小200股1股递增(不整倍); 主板/创业/北交100整倍; 卖出不取整。 - limit.py 加 lot_size_for(symbol) - matcher cross_order 买入取整按板块(star≥200不取整, 其余100整倍) - routes_paper cta size=lot_size_for(symbol) - test: 688981 买150拒/买250不取整; 主板用例不变; 63/63通过
This commit is contained in:
@@ -145,8 +145,10 @@ def _run_replay(db, aid, req: PaperCreateRequest):
|
||||
cls = get_strategy_class(s.name)
|
||||
if cls is None:
|
||||
continue # 策略不可用(本机无 vnpy_ctastrategy)→ 跳过
|
||||
from sanguo_trader.limit import lot_size_for
|
||||
cta = PaperCtaEngine(s.name, match_session=s.match_session,
|
||||
listing_days=s.listing_days, size=100) # A 股 1 手=100 股
|
||||
listing_days=s.listing_days,
|
||||
size=lot_size_for(s.symbol)) # 主板 100 / 科创 200 股一手
|
||||
vt_symbol = f"{s.symbol}.{guess_exchange(s.symbol).value}"
|
||||
strat = cls(cta, s.name, vt_symbol, s.params) # CtaTemplate(cta_engine, name, vt_symbol, setting)
|
||||
strat.trading = True # 允许 send_order(等价 on_start)
|
||||
|
||||
@@ -31,6 +31,11 @@ def get_board(symbol: str) -> str:
|
||||
return "main" # 沪/深主板
|
||||
|
||||
|
||||
def lot_size_for(symbol: str) -> int:
|
||||
"""最小交易手数对应股数:科创板 200(1 股递增),其余板块 100。"""
|
||||
return 200 if get_board(symbol) == "star" else 100
|
||||
|
||||
|
||||
_LIMIT_RATIO = {"main": 0.10, "gem": 0.20, "star": 0.20, "bse": 0.30}
|
||||
_ST_RATIO = 0.05
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""A 股撮合纯函数(match_session / 费率 / 100 股 / 封板)。
|
||||
"""A 股撮合纯函数(match_session / 费率 / 100·200 股 / 封板)。
|
||||
|
||||
match_bar 必须是 raw 价格(spec §3.3 / §6.2)。所有费率来自 AccountConfig。
|
||||
拒单返回 PaperReject;成交返回 PaperTrade。
|
||||
@@ -7,6 +7,9 @@ match_bar 必须是 raw 价格(spec §3.3 / §6.2)。所有费率来自 Acco
|
||||
费用口径(review H3 修正):
|
||||
- transfer_fee 直接出**双向总额**(沪深买卖均收),Account 不再 ×2。
|
||||
- stamp_duty 仅卖;commission 最低 5 元。
|
||||
|
||||
最小手数(分期项):科创板 200 股起、1 股递增(不整倍);
|
||||
主板/创业/北交所 100 股整倍。卖出不取整(允许零股退出)。
|
||||
"""
|
||||
import pandas as pd
|
||||
|
||||
@@ -17,6 +20,7 @@ from .limit import (
|
||||
limit_down_price,
|
||||
limit_ratio,
|
||||
limit_up_price,
|
||||
lot_size_for,
|
||||
)
|
||||
from .models import (
|
||||
AccountConfig,
|
||||
@@ -27,8 +31,6 @@ from .models import (
|
||||
PaperTrade,
|
||||
)
|
||||
|
||||
MIN_LOT = 100
|
||||
|
||||
|
||||
def cross_order(
|
||||
order: PaperOrder,
|
||||
@@ -43,7 +45,7 @@ def cross_order(
|
||||
1. 涨跌停封板拒单(raw,按板块幅度)
|
||||
2. 成交价(NEXT_OPEN=bar.open / CURRENT_CLOSE=bar.close)
|
||||
3. 限价单触价检查(含 H4:限价超涨停/跌停先拒)
|
||||
4. 100 股取整(买入向下取整;卖出允许零股)
|
||||
4. 买入最小手数(科创 200 起 1 股递增;其余 100 整倍;卖出不取整)
|
||||
5. 滑点(买 += slippage,卖 -= slippage;H2)
|
||||
6. 费用(佣金 min 5 元 / 印花税仅卖 / 过户费双向 H3)
|
||||
"""
|
||||
@@ -92,12 +94,18 @@ def cross_order(
|
||||
if order.side == OrderSide.SELL and fill_price < order.price:
|
||||
return PaperReject(order.strategy_id, symbol, "limit_not_touched", bar_date)
|
||||
|
||||
# 4. 100 股取整(买入向下取整;卖出不取整,允许零股退出)
|
||||
# 4. 买入最小手数(科创 200 起 1 股递增;其余 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)
|
||||
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:
|
||||
|
||||
@@ -174,6 +174,29 @@ def test_sell_odd_lot_allowed():
|
||||
assert isinstance(t, PaperTrade) and t.volume == 50
|
||||
|
||||
|
||||
# ---- 科创板 200 股最小手数(分期项)----
|
||||
def test_star_buy_below_200_rejected():
|
||||
"""科创板 688981 买 150 股 < 200 → 拒单(200 股起)。"""
|
||||
r = cross_order(
|
||||
PaperOrder("s1", "688981", OrderSide.BUY, 0, 150, True),
|
||||
mkbar(10, 10, 10, 10),
|
||||
PREV,
|
||||
CFG,
|
||||
)
|
||||
assert isinstance(r, PaperReject) and r.reason == "volume_below_min_lot"
|
||||
|
||||
|
||||
def test_star_buy_250_not_rounded_to_200():
|
||||
"""科创板 688981 买 250 股 → ≥200,1 股递增不取整,成交 250(非主板 100 整倍)。"""
|
||||
t = cross_order(
|
||||
PaperOrder("s1", "688981", OrderSide.BUY, 0, 250, True),
|
||||
mkbar(10, 10, 10, 10),
|
||||
PREV,
|
||||
CFG,
|
||||
)
|
||||
assert isinstance(t, PaperTrade) and t.volume == 250
|
||||
|
||||
|
||||
# ---- 费用 ----
|
||||
def test_commission_uses_min_5_yuan():
|
||||
# 100 股 × 10 元 × 0.0003 = 0.3 → 不足 5 元,收 5
|
||||
|
||||
Reference in New Issue
Block a user