mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
fix(realtime): 交易时段判断与落盘日期显式使用北京时间
quote_service/depth_service 的 _is_trading_hours 原用 naive datetime.now() 取服务器本地时间; Docker 镜像 (python:slim) 默认 UTC, 北京 9:15-15:05 = UTC 1:15-7:05, 轮询窗口完全错开——Docker 部署的实时行情/五档监控在真实交易时段一次都不会跑。 - 新增 app/market_time.py: CN_TZ 固定 UTC+8 (A股无夏令时, 不依赖系统 tzdata, Windows 桌面版同样可用) - 两个服务的 _is_trading_hours / date.today() 全部切换 - monitor.py 的 as_of 日期同步对齐 - Dockerfile 加 ENV TZ=Asia/Shanghai 兜底 (日志时间戳)
This commit is contained in:
@@ -72,5 +72,8 @@ ENV STATIC_DIR=/app/static \
|
||||
COPY --from=frontend-builder /build/dist ./static
|
||||
|
||||
ENV PYTHONPATH=/app
|
||||
# 兜底时区: 交易时段判断已在代码里显式用北京时间 (app/market_time.py),
|
||||
# 此处让日志时间戳等其余 naive 时间也对齐北京时间。
|
||||
ENV TZ=Asia/Shanghai
|
||||
EXPOSE 3018
|
||||
CMD ["uv", "run", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "3018"]
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"""A股市场时间工具 — 固定北京时间 (UTC+8, 无夏令时)。
|
||||
|
||||
服务器/容器本地时区不可靠 (python:slim 镜像默认 UTC), 交易时段判断、
|
||||
实时行情落盘日期等必须显式使用北京时间, 否则 Docker 部署时轮询窗口
|
||||
与真实交易时段完全错开 (北京 9:15-15:05 = UTC 1:15-7:05)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
CN_TZ = timezone(timedelta(hours=8))
|
||||
|
||||
|
||||
def cn_now() -> datetime:
|
||||
"""当前北京时间 (带时区)。"""
|
||||
return datetime.now(CN_TZ)
|
||||
|
||||
|
||||
def cn_today() -> date:
|
||||
"""当前北京日期。"""
|
||||
return datetime.now(CN_TZ).date()
|
||||
@@ -24,7 +24,9 @@ import logging
|
||||
import math
|
||||
import threading
|
||||
import time
|
||||
from datetime import date, datetime, time as dt_time
|
||||
from datetime import date, time as dt_time
|
||||
|
||||
from app.market_time import cn_now, cn_today
|
||||
from pathlib import Path
|
||||
|
||||
import polars as pl
|
||||
@@ -89,7 +91,7 @@ class DepthService:
|
||||
if not self._has_capability():
|
||||
logger.info("depth sealed: 无 DEPTH5_BATCH 能力, 跳过启动补跑")
|
||||
return
|
||||
today = date.today()
|
||||
today = cn_today()
|
||||
if self._persisted_for_date(today):
|
||||
# parquet 已存在: 恢复内存缓存(避免重启后每次查询都读 parquet)
|
||||
self._restore_from_parquet(today)
|
||||
@@ -579,7 +581,8 @@ class DepthService:
|
||||
|
||||
@staticmethod
|
||||
def _is_trading_hours() -> bool:
|
||||
now = datetime.now()
|
||||
# 显式北京时间: 容器/服务器本地时区可能是 UTC, 用 naive now() 会整体错开轮询窗口
|
||||
now = cn_now()
|
||||
t = now.time()
|
||||
morning = dt_time(9, 25) <= t <= dt_time(11, 35)
|
||||
afternoon = dt_time(12, 55) <= t <= dt_time(15, 5)
|
||||
|
||||
@@ -26,10 +26,12 @@ from __future__ import annotations
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from datetime import date, datetime, time as dt_time
|
||||
from datetime import date, time as dt_time
|
||||
|
||||
import polars as pl
|
||||
|
||||
from app.market_time import cn_now, cn_today
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -584,7 +586,7 @@ class QuoteService:
|
||||
if not select_exprs:
|
||||
return pl.DataFrame()
|
||||
result = df.select(select_exprs).with_columns(
|
||||
pl.lit(date.today()).cast(pl.Date).alias("date"),
|
||||
pl.lit(cn_today()).cast(pl.Date).alias("date"),
|
||||
)
|
||||
# 修复: API 在非交易时段可能返回 open/high/low=0 或 null,
|
||||
# 导致蜡烛从 0 开始。用 close 填充这些异常值。
|
||||
@@ -643,7 +645,8 @@ class QuoteService:
|
||||
|
||||
@staticmethod
|
||||
def _is_trading_hours() -> bool:
|
||||
now = datetime.now()
|
||||
# 显式北京时间: 容器/服务器本地时区可能是 UTC, 用 naive now() 会整体错开轮询窗口
|
||||
now = cn_now()
|
||||
t = now.time()
|
||||
morning = dt_time(9, 15) <= t <= dt_time(11, 35)
|
||||
afternoon = dt_time(12, 55) <= t <= dt_time(15, 5)
|
||||
@@ -879,7 +882,7 @@ class QuoteService:
|
||||
不写 daily, 直接传给 compute_enriched_today 避免重复计算。
|
||||
"""
|
||||
try:
|
||||
today = date.today()
|
||||
today = cn_today()
|
||||
t0 = time.perf_counter()
|
||||
|
||||
# ---- 尝试增量路径 ----
|
||||
|
||||
@@ -19,6 +19,7 @@ from typing import Any, Callable
|
||||
|
||||
import polars as pl
|
||||
|
||||
from app.market_time import cn_today
|
||||
from app.strategy.custom_signals import _OP_BUILDERS # type: ignore # 复用运算符构造器
|
||||
from app.strategy import config as _strategy_config
|
||||
|
||||
@@ -555,7 +556,7 @@ class MonitorRuleEngine:
|
||||
# 现接入 history_loader, 拼历史窗口 + 今日实时行情, 经 precomputed_history 喂给引擎。
|
||||
# loader 为 None (未装配) 时退回跳过, 保持旧行为, 不破坏无历史场景。
|
||||
run_kwargs: dict = {
|
||||
"as_of": _dt.date.today(),
|
||||
"as_of": cn_today(),
|
||||
"overrides": overrides,
|
||||
}
|
||||
if s.filter_history_fn:
|
||||
@@ -563,7 +564,7 @@ class MonitorRuleEngine:
|
||||
logger.debug("策略 %s 需要历史数据但未注入 history_loader, 跳过实时监控", sid)
|
||||
return []
|
||||
try:
|
||||
today = _dt.date.today()
|
||||
today = cn_today()
|
||||
lookback = max(1, getattr(s, "lookback_days", 30))
|
||||
hist_df = self._history_loader(today, lookback)
|
||||
if hist_df is None or hist_df.is_empty():
|
||||
@@ -597,7 +598,7 @@ class MonitorRuleEngine:
|
||||
import math
|
||||
self._latest_strategy_results[sid] = {
|
||||
"total": result.total,
|
||||
"as_of": str(_dt.date.today()),
|
||||
"as_of": str(cn_today()),
|
||||
"rows": [
|
||||
{k: (None if isinstance(v, float) and not math.isfinite(v) else v)
|
||||
for k, v in row.items()}
|
||||
|
||||
Reference in New Issue
Block a user