mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 15:34:16 +08:00
fix(capabilities): redetect 同步刷新 app.state 快照并升级 tickflow SDK 0.1.25
- /api/capabilities/redetect 重新探测后同步写回 app.state.capabilities, minute_refresh 等门控服务即时读到新档位, 不再需要重启 - tickflow 0.1.24 缺 intraday_universe 方法导致 Expert 档全量分钟探测失败, 升级到 0.1.25 - 全量分钟轮次异常与模式补充 warning 日志, 便于线上诊断
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"""API 路由 — Phase 0 仅 /health 与 /api/capabilities。"""
|
"""API 路由 — Phase 0 仅 /health 与 /api/capabilities。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Request
|
||||||
|
|
||||||
from app import __version__
|
from app import __version__
|
||||||
from app.tickflow import client as tf_client
|
from app.tickflow import client as tf_client
|
||||||
@@ -31,9 +31,14 @@ def capabilities() -> dict:
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/api/capabilities/redetect")
|
@router.post("/api/capabilities/redetect")
|
||||||
def redetect() -> dict:
|
def redetect(request: Request) -> dict:
|
||||||
"""用户在设置页"重新检测"按钮。"""
|
"""用户在设置页"重新检测"按钮。"""
|
||||||
capset = detect_capabilities(force=True)
|
capset = detect_capabilities(force=True)
|
||||||
|
# 同步刷新 app.state 快照 (minute_refresh 等服务的门控读这里) 与财务调度器,
|
||||||
|
# 与 settings.py 各探测路径一致 — 否则重检测后服务侧仍读旧 capset 被错误门控
|
||||||
|
request.app.state.capabilities = capset
|
||||||
|
from app.api.settings import _sync_financial_scheduler_caps
|
||||||
|
_sync_financial_scheduler_caps(request.app.state, capset)
|
||||||
return {
|
return {
|
||||||
"label": tier_label(),
|
"label": tier_label(),
|
||||||
"capabilities": capset.to_dict(),
|
"capabilities": capset.to_dict(),
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
|
import logging
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
@@ -44,6 +45,8 @@ import polars as pl
|
|||||||
from app.market_time import cn_now, cn_today, in_continuous_session
|
from app.market_time import cn_now, cn_today, in_continuous_session
|
||||||
from app.services import preferences
|
from app.services import preferences
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# 轮询间隔允许范围 (秒): 稳态轮单请求无并发脉冲, 下限 3s;
|
# 轮询间隔允许范围 (秒): 稳态轮单请求无并发脉冲, 下限 3s;
|
||||||
# 上限 120s — universe 端点每标的只回最新 3 根, 间隔超过 3 分钟必留缺口,
|
# 上限 120s — universe 端点每标的只回最新 3 根, 间隔超过 3 分钟必留缺口,
|
||||||
# 每轮都会触发修复轮, 稳态设计失效, 故不允许配到 120s 以上。
|
# 每轮都会触发修复轮, 稳态设计失效, 故不允许配到 120s 以上。
|
||||||
@@ -187,6 +190,7 @@ class MinuteRefreshService:
|
|||||||
continue
|
continue
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self._state.last_error = f"round failed: {e}"
|
self._state.last_error = f"round failed: {e}"
|
||||||
|
logger.warning("全量分钟轮次异常: %s", e)
|
||||||
self._stop.wait(_LOOP_STEP_S)
|
self._stop.wait(_LOOP_STEP_S)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -231,8 +235,11 @@ class MinuteRefreshService:
|
|||||||
|
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
mode = self._select_mode()
|
mode = self._select_mode()
|
||||||
|
mode_label = "增量" if mode == "increment" else "全天修复"
|
||||||
with self._round_lock:
|
with self._round_lock:
|
||||||
if mode == "increment":
|
if mode == "increment":
|
||||||
|
# fetch 计时只覆盖网络取数; full 分支的 universe 维表读取不计入
|
||||||
|
fetch_started = time.perf_counter()
|
||||||
df, requests = kline_sync.fetch_intraday_universe_increment()
|
df, requests = kline_sync.fetch_intraday_universe_increment()
|
||||||
self._state.last_symbols = (
|
self._state.last_symbols = (
|
||||||
df["symbol"].n_unique() if not df.is_empty() else 0
|
df["symbol"].n_unique() if not df.is_empty() else 0
|
||||||
@@ -242,18 +249,27 @@ class MinuteRefreshService:
|
|||||||
self._state.last_symbols = len(symbols)
|
self._state.last_symbols = len(symbols)
|
||||||
if not symbols:
|
if not symbols:
|
||||||
self._state.last_error = "empty universe (instruments 未加载)"
|
self._state.last_error = "empty universe (instruments 未加载)"
|
||||||
|
logger.warning("全量分钟[%s] 本轮中止: 标的池为空 (instruments 未加载)", mode_label)
|
||||||
return
|
return
|
||||||
capset = getattr(self._app_state, "capabilities", None) if self._app_state else None
|
capset = getattr(self._app_state, "capabilities", None) if self._app_state else None
|
||||||
|
fetch_started = time.perf_counter()
|
||||||
df, requests = kline_sync.fetch_intraday_full_market_burst(symbols, capset)
|
df, requests = kline_sync.fetch_intraday_full_market_burst(symbols, capset)
|
||||||
|
fetch_ms = (time.perf_counter() - fetch_started) * 1000
|
||||||
self._state.last_requests = requests
|
self._state.last_requests = requests
|
||||||
if df.is_empty():
|
if df.is_empty():
|
||||||
self._empty_rounds += 1
|
self._empty_rounds += 1
|
||||||
self._state.last_error = f"intraday {mode} returned no data"
|
self._state.last_error = f"intraday {mode} returned no data"
|
||||||
|
logger.warning(
|
||||||
|
"全量分钟[%s] 本轮返回空数据: %d 请求, 取数 %.0fms",
|
||||||
|
mode_label, requests, fetch_ms,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
self._empty_rounds = 0
|
self._empty_rounds = 0
|
||||||
|
write_started = time.perf_counter()
|
||||||
written = kline_sync._write_minute_partition(
|
written = kline_sync._write_minute_partition(
|
||||||
df, self._repo.store.data_dir / "kline_minute",
|
df, self._repo.store.data_dir / "kline_minute",
|
||||||
)
|
)
|
||||||
|
write_ms = (time.perf_counter() - write_started) * 1000
|
||||||
|
|
||||||
self._state.rounds += 1
|
self._state.rounds += 1
|
||||||
self._state.last_round_at = time.time()
|
self._state.last_round_at = time.time()
|
||||||
@@ -261,6 +277,12 @@ class MinuteRefreshService:
|
|||||||
self._state.last_rows = written
|
self._state.last_rows = written
|
||||||
self._state.last_mode = mode
|
self._state.last_mode = mode
|
||||||
self._state.last_error = None
|
self._state.last_error = None
|
||||||
|
logger.info(
|
||||||
|
"全量分钟[%s] 第 %d 轮: 取数 %.0fms (%d 请求, %d 标的), "
|
||||||
|
"落盘 %.0fms (%d 行), 总计 %.0fms",
|
||||||
|
mode_label, self._state.rounds, fetch_ms, self._state.last_requests,
|
||||||
|
self._state.last_symbols, write_ms, written, self._state.last_round_ms,
|
||||||
|
)
|
||||||
|
|
||||||
def _universe(self) -> list[str]:
|
def _universe(self) -> list[str]:
|
||||||
"""全市场 A 股标的 (instruments 维表, 与盘后分钟同步同一来源)。"""
|
"""全市场 A 股标的 (instruments 维表, 与盘后分钟同步同一来源)。"""
|
||||||
|
|||||||
Generated
+1840
-1840
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user