Files
AlphaMaster/web/data_sources/tongdaxin_source.py
T
2026-07-15 23:18:36 +08:00

209 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""通达信数据源(pytdx,免费行情服务器,A 股 / 指数)。"""
from __future__ import annotations
import threading
import time
from datetime import datetime, timezone, timedelta
from web.data_sources.base import Bar, DataSource, DataSourceUnavailable
# 通达信行情服务器(多个备选)
_SERVERS = [
("115.238.90.165", 7709),
("180.153.18.170", 7709),
("119.147.212.81", 7709),
("14.17.75.71", 7709),
("59.173.18.77", 7709),
]
# 项目周期 -> pytdx category
_CAT = {
"1m": 8,
"5m": 0,
"15m": 1,
"30m": 2,
"1h": 3,
"1d": 9,
"1w": 5,
"1M": 6,
}
_PRESETS = ["600519", "000001", "300750", "601318", "000858", "sh000001", "sz399006"]
def _parse_market(code: str) -> tuple[int, str]:
"""返回 (market, pure_code)。1=上海, 0=深圳。"""
c = code.strip().upper()
if c.startswith("SH"):
return 1, c[2:]
if c.startswith("SZ"):
return 0, c[2:]
if c[:1] in ("6", "5", "9") or c.startswith("11") or c.startswith("13"):
return 1, c
return 0, c
_CST = timezone(timedelta(hours=8)) # 通达信返回的 datetime 为北京时间(UTC+8)
def _is_index(market: int, code: str) -> bool:
"""判断是否为指数代码。
上证指数系列:market=1 且 code 以 000 开头(如 000001 上证指数、000300 沪深300)。
深证指数系列:market=0 且 code 以 399 开头(如 399001 深证成指、399006 创业板指)。
注意:深圳市场 000xxx 是股票(如 000001 平安银行),只有 399xxx 才是深证指数,
因此不能用纯 code 前缀判断,必须结合 market。
"""
if market == 1 and code.startswith("000"):
return True
if market == 0 and code.startswith("399"):
return True
return False
def _parse_dt(s: str) -> int:
"""通达信 datetime(北京时间,如 '2026-07-15 15:00')-> 收盘时刻的 Unix 秒。
返回值语义为「该 bar 的收盘时刻(UTC 秒)」;drop_forming / _ensure_closed_bars
据此判断是否已收盘(ts > now 即仍在形成中)。
"""
for fmt in ("%Y-%m-%d %H:%M", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
try:
return int(datetime.strptime(str(s), fmt).replace(tzinfo=_CST).timestamp())
except ValueError:
continue
return 0
def _looks_corrupted(raw) -> bool:
"""检测返回数据是否乱码(datetime 年份异常)。
典型场景:对指数误用 get_security_bars,第 2 条起 datetime 损坏(如 7772-67-85);
或纯数字指数代码未带 sh/sz 前缀(如 000300)导致市场误判后取到异常数据。
"""
if not raw:
return False
bad = 0
for r in raw:
s = str(r.get("datetime", ""))
y = int(s[:4]) if len(s) >= 4 and s[:4].isdigit() else -1
if not (1990 <= y <= 2035):
bad += 1
return bad > 0 and bad >= len(raw) * 0.3
class TongdaxinSource(DataSource):
kind = "tongdaxin"
label = "通达信"
def __init__(self) -> None:
self._api = None
self._lock = threading.Lock()
def available(self) -> tuple[bool, str]:
try:
import pytdx # noqa: F401
except ImportError:
return (False, "未安装 pytdx:pip install pytdx")
return (True, "免费行情服务器 · A 股 / 指数")
def supported_timeframes(self) -> list[str]:
return list(_CAT.keys())
def preset_symbols(self) -> list[str]:
return list(_PRESETS)
def connect(self) -> None:
if self._api is not None:
return
try:
from pytdx.hq import TdxHq_API
except ImportError as exc:
raise DataSourceUnavailable("未安装 pytdx") from exc
api = TdxHq_API()
for host, port in _SERVERS:
try:
if api.connect(host, port):
self._api = api
return
except Exception:
continue
raise DataSourceUnavailable("通达信所有行情服务器连接失败")
def disconnect(self) -> None:
if self._api is not None:
try:
self._api.disconnect()
except Exception:
pass
self._api = None
def _fetch_raw(self, cat: int, market: int, code: str, want: int, is_index: bool):
"""指数走 get_index_bars,股票走 get_security_bars。
通达信协议规定指数必须用 get_index_bars;若对指数用 get_security_bars,
返回数据从第 2 条起 datetime 会损坏(年份变成 7772、228200 等乱码)。
"""
if is_index:
return self._api.get_index_bars(cat, market, code, 0, want)
return self._api.get_security_bars(cat, market, code, 0, want)
def fetch_bars(
self, symbol: str, timeframe: str, n: int, drop_forming: bool = True
) -> list[Bar]:
"""拉取 K 线。
注意:返回的是不复权数据(pytdx 限制),历史含除权跳空;volume 单位为
「手」(1手=100股),与 OKX/MT5 的 volume 量纲不同,跨源不可比。
"""
if timeframe not in _CAT:
raise DataSourceUnavailable(f"通达信不支持周期 {timeframe}")
market, code = _parse_market(symbol)
cat = _CAT[timeframe]
want = min(max(n + 2, 20), 800) # 单次上限 800
is_index = _is_index(market, code)
with self._lock:
self.connect()
try:
raw = self._fetch_raw(cat, market, code, want, is_index)
except Exception:
# 连接可能失效,重连一次
self._api = None
self.connect()
raw = self._fetch_raw(cat, market, code, want, is_index)
if not raw:
raise DataSourceUnavailable(
f"通达信无数据:{symbol}。请确认代码正确;指数需带 sh/sz 前缀(如 sh000001)。"
)
if _looks_corrupted(raw):
# 数据乱码通常意味着接口选错(指数误用股票接口)或代码/市场不匹配。
# 纯数字指数代码(如 000300)未带 sh/sz 前缀时会误判市场,此处给出明确提示。
raise DataSourceUnavailable(
f"通达信返回数据异常:{symbol}。若为指数请使用 sh/sz 前缀"
f"(如 sh000001、sz399006),股票代码请确认无误。"
)
bars: list[Bar] = []
for r in raw:
bars.append(
Bar(
ts=_parse_dt(r.get("datetime", "")),
open=float(r["open"]),
high=float(r["high"]),
low=float(r["low"]),
close=float(r["close"]),
volume=float(r.get("vol", 0.0) or 0.0), # 单位:手(1手=100股)
)
)
bars.sort(key=lambda b: b.ts) # 保证升序
# 剔除尚未收盘的 bar:通达信 datetime 为收盘时刻(北京时间),_parse_dt
# 返回的 ts 即收盘时刻的 UTC 秒;ts > now 说明该 bar 仍在形成中。
# (不再盲删最后一条,以免盘后把当天已收盘 bar 误删,导致 last_bar 滞后一天。)
if drop_forming and bars:
now = time.time()
while bars and int(bars[-1].ts) > now:
bars.pop()
return bars[-n:]