Files
sanguo_vnpy_v2/tests/trader/test_limit.py
T
claude_dev f940841a9a fix(trader): 价格精度 _price_eq + Decimal ROUND_HALF_UP (C1/H1)
C1 CRITICAL: is_one_word_lock/is_t_lock 用 == 比较价格,浮点尾数差
(5.5600000000000005 vs 5.56) 导致一字板当可成交。改用 math.isclose
容差比较 (pricetick/2)。新增 _price_eq(),所有价格 == 改用之;
相对比较 (low<open, high>open) 保留原语义。

H1 HIGH: limit_up/down_price 用 Python round() 是 banker's rounding
(round(610.5)=610),导致 5.55×1.10→6.10 而非 6.11。改用
decimal.Decimal(str(x)) + ROUND_HALF_UP。

测试:
- 5.05 一字涨停 (浮点尾数差 case)
- 9.99×1.2 创业板一字板
- 0.35×1.05 ST T 字板
- 5.55×1.10 → 6.11 (banker's 消除)
- 4.45×0.90 → 4.01

60 tests pass, 100% cov 保持。

Refs: Phase 3c C-S0 review findings [C1][H1]
2026-07-07 10:47:39 +08:00

207 lines
7.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 价格。"""
import pandas as pd
from sanguo_trader.limit import (
get_board,
limit_ratio,
limit_up_price,
limit_down_price,
is_one_word_lock,
is_t_lock,
is_locked_for_buy_symbol,
is_locked_for_sell_symbol,
)
from sanguo_trader.models import AccountConfig
CFG = AccountConfig(initial_capital=1_000_000)
def bar(open, high, low, close):
return pd.Series({"open": open, "high": high, "low": low, "close": close})
# ---- 板块分类 ----
def test_board_classification():
assert get_board("600000") == "main" # 沪市主板
assert get_board("000001") == "main" # 深市主板
assert get_board("300750") == "gem" # 创业板
assert get_board("301128") == "gem" # 创业板(301
assert get_board("688981") == "star" # 科创板
assert get_board("689009") == "star" # 科创板(689
assert get_board("830799") == "bse" # 北交所
assert get_board("920002") == "bse" # 北交所(920
# ---- 幅度查表 ----
def test_limit_ratio():
assert limit_ratio("main", is_st=False) == 0.10
assert limit_ratio("gem", is_st=False) == 0.20
assert limit_ratio("star", is_st=False) == 0.20
assert limit_ratio("bse", is_st=False) == 0.30
# ST 统一 5%
assert limit_ratio("main", is_st=True) == 0.05
assert limit_ratio("gem", is_st=True) == 0.05
assert limit_ratio("star", is_st=True) == 0.05
assert limit_ratio("bse", is_st=True) == 0.30 # 北交所 ST 同 30%
# ---- 涨跌停价(按 pricetick 取整)----
def test_limit_up_price_rounds_to_pricetick():
# 10.00 * 1.10 = 11.00
assert limit_up_price(10.0, 0.10, 0.01) == 11.0
# 9.99 * 1.20 = 11.988 → 11.99
assert limit_up_price(9.99, 0.20, 0.01) == 11.99
# 5.00 * 1.30 = 6.5
assert limit_up_price(5.0, 0.30, 0.01) == 6.5
def test_limit_down_price_rounds_to_pricetick():
# 10.00 * 0.90 = 9.00
assert limit_down_price(10.0, 0.10, 0.01) == 9.0
# 10.00 * 0.80 = 8.00
assert limit_down_price(10.0, 0.20, 0.01) == 8.0
# ---- 一字板 / T 字板 ----
def test_one_word_lock_detected():
up = limit_up_price(10.0, 0.10, 0.01)
assert is_one_word_lock(bar(11.0, 11.0, 11.0, 11.0), up) is True
# 非一字
assert is_one_word_lock(bar(11.0, 11.5, 10.8, 11.0), up) is False
assert is_one_word_lock(bar(11.0, 11.0, 10.5, 11.0), up) is False # T 字板非一字
def test_t_lock_detected():
up = limit_up_price(10.0, 0.10, 0.01)
# T 字板:开=涨停 收=涨停 low<open
assert is_t_lock(bar(11.0, 11.0, 10.5, 11.0), up) is True
# 一字板不是 T 字板
assert is_t_lock(bar(11.0, 11.0, 11.0, 11.0), up) is False
# 开≠涨停 不是 T 字板
assert is_t_lock(bar(10.5, 11.0, 10.5, 11.0), up) is False
# ---- 买单封板拒单(按 symbol 自动判板块)----
def test_buy_locked_one_word_main_board():
# 主板 600000,一字涨停 → 买不进
assert is_locked_for_buy_symbol(
bar(11.0, 11.0, 11.0, 11.0), "600000", 10.0, CFG, is_st=False
) is True
def test_buy_locked_t_lock_conservative():
# T 字板 → 保守拒买
assert is_locked_for_buy_symbol(
bar(11.0, 11.0, 10.5, 11.0), "600000", 10.0, CFG, is_st=False
) is True
def test_buy_not_locked_when_opened():
# 开板(low 远低于涨停)→ 可买
assert is_locked_for_buy_symbol(
bar(10.5, 10.8, 10.2, 10.6), "600000", 10.0, CFG, is_st=False
) is False
def test_buy_locked_gem_board_20pct():
# 创业板 30075010.00 → 涨停 12.00
assert is_locked_for_buy_symbol(
bar(12.0, 12.0, 12.0, 12.0), "300750", 10.0, CFG, is_st=False
) is True
def test_buy_locked_star_board_20pct():
# 科创板 688981
assert is_locked_for_buy_symbol(
bar(12.0, 12.0, 12.0, 12.0), "688981", 10.0, CFG, is_st=False
) is True
def test_buy_locked_bse_board_30pct():
# 北交所 8307995.00 → 涨停 6.5
assert is_locked_for_buy_symbol(
bar(6.5, 6.5, 6.5, 6.5), "830799", 5.0, CFG, is_st=False
) is True
def test_buy_locked_st_5pct():
# ST 股 5% 涨停,10.00 → 10.50
assert is_locked_for_buy_symbol(
bar(10.5, 10.5, 10.5, 10.5), "600000", 10.0, CFG, is_st=True
) is True
# ---- 卖单封板(跌停对称)----
def test_sell_locked_one_word_limit_down():
# 主板一字跌停:10 → 9.0
assert is_locked_for_sell_symbol(
bar(9.0, 9.0, 9.0, 9.0), "600000", 10.0, CFG, is_st=False
) is True
def test_sell_locked_t_lock_limit_down():
# 跌停 T 字板:开=跌停 收=跌停 high>open(盘中反弹过)
assert is_locked_for_sell_symbol(
bar(9.0, 9.5, 9.0, 9.0), "600000", 10.0, CFG, is_st=False
) is True
def test_sell_not_locked_when_opened():
# 非跌停形态 → 可卖
assert is_locked_for_sell_symbol(
bar(9.5, 9.8, 9.2, 9.6), "600000", 10.0, CFG, is_st=False
) is False
# ---- C1: 浮点 == 比较漏判封板 ----
# limit_up_price(5.05, 0.10, 0.01) 由于浮点 = 5.5600000000000005
# bar.open=5.56 时若用 == 比较会判 False → 一字板当可成交(CRITICAL)。
def test_one_word_lock_float_precision_main_board():
"""5.05 一字涨停:limit_up 浮点尾数差不应漏判。"""
up = limit_up_price(5.05, 0.10, 0.01)
# 触发条件:浮点 == 比较会失败(5.5600000000000005 != 5.56
assert is_one_word_lock(bar(5.56, 5.56, 5.56, 5.56), up) is True
assert is_locked_for_buy_symbol(
bar(5.56, 5.56, 5.56, 5.56), "600000", 5.05, CFG, is_st=False
) is True
def test_one_word_lock_float_precision_gem_board():
"""创业板 3007509.99×1.2 浮点尾数差。"""
up = limit_up_price(9.99, 0.20, 0.01) # 11.988 → 11.99
assert is_one_word_lock(bar(11.99, 11.99, 11.99, 11.99), up) is True
assert is_locked_for_buy_symbol(
bar(11.99, 11.99, 11.99, 11.99), "300750", 9.99, CFG, is_st=False
) is True
def test_t_lock_float_precision_st_stock():
"""ST 0.35×1.05 = 0.3675 → 0.37(浮点尾数差),T 字板。"""
up = limit_up_price(0.35, 0.05, 0.01)
# T 字板:开=收=涨停,low<open
assert is_t_lock(bar(0.37, 0.37, 0.35, 0.37), up) is True
assert is_locked_for_buy_symbol(
bar(0.37, 0.37, 0.35, 0.37), "600000", 0.35, CFG, is_st=True
) is True
def test_one_word_lock_still_detects_exact_match():
"""精度修复不应破坏精确匹配场景。"""
up = limit_up_price(10.0, 0.10, 0.01)
assert is_one_word_lock(bar(11.0, 11.0, 11.0, 11.0), up) is True
# ---- H1: round 是 banker's rounding → 用 Decimal ROUND_HALF_UP ----
def test_limit_up_price_uses_half_up_not_bankers():
# 5.55 * 1.10 = 6.105 → 6.105 / 0.01 = 610.5
# banker's round(610.5) = 610(偶)→ 6.10 ❌
# ROUND_HALF_UP → 611 → 6.11 ✅
assert limit_up_price(5.55, 0.10, 0.01) == 6.11
def test_limit_down_price_uses_half_up_not_bankers():
# 4.45 * 0.90 = 4.005 → 4.005 / 0.01 = 400.5
# banker's round(400.5) = 400 → 4.00 ❌
# ROUND_HALF_UP → 401 → 4.01 ✅
assert limit_down_price(4.45, 0.10, 0.01) == 4.01