分时图实时刷新: 日期修复 + 间隔可配 + 对话框实时 + 自选刷新图标 (#114)

* fix(kline): 分时图盘中显示昨天而非今天

get_minute_batch 的日期选择存在死循环: trade_date=date.today() 后立即
if trade_date==date.today() (恒真), 无条件用 latest_minute_date_global()
覆盖今天; 但批量实时补拉不落库, 该值盘中恒返回昨天 -> 永远回退到昨天 ->
expected=240 判定昨日完整 -> 不触发今天补拉。

改为仅在真正非交易日回退 (周末 + 收盘后无今日日K 的节假日), 盘中保持
今天让 completeness 检查触发实时补拉。同时全量 date.today()/datetime.now()
改用北京时间 cn_today()/cn_now(), 与 quote_service/monitor 等一致。

影响: 自选列表分时列 + 单股详情分时图。

* feat(settings): 分时图刷新间隔可配置 (默认6s, 范围3-60s)

原写死 15s 轮询, 现可在 实时监控设置 -> 分时图刷新 卡片用滑块调节。
复用 minute_intraday_refresh 偏好链路, 新增 minute_intraday_refresh_interval
字段 (preferences + settings API + 前端 Preferences 接口)。

- preferences.py: getter 默认6s clamp[3,60] + set/get config 接入
- settings.py: RealtimeMonitorConfigIn 字段 + get_preferences 返回
- Monitoring.tsx: 滑块控件 (参考行情轮询滑块, 2s 防抖保存)
- Watchlist/Screener: 15_000 -> interval*1000, fallback ?? 6

* feat(dialog): 个股对话框日K/分时实时刷新

日K走 SSE 精准刷新: 对话框打开时注册焦点股票, quotes_updated 推送时
invalidate ['kline', symbol], 后端 _maybe_inject_live_candle 只读内存
不调 TickFlow, 秒级零成本。关闭/切股自动注销焦点。

分时走轮询: 复用分时刷新开关 + 间隔偏好, 经 StockPanel.refetchIntervalMs
透传到 StockIntradayChart。分时端点数据不完整会调 TickFlow, 不接 SSE
避免打爆限流。

不影响回测弹窗 (不注册焦点、不传间隔)。

* feat(watchlist): 分时列表头加刷新图标 (与策略列表一致)

分时列可见 + 未自动刷新时显示手动刷新按钮 (点击 refetch, 加载旋转);
自动刷新中显示持续旋转小图标提示。逻辑/样式对齐 ScreenerTable。

* fix(badge): 真假涨停修正弹层被父级裁剪遮挡

弹出层原用 absolute 定位, 被看板卡片父级的 overflow-hidden +
backdrop-blur containing block 裁剪/遮挡。改用 createPortal 渲染到 body,
基于徽章 getBoundingClientRect 计算弹层坐标, 脱离父级裁剪。

---------

Co-authored-by: shy3130 <shy3130@users.noreply.github.com>
This commit is contained in:
wshy
2026-07-14 15:46:18 +08:00
committed by GitHub
co-authored by shy3130
parent 77c0c643b2
commit d99112c160
12 changed files with 286 additions and 88 deletions
+8 -1
View File
@@ -429,6 +429,7 @@ def get_preferences() -> dict:
"webhook_default_channels": preferences.get_webhook_default_channels(),
"sidebar_index_symbols": preferences.get_sidebar_index_symbols(),
"minute_intraday_refresh": preferences.get_minute_intraday_refresh(),
"minute_intraday_refresh_interval": preferences.get_minute_intraday_refresh_interval(),
"nav_order": preferences.get_nav_order(),
"nav_hidden": preferences.get_nav_hidden(),
"screener_auto_run": preferences.get_screener_auto_run(),
@@ -754,6 +755,7 @@ class RealtimeMonitorConfigIn(BaseModel):
sidebar_index_symbols: list[str] | None = None
screener_auto_run: bool | None = None
minute_intraday_refresh: bool | None = None
minute_intraday_refresh_interval: int | None = None
@router.put("/preferences/realtime-monitor")
@@ -1311,7 +1313,12 @@ def run_limit_ladder_fix(request: Request) -> dict:
depth_svc = getattr(request.app.state, "depth_service", None)
if not depth_svc:
raise HTTPException(status_code=503, detail="depth 服务未初始化")
return depth_svc.run_once()
result = depth_svc.run_once()
# sealed 数据变了, 清看板总览缓存, 否则看板在 TTL 窗口内仍返回旧的 limit_up/fake 等
if result.get("ok"):
from app.api.overview import invalidate_overview_cache
invalidate_overview_cache()
return result
class DepthPollingIntervalIn(BaseModel):