feat: HK realtime via Eastmoney push2 API — no more 15-min delay
price_monitor.py now splits A-share (Tencent realtime) and HK (Eastmoney realtime): - fetch_all_prices() → A-shares only via qt.gtimg.cn - fetch_hk_eastmoney() → HK stocks via push2.eastmoney.com (real-time, free) - _fetch_hk_tencent_fallback() → fallback if Eastmoney fails (15-min delay) - Eastmoney API: market code 116, fields f43(price)/f170(chg%)/f60(prev_close)
This commit is contained in:
+127
-13
@@ -52,27 +52,27 @@ UA = "Mozilla/5.0"
|
|||||||
# ── 批量拉取价格 ──────────────────────────────────────────────────────────
|
# ── 批量拉取价格 ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def fetch_all_prices(codes):
|
def fetch_all_prices(codes):
|
||||||
"""腾讯批量行情API:一次请求拉取所有股票(A股+港股)
|
"""腾讯批量行情API:仅用于A股(沪市/深市)
|
||||||
A股:sh600110 / sz000001
|
A股:sh600110 / sz000001
|
||||||
港股:hk00700
|
港股已迁移至 fetch_hk_eastmoney()(东方财富实时行情)
|
||||||
返回 {code: (price, change, change_pct)}
|
返回 {code: (price, change, change_pct)}
|
||||||
"""
|
"""
|
||||||
if not codes:
|
if not codes:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# 构建批量查询串
|
# 只处理A股(6位代码),港股走东方财富
|
||||||
|
a_codes = [c for c in codes if len(str(c).strip()) == 6]
|
||||||
|
if not a_codes:
|
||||||
|
return {}
|
||||||
|
|
||||||
symbols = []
|
symbols = []
|
||||||
code_map = {} # symbol -> original_code
|
code_map = {}
|
||||||
for code in codes:
|
for code in a_codes:
|
||||||
code_s = str(code).strip()
|
code_s = str(code).strip()
|
||||||
if len(code_s) == 6:
|
|
||||||
# A股:沪市以5/6/9开头,深市以0/3开头
|
|
||||||
if code_s.startswith(('5', '6', '9')):
|
if code_s.startswith(('5', '6', '9')):
|
||||||
sym = f"sh{code_s}"
|
sym = f"sh{code_s}"
|
||||||
else:
|
else:
|
||||||
sym = f"sz{code_s}"
|
sym = f"sz{code_s}"
|
||||||
else:
|
|
||||||
sym = f"hk{code_s}"
|
|
||||||
symbols.append(sym)
|
symbols.append(sym)
|
||||||
code_map[sym] = code_s
|
code_map[sym] = code_s
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ def fetch_all_prices(codes):
|
|||||||
with urllib.request.urlopen(req, timeout=10) as r:
|
with urllib.request.urlopen(req, timeout=10) as r:
|
||||||
text = r.read().decode("gbk")
|
text = r.read().decode("gbk")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"⚠️ 批量拉取失败: {e}", file=sys.stderr)
|
print(f"⚠️ 腾讯A股拉取失败: {e}", file=sys.stderr)
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
results = {}
|
results = {}
|
||||||
@@ -91,7 +91,6 @@ def fetch_all_prices(codes):
|
|||||||
if not line or "=" not in line:
|
if not line or "=" not in line:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
# 格式: v_sh600110="1~诺德股份~600110~11.84~11.90~..."
|
|
||||||
raw_value = line.split("=", 1)[1].strip().strip('"').strip(";")
|
raw_value = line.split("=", 1)[1].strip().strip('"').strip(";")
|
||||||
fields = raw_value.split("~")
|
fields = raw_value.split("~")
|
||||||
if len(fields) < 6:
|
if len(fields) < 6:
|
||||||
@@ -111,6 +110,118 @@ def fetch_all_prices(codes):
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
# ── 港股实时行情(东方财富,无15分钟延迟)──────────────────────────────
|
||||||
|
|
||||||
|
def fetch_hk_eastmoney(codes):
|
||||||
|
"""东方财富港股实时行情 API — 免费、实时、无15分钟延迟。
|
||||||
|
|
||||||
|
API: push2.eastmoney.com
|
||||||
|
市场代码: 116 (港交所)
|
||||||
|
格式: 116.00700
|
||||||
|
|
||||||
|
返回 {code: (price, change, change_pct)}
|
||||||
|
|
||||||
|
Fallback: 失败时回退到腾讯 qt.gtimg.cn(15分钟延迟)
|
||||||
|
"""
|
||||||
|
if not codes:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
hk_codes = [str(c).strip() for c in codes if len(str(c).strip()) <= 5]
|
||||||
|
if not hk_codes:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
# 主通道:东方财富实时行情
|
||||||
|
try:
|
||||||
|
secids = ",".join(f"116.{c}" for c in hk_codes)
|
||||||
|
url = (f"https://push2.eastmoney.com/api/qt/stock/get"
|
||||||
|
f"?secid={secids}"
|
||||||
|
f"&fields=f43,f44,f45,f46,f47,f48,f57,f58,f169,f170,f60"
|
||||||
|
f"&fltt=2&invt=2")
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||||
|
with urllib.request.urlopen(req, timeout=8) as r:
|
||||||
|
data = json.loads(r.read().decode("utf-8"))
|
||||||
|
|
||||||
|
# 解析:可能是单条 {"data":{...}} 或多条 {"data":[{...},...]}
|
||||||
|
items = data.get("data")
|
||||||
|
if items is None:
|
||||||
|
pass
|
||||||
|
elif isinstance(items, dict):
|
||||||
|
items = [items]
|
||||||
|
|
||||||
|
if isinstance(items, list):
|
||||||
|
for item in items:
|
||||||
|
if not item:
|
||||||
|
continue
|
||||||
|
# 从 secid 反推原始代码: "116.00700" → "00700"
|
||||||
|
raw_secid = str(item.get("secid", item.get("code", "")))
|
||||||
|
code = raw_secid.replace("116.", "").replace("116.", "")
|
||||||
|
if not code:
|
||||||
|
continue
|
||||||
|
# 找原始codes中匹配的
|
||||||
|
matched = None
|
||||||
|
code_padded = code.zfill(5)
|
||||||
|
for c in hk_codes:
|
||||||
|
if c == code or c == code_padded or code_padded.endswith(c):
|
||||||
|
matched = c
|
||||||
|
break
|
||||||
|
if not matched:
|
||||||
|
continue
|
||||||
|
|
||||||
|
price = float(item.get("f43", 0)) / 100 if item.get("f43") else 0
|
||||||
|
prev_close = float(item.get("f60", 0)) / 100 if item.get("f60") else 0
|
||||||
|
change = round(price - prev_close, 2) if prev_close > 0 else 0
|
||||||
|
change_pct = str(item.get("f170", "0"))
|
||||||
|
if price > 0:
|
||||||
|
results[matched] = (price, change, change_pct)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"⚠️ 东方财富港股拉取失败: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
# Fallback: 腾讯 qt.gtimg.cn(15分钟延迟)
|
||||||
|
missing = [c for c in hk_codes if c not in results]
|
||||||
|
if missing:
|
||||||
|
try:
|
||||||
|
fallback = _fetch_hk_tencent_fallback(missing)
|
||||||
|
results.update(fallback)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _fetch_hk_tencent_fallback(codes):
|
||||||
|
"""腾讯港股行情(15分钟延迟,仅作 fallback)"""
|
||||||
|
symbols = [f"hk{c}" for c in codes]
|
||||||
|
url = f"http://qt.gtimg.cn/q={','.join(symbols)}"
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": UA})
|
||||||
|
with urllib.request.urlopen(req, timeout=10) as r:
|
||||||
|
text = r.read().decode("gbk")
|
||||||
|
|
||||||
|
code_map = {f"hk{c}": c for c in codes}
|
||||||
|
results = {}
|
||||||
|
for line in text.strip().split("\n"):
|
||||||
|
if "=" not in line:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
raw = line.split("=", 1)[1].strip().strip('"').strip(";")
|
||||||
|
fields = raw.split("~")
|
||||||
|
if len(fields) < 6:
|
||||||
|
continue
|
||||||
|
sym = line.split("=", 1)[0].strip().lstrip("v_")
|
||||||
|
orig = code_map.get(sym)
|
||||||
|
if not orig:
|
||||||
|
continue
|
||||||
|
price = float(fields[3]) if fields[3] else 0
|
||||||
|
prev_close = float(fields[4]) if fields[4] else 0
|
||||||
|
change = price - prev_close if prev_close > 0 else 0
|
||||||
|
change_pct = fields[32] if len(fields) > 32 and fields[32] else "0"
|
||||||
|
results[orig] = (price, change, change_pct)
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
continue
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
def refresh_data_prices():
|
def refresh_data_prices():
|
||||||
"""一次性刷新portfolio.json和watchlist.json的所有实时价"""
|
"""一次性刷新portfolio.json和watchlist.json的所有实时价"""
|
||||||
all_codes = set()
|
all_codes = set()
|
||||||
@@ -133,8 +244,11 @@ def refresh_data_prices():
|
|||||||
if not all_codes:
|
if not all_codes:
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
# 一次性批量拉取
|
# 分批拉取:A股走腾讯(实时) + 港股走东方财富(实时)
|
||||||
prices = fetch_all_prices(list(all_codes))
|
all_list = list(all_codes)
|
||||||
|
prices = fetch_all_prices(all_list) # A股(腾讯,实时)
|
||||||
|
hk_prices = fetch_hk_eastmoney(all_list) # 港股(东方财富,实时)
|
||||||
|
prices.update(hk_prices)
|
||||||
updated = 0
|
updated = 0
|
||||||
|
|
||||||
# 保存全量实时价快照(供报告管道消费,确保分析用最新数据)
|
# 保存全量实时价快照(供报告管道消费,确保分析用最新数据)
|
||||||
|
|||||||
Reference in New Issue
Block a user