Files
qihuo/modules/market/kline_stream.py
T

142 lines
4.7 KiB
Python

# Copyright (c) 2025-2026 马建军. All rights reserved.
# 专有软件 — 未经授权禁止复制、传播、转售。
# 严禁用于:带单/代客理财、向他人推荐期货品种或买卖建议、融资配资等业务。
# 详见 LICENSE.zh-CN.txt 与 docs/软件购买与使用协议.md
"""K 线 SSE 推送与后台刷新。"""
from __future__ import annotations
import json
import logging
import queue
import threading
import time
from dataclasses import dataclass, field
from datetime import datetime
from typing import Callable, Optional
from zoneinfo import ZoneInfo
from modules.market.kline_chart import fetch_market_klines, ths_to_sina_chart_symbol
from modules.market.kline_store import connect_kline_db, is_cache_fresh, load_meta, ensure_kline_tables
from modules.market.market_sessions import is_trading_session
logger = logging.getLogger(__name__)
TZ = ZoneInfo("Asia/Shanghai")
FAST_PERIODS = frozenset({
"timeshare", "1m", "2m", "5m", "15m", "1h", "2h", "4h",
})
def sse_format(event: str, data: dict) -> str:
return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False, default=str)}\n\n"
@dataclass
class KlineSubscription:
symbol: str
period: str
market_code: str = ""
sina_code: str = ""
queue: queue.Queue = field(default_factory=queue.Queue)
class KlineStreamHub:
def __init__(self):
self._lock = threading.Lock()
self._subs: list[KlineSubscription] = []
def subscribe(
self,
symbol: str,
period: str,
market_code: str = "",
sina_code: str = "",
) -> KlineSubscription:
sub = KlineSubscription(
symbol=symbol.strip(),
period=(period or "15m").strip().lower(),
market_code=market_code.strip(),
sina_code=sina_code.strip(),
)
with self._lock:
self._subs.append(sub)
return sub
def unsubscribe(self, sub: KlineSubscription) -> None:
with self._lock:
try:
self._subs.remove(sub)
except ValueError:
pass
def _snapshot_subs(self) -> list[KlineSubscription]:
with self._lock:
return list(self._subs)
def publish(self, sub: KlineSubscription, event: str, data: dict) -> None:
try:
sub.queue.put_nowait({"event": event, "data": data})
except queue.Full:
pass
def _should_refresh(self, sub: KlineSubscription, db_path: str) -> bool:
chart_sym = ths_to_sina_chart_symbol(sub.symbol)
if not chart_sym:
return False
if is_trading_session() and sub.period in FAST_PERIODS:
return True
try:
from modules.core.paths import KLINE_DB_PATH
db_path = db_path or KLINE_DB_PATH
conn = connect_kline_db(db_path)
ensure_kline_tables(conn)
meta = load_meta(conn, chart_sym, sub.period)
conn.close()
if not meta:
return True
return not is_cache_fresh(sub.period, meta.get("updated_at", ""))
except Exception as exc:
logger.warning("kline refresh check failed: %s", exc)
return True
def worker_loop(
self,
db_path: str,
quote_fn: Callable[..., dict],
get_mode_fn: Optional[Callable[[], str]] = None,
) -> None:
while True:
try:
subs = self._snapshot_subs()
for sub in subs:
if not self._should_refresh(sub, db_path):
continue
try:
kline_data = fetch_market_klines(
sub.symbol,
sub.period,
db_path,
force_remote=True,
prefer_ctp=False,
)
if kline_data.get("bars"):
self.publish(sub, "kline", kline_data)
quote_data = quote_fn(
sub.symbol, sub.market_code, sub.sina_code,
)
if quote_data:
self.publish(sub, "quote", quote_data)
except Exception as exc:
logger.warning(
"kline stream refresh %s %s: %s",
sub.symbol, sub.period, exc,
)
except Exception as exc:
logger.warning("kline stream worker: %s", exc)
time.sleep(1)
kline_hub = KlineStreamHub()