mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
原 vol_ratio_5d 有两个叠加缺陷导致盘中严重偏低: 1. 分母含自身(rolling_mean(5) 算了当天) → 量比偏低约1.4倍 2. 盘中无时间折算(部分量直接比全天量级) → 开盘时段极低 改为标准量比公式(同花顺/东方财富/通达信一致, 已查证官方定义): 量比 = 今日累计成交量 / (前5日均量 × 已交易分钟数/240) - 分母不含当天(volume.shift(1).rolling_mean(5) / tail(5)前5日) - 盘中按已交易分钟数折算(×240/elapsed), 盘后系数=1.0不受影响 已交易分钟数用行情 quote_ts 推算(比服务端时间更准, 无网络延迟): - market_time.py: trading_minutes_elapsed_from_ts(毫秒时间戳) - normalizer.py: DAILY_COLS 加 quote_ts, 保留 SDK timestamp - quote_service.py: _build_daily 保留 quote_ts, 传入 elapsed_minutes - pipeline.py: 两路径(盘后全量/盘中增量)改为标准算法 + ENRICHED_STORAGE_COLS 加 quote_ts - repository.py: live_agg 新增 _vol_ma5_prev_sum(tail(5)前5日和) quote_ts 落盘后还可用于: 盘后收盘数据校验(非15:00重拉)、跨天完整性检查。 不改动: vol_ma5/vol_ma10保留原语义(含当天均量), 策略/选股阈值不变。 129后端测试全过, EOD量比验证正确(707636/100000=7.076)。
80 lines
2.8 KiB
Python
80 lines
2.8 KiB
Python
"""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, time as dt_time, timedelta, timezone
|
|
|
|
CN_TZ = timezone(timedelta(hours=8))
|
|
|
|
# A 股交易时段 (北京时间): 上午 9:30-11:30 (120 分钟) + 下午 13:00-15:00 (120 分钟) = 240 分钟
|
|
_TRADING_TOTAL_MINUTES = 240
|
|
_MORNING_START = dt_time(9, 30)
|
|
_MORNING_END = dt_time(11, 30)
|
|
_AFTERNOON_START = dt_time(13, 0)
|
|
_AFTERNOON_END = dt_time(15, 0)
|
|
|
|
|
|
def cn_now() -> datetime:
|
|
"""当前北京时间 (带时区)。"""
|
|
return datetime.now(CN_TZ)
|
|
|
|
|
|
def cn_today() -> date:
|
|
"""当前北京日期。"""
|
|
return datetime.now(CN_TZ).date()
|
|
|
|
|
|
def trading_minutes_elapsed_from_dt(dt: datetime) -> float:
|
|
"""根据北京时间 datetime 计算当日已交易分钟数。
|
|
|
|
交易时段: 9:30-11:30 (0~120) + 13:00-15:00 (120~240)。
|
|
- 开盘前 = 0; 午休(11:30-13:00) = 120(保持上午累计); 收盘后 = 240。
|
|
- 非交易日(周末) = 240 (视作全天, 避免量比被折算成 0)。
|
|
"""
|
|
t = dt.time()
|
|
if t < _MORNING_START:
|
|
return 0.0
|
|
if t < _MORNING_END:
|
|
return (dt.hour * 60 + dt.minute - 9 * 60 - 30) + dt.second / 60.0
|
|
if t < _AFTERNOON_START:
|
|
return 120.0 # 午休, 保持上午累计
|
|
if t < _AFTERNOON_END:
|
|
return 120.0 + (dt.hour * 60 + dt.minute - 13 * 60) + dt.second / 60.0
|
|
return float(_TRADING_TOTAL_MINUTES)
|
|
|
|
|
|
def trading_minutes_elapsed() -> float:
|
|
"""当前已交易分钟数 (基于服务端北京时间)。
|
|
|
|
量比折算的兜底: 当行情 timestamp 缺失时用服务端时间。
|
|
优先使用 trading_minutes_elapsed_from_ts (行情真实时间, 更准)。
|
|
"""
|
|
return trading_minutes_elapsed_from_dt(cn_now())
|
|
|
|
|
|
def trading_minutes_elapsed_from_ts(ts_ms: int | float | None) -> float:
|
|
"""从行情时间戳(毫秒)计算当日已交易分钟数。
|
|
|
|
优先使用此函数: 行情 timestamp 是真实成交时间, 比服务端时间更准
|
|
(服务端时间含网络/限流延迟)。
|
|
|
|
Args:
|
|
ts_ms: 毫秒级 Unix 时间戳 (TickFlow SDK quote.timestamp / kline.timestamp)
|
|
|
|
Returns:
|
|
已交易分钟数 (0~240)。timestamp 为 None/无效时返回 240 (视作全天,
|
|
避免量比被折算成 0)。
|
|
"""
|
|
if not ts_ms:
|
|
return float(_TRADING_TOTAL_MINUTES)
|
|
try:
|
|
dt = datetime.fromtimestamp(int(ts_ms) / 1000.0, tz=CN_TZ)
|
|
except (ValueError, TypeError, OSError):
|
|
return float(_TRADING_TOTAL_MINUTES)
|
|
return trading_minutes_elapsed_from_dt(dt)
|
|
|