Files
claude_dev 05dba7fe46 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通过
2026-07-07 23:37:50 +08:00

143 lines
5.3 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 股涨跌停纯函数(板块表 + 封板判断)。
全函数无副作用,全部用 **raw** 价格(spec §3.3 / §6.2)。
封板判据:
- 严格一字板:open=high=low=close=limit_price → 无对手盘
- T 字板(涨停):开=涨停 收=涨停 low<open → 盘中砸过板,保守拒单
- T 字板(跌停):开=跌停 收=跌停 high>open → 盘中反弹过,对称保守拒单
精度约定:
- 涨跌停价用 Decimal ROUND_HALF_UP(非 banker's round),消除 5.55×1.10→6.10 类问题。
- 封板价格相等用 math.isclose 容差比较(pricetick/2),
消除 5.5600000000000005 == 5.56 漏判(review C1)。
"""
import math
from decimal import ROUND_HALF_UP, Decimal
import pandas as pd
from .models import AccountConfig
# ---- 板块分类(按代码前缀)----
def get_board(symbol: str) -> str:
"""按 symbol 前缀判断板块(spec §6.2)。"""
if symbol.startswith(("300", "301")):
return "gem" # 创业板
if symbol.startswith(("688", "689")):
return "star" # 科创板
if symbol.startswith(("8", "4", "920")):
return "bse" # 北交所
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
def limit_ratio(board: str, is_st: bool) -> float:
"""涨跌停幅度。ST 统一 5%(北交所 ST 同 30%)。"""
if is_st and board != "bse":
return _ST_RATIO
return _LIMIT_RATIO[board]
# ---- 涨跌停价(ROUND_HALF_UP 四舍五入取整)----
def _round_half_up_to_tick(value: float, pricetick: float) -> float:
"""四舍五入到 pricetick 整数倍(用 Decimal 避免 banker's rounding)。"""
d_value = Decimal(str(value)) / Decimal(str(pricetick))
d_quantized = d_value.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
return float(d_quantized * Decimal(str(pricetick)))
def limit_up_price(prev_close_raw: float, ratio: float, pricetick: float) -> float:
return _round_half_up_to_tick(prev_close_raw * (1 + ratio), pricetick)
def limit_down_price(prev_close_raw: float, ratio: float, pricetick: float) -> float:
return _round_half_up_to_tick(prev_close_raw * (1 - ratio), pricetick)
# ---- 价格相等(容差比较,处理浮点尾数差)----
def _price_eq(a: float, b: float, pricetick: float = 0.01) -> bool:
"""浮点等价比较(容差 pricetick/2)。
例:5.5600000000000005 与 5.56 在 pricetick=0.01 下视为相等。
相对比较(<, >)保留原语义,不用此函数。
"""
return math.isclose(a, b, abs_tol=pricetick / 2)
# ---- 封板形态 ----
def is_one_word_lock(bar: pd.Series, limit_price: float, pricetick: float = 0.01) -> bool:
"""严格一字板:开=高=低=收=limit_price(容差比较)。"""
return bool(
_price_eq(bar["open"], limit_price, pricetick)
and _price_eq(bar["high"], limit_price, pricetick)
and _price_eq(bar["low"], limit_price, pricetick)
and _price_eq(bar["close"], limit_price, pricetick)
)
def is_t_lock(bar: pd.Series, limit_price: float, pricetick: float = 0.01) -> bool:
"""T 字板(涨停型):开=涨停、收=涨停、low<open(盘中砸过涨停)。"""
return bool(
_price_eq(bar["open"], limit_price, pricetick)
and _price_eq(bar["close"], limit_price, pricetick)
and bar["low"] < bar["open"]
)
def _is_t_lock_down(bar: pd.Series, limit_price: float, pricetick: float = 0.01) -> bool:
"""T 字板(跌停型,对称):开=跌停、收=跌停、high>open(盘中反弹过跌停)。"""
return bool(
_price_eq(bar["open"], limit_price, pricetick)
and _price_eq(bar["close"], limit_price, pricetick)
and bar["high"] > bar["open"]
)
# ---- 正式入口(带 symbol,自动判板块)----
def is_locked_for_buy_symbol(
bar: pd.Series,
symbol: str,
prev_close_raw: float,
cfg: AccountConfig,
is_st: bool = False,
listing_days: int = 0,
) -> bool:
"""涨停封板(一字板或 T 字板)→ 买单拒单。
新股前 5 日无涨跌幅限制(创业/科创/北交所):listing_days<5 → 不锁。
主板新股首日 ±44% 首版未实现(TODO),按普通 ±10% 处理。
"""
board = get_board(symbol)
if 1 <= listing_days <= 5 and board in ("gem", "star", "bse"):
return False
up = limit_up_price(prev_close_raw, limit_ratio(board, is_st), cfg.pricetick)
return is_one_word_lock(bar, up, cfg.pricetick) or is_t_lock(bar, up, cfg.pricetick)
def is_locked_for_sell_symbol(
bar: pd.Series,
symbol: str,
prev_close_raw: float,
cfg: AccountConfig,
is_st: bool = False,
listing_days: int = 0,
) -> bool:
"""跌停封板(一字板或跌停 T 字板)→ 卖单拒单。
新股前 5 日无涨跌幅限制(创业/科创/北交所):listing_days<5 → 不锁。
"""
board = get_board(symbol)
if 1 <= listing_days <= 5 and board in ("gem", "star", "bse"):
return False
down = limit_down_price(prev_close_raw, limit_ratio(board, is_st), cfg.pricetick)
return is_one_word_lock(bar, down, cfg.pricetick) or _is_t_lock_down(bar, down, cfg.pricetick)