Files
sanguo_vnpy_v2/sanguo_trader/limit.py
T
claude_dev 05d74fc2c1 fix(trader): M+L 接口校验 (listing_days/NaN/输入校验/类型注数) review
M1: PaperOrder+limit+matcher 加 listing_days(创业/科创/北交所前5日不锁,0=已过)
M3: is_locked_for_*_symbol cfg 注解 AccountConfig
M4: matcher NaN bar 拒单 bar_missing
L3: PositionLedger price/volume 正数校验
L5: PaperOrder __post_init__ volume 类型校验(拒 float/bool)
M5: current_close 契约 docstring + H3 残留注释修正(transfer_fee 双向)
79 tests passed.
2026-07-07 11:23:16 +08:00

138 lines
5.1 KiB
Python
Raw 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" # 沪/深主板
_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)