mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 14:24:15 +08:00
fix(depth): 午间休市停止 sealed 轮询, 修复集合竞价盘口覆盖致涨停误判 (#73)
_poll_loop 原用宽窗口 _is_trading_hours() (9:25-11:35 / 12:55-15:05), 12:55-13:00 午后集合竞价准备期会恢复拉 depth. 此时 ask1/bid1 为竞价盘口, 语义不同于连续竞价, 「涨停价上卖一==0」的真封判定失效, 会用竞价盘口覆盖 11:30 已定格的正确 sealed 值, 导致涨停股误判为 sealed=False (假涨停) 被错误 扣减, 部分错误归入「炸板」, 连板梯队与涨跌停计数失真. 修复: depth sealed 轮询改用新增的 _is_continuous_trading() (9:30-11:30 / 13:00-15:00, 仅工作日), 与 quote_service 同名方法窗口一致. 午间 11:30-13:00 停止轮询, _sealed_cache 定格在 11:30 的正确值供正常显示, quote_service 的价格轮询保持宽窗口 (合理, 盘前预热/收盘捕捉) 不受影响. is_sealed_ready 不改动: 数据已落盘/缓存, 午间正常连续运行时本就不会降级, 直接复用定格值即可. 新增 13 个测试覆盖连续竞价窗口边界 + 午间休市/集合竞价准备期/周末回归点, 并逐点校验与 quote_service._is_continuous_trading 一致.
This commit is contained in:
@@ -429,10 +429,10 @@ class DepthService:
|
||||
"""盘中轮询: 按 capset 自适应间隔拉 depth, 更新内存缓存。"""
|
||||
while self._running:
|
||||
try:
|
||||
if self._is_trading_hours():
|
||||
if self._is_continuous_trading():
|
||||
self._poll_once()
|
||||
else:
|
||||
logger.debug("depth sealed: 非交易时段, 跳过")
|
||||
logger.debug("depth sealed: 非连续竞价时段, 跳过(避免集合竞价盘口覆盖 11:30 定格值)")
|
||||
except Exception as e: # noqa: BLE001
|
||||
logger.warning("depth sealed 轮询异常: %s", e)
|
||||
|
||||
@@ -583,3 +583,20 @@ class DepthService:
|
||||
morning = dt_time(9, 25) <= t <= dt_time(11, 35)
|
||||
afternoon = dt_time(12, 55) <= t <= dt_time(15, 5)
|
||||
return now.weekday() < 5 and (morning or afternoon)
|
||||
|
||||
@staticmethod
|
||||
def _is_continuous_trading() -> bool:
|
||||
"""A股连续竞价时段(北京时间): 9:30-11:30 / 13:00-15:00, 仅工作日。
|
||||
|
||||
比 _is_trading_hours 严格: 排除午间休市前后(11:30-13:00)。
|
||||
depth sealed 轮询用此窗口而非宽窗口, 关键原因:
|
||||
12:55-13:00 午后集合竞价准备期, ask1/bid1 盘口语义与连续竞价不同,
|
||||
「涨停价上卖一==0」的真封判定在此期间失效, 会用竞价盘口覆盖 11:30
|
||||
已定格的正确 sealed 值, 导致涨停股误判为 sealed=False(假涨停)被错误扣减。
|
||||
与 quote_service._is_continuous_trading 窗口定义保持一致。
|
||||
"""
|
||||
now = cn_now()
|
||||
t = now.time()
|
||||
morning = dt_time(9, 30) <= t <= dt_time(11, 30)
|
||||
afternoon = dt_time(13, 0) <= t <= dt_time(15, 0)
|
||||
return now.weekday() < 5 and (morning or afternoon)
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"""depth_service 连续竞价窗口测试 (午间休市涨跌停误判回归).
|
||||
|
||||
回归点: _poll_loop 原用 _is_trading_hours() (宽窗口 9:25-11:35 / 12:55-15:05),
|
||||
12:55-13:00 午后集合竞价准备期会恢复拉 depth, 此时 ask1/bid1 竞价盘口语义使
|
||||
「涨停价上卖一==0」真封判定失效, 覆盖 11:30 已定格的正确 sealed 值, 导致
|
||||
涨停股误判为 sealed=False (假涨停) 被错误扣减。
|
||||
|
||||
修复: depth sealed 轮询改用 _is_continuous_trading() (9:30-11:30 / 13:00-15:00),
|
||||
午间 11:30-13:00 停止轮询, 缓存保持 11:30 正确值。窗口定义须与
|
||||
quote_service._is_continuous_trading 一致。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from unittest.mock import patch
|
||||
|
||||
from app.market_time import CN_TZ
|
||||
from app.services import depth_service
|
||||
from app.services.depth_service import DepthService
|
||||
|
||||
|
||||
def _cn_now_at(hour: int, minute: int, weekday: int = 0):
|
||||
"""构造一个北京时间固定时刻的 cn_now 替身。weekday: 0=周一 ... 6=周日。"""
|
||||
# 2024-01-01 是周一, 据此定位目标 weekday 的日期
|
||||
day = 1 + ((weekday - datetime(2024, 1, 1).weekday()) % 7)
|
||||
fixed = datetime(2024, 1, day, hour, minute, tzinfo=CN_TZ)
|
||||
|
||||
def _fn():
|
||||
return fixed
|
||||
|
||||
return _fn
|
||||
|
||||
|
||||
def _set_cn_now(hour: int, minute: int, weekday: int = 0):
|
||||
"""patch depth_service 模块内的 cn_now 到固定时刻, 返回 patcher。"""
|
||||
return patch.object(depth_service, "cn_now", _cn_now_at(hour, minute, weekday))
|
||||
|
||||
|
||||
# ── 连续竞价窗口边界 (应返回 True) ───────────────────────────────────
|
||||
def test_morning_open_930_is_continuous():
|
||||
with _set_cn_now(9, 30, weekday=0):
|
||||
assert DepthService._is_continuous_trading() is True
|
||||
|
||||
|
||||
def test_morning_close_1130_is_continuous():
|
||||
"""11:30 早盘最后一刻, 仍连续竞价, 应拉 depth 定格正确值。"""
|
||||
with _set_cn_now(11, 30, weekday=0):
|
||||
assert DepthService._is_continuous_trading() is True
|
||||
|
||||
|
||||
def test_afternoon_open_1300_is_continuous():
|
||||
with _set_cn_now(13, 0, weekday=0):
|
||||
assert DepthService._is_continuous_trading() is True
|
||||
|
||||
|
||||
def test_afternoon_close_1500_is_continuous():
|
||||
with _set_cn_now(15, 0, weekday=0):
|
||||
assert DepthService._is_continuous_trading() is True
|
||||
|
||||
|
||||
# ── 回归关键点: 午间休市 + 集合竞价准备期必须停止轮询 (应返回 False) ──
|
||||
def test_lunch_break_1200_not_continuous():
|
||||
"""12:00 午间休市, 必须停止轮询。"""
|
||||
with _set_cn_now(12, 0, weekday=0):
|
||||
assert DepthService._is_continuous_trading() is False
|
||||
|
||||
|
||||
def test_auction_prep_1255_not_continuous():
|
||||
"""12:55 午后集合竞价准备期, 必须停止轮询 (最关键回归点)。
|
||||
|
||||
这是原 bug 的根因时刻: 宽窗口在此恢复拉 depth, 竞价盘口覆盖 11:30 正确值。
|
||||
"""
|
||||
with _set_cn_now(12, 55, weekday=0):
|
||||
assert DepthService._is_continuous_trading() is False
|
||||
|
||||
|
||||
def test_just_before_open_1259_not_continuous():
|
||||
"""12:59, 13:00 开盘前 1 分钟, 仍属集合竞价准备期, 不可拉。"""
|
||||
with _set_cn_now(12, 59, weekday=0):
|
||||
assert DepthService._is_continuous_trading() is False
|
||||
|
||||
|
||||
def test_just_after_morning_close_1131_not_continuous():
|
||||
"""11:31 早盘刚收盘, 不可拉 (定格 11:30)。"""
|
||||
with _set_cn_now(11, 31, weekday=0):
|
||||
assert DepthService._is_continuous_trading() is False
|
||||
|
||||
|
||||
def test_morning_auction_925_not_continuous():
|
||||
"""9:25 开盘集合竞价, 非连续竞价, 不可拉 (避免指示价误判)。"""
|
||||
with _set_cn_now(9, 25, weekday=0):
|
||||
assert DepthService._is_continuous_trading() is False
|
||||
|
||||
|
||||
def test_after_close_1505_not_continuous():
|
||||
"""15:05 收盘后, 不可拉。"""
|
||||
with _set_cn_now(15, 5, weekday=0):
|
||||
assert DepthService._is_continuous_trading() is False
|
||||
|
||||
|
||||
# ── 周末全天不交易 ────────────────────────────────────────────────────
|
||||
def test_weekend_saturday_not_trading():
|
||||
"""10:30 周六, 落在连续竞价时间区间内, 但周末不交易。"""
|
||||
with _set_cn_now(10, 30, weekday=5):
|
||||
assert DepthService._is_continuous_trading() is False
|
||||
|
||||
|
||||
def test_weekend_sunday_not_trading():
|
||||
"""14:00 周日, 落在连续竞价时间区间内, 但周末不交易。"""
|
||||
with _set_cn_now(14, 0, weekday=6):
|
||||
assert DepthService._is_continuous_trading() is False
|
||||
|
||||
|
||||
# ── 与 quote_service 窗口定义一致性 ──────────────────────────────────
|
||||
def test_window_matches_quote_service():
|
||||
"""depth_service._is_continuous_trading 必须与 quote_service 同名方法逐点一致。
|
||||
|
||||
同时 patch 两个模块各自的 cn_now 引用, 确保比较的是同一时刻下的窗口判定。
|
||||
"""
|
||||
from app.services import quote_service
|
||||
from app.services.quote_service import QuoteService
|
||||
|
||||
for weekday in range(7):
|
||||
for hour in range(0, 24):
|
||||
for minute in (0, 30):
|
||||
fn = _cn_now_at(hour, minute, weekday)
|
||||
with patch.object(depth_service, "cn_now", fn), \
|
||||
patch.object(quote_service, "cn_now", fn):
|
||||
assert DepthService._is_continuous_trading() == QuoteService._is_continuous_trading(), (
|
||||
f"窗口不一致 @ weekday={weekday} {hour:02d}:{minute:02d}"
|
||||
)
|
||||
Reference in New Issue
Block a user