diff --git a/price_monitor.py b/price_monitor.py index e080037..296d647 100644 --- a/price_monitor.py +++ b/price_monitor.py @@ -52,27 +52,27 @@ UA = "Mozilla/5.0" # ── 批量拉取价格 ────────────────────────────────────────────────────────── def fetch_all_prices(codes): - """腾讯批量行情API:一次请求拉取所有股票(A股+港股) + """腾讯批量行情API:仅用于A股(沪市/深市) A股:sh600110 / sz000001 - 港股:hk00700 + 港股已迁移至 fetch_hk_eastmoney()(东方财富实时行情) 返回 {code: (price, change, change_pct)} """ if not codes: return {} - # 构建批量查询串 + # 只处理A股(6位代码),港股走东方财富 + a_codes = [c for c in codes if len(str(c).strip()) == 6] + if not a_codes: + return {} + symbols = [] - code_map = {} # symbol -> original_code - for code in codes: + code_map = {} + for code in a_codes: code_s = str(code).strip() - if len(code_s) == 6: - # A股:沪市以5/6/9开头,深市以0/3开头 - if code_s.startswith(('5', '6', '9')): - sym = f"sh{code_s}" - else: - sym = f"sz{code_s}" + if code_s.startswith(('5', '6', '9')): + sym = f"sh{code_s}" else: - sym = f"hk{code_s}" + sym = f"sz{code_s}" symbols.append(sym) code_map[sym] = code_s @@ -82,7 +82,7 @@ def fetch_all_prices(codes): with urllib.request.urlopen(req, timeout=10) as r: text = r.read().decode("gbk") except Exception as e: - print(f"⚠️ 批量拉取失败: {e}", file=sys.stderr) + print(f"⚠️ 腾讯A股拉取失败: {e}", file=sys.stderr) return {} results = {} @@ -91,7 +91,6 @@ def fetch_all_prices(codes): if not line or "=" not in line: continue try: - # 格式: v_sh600110="1~诺德股份~600110~11.84~11.90~..." raw_value = line.split("=", 1)[1].strip().strip('"').strip(";") fields = raw_value.split("~") if len(fields) < 6: @@ -111,6 +110,118 @@ def fetch_all_prices(codes): 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(): """一次性刷新portfolio.json和watchlist.json的所有实时价""" all_codes = set() @@ -133,8 +244,11 @@ def refresh_data_prices(): if not all_codes: return 0 - # 一次性批量拉取 - prices = fetch_all_prices(list(all_codes)) + # 分批拉取:A股走腾讯(实时) + 港股走东方财富(实时) + 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 # 保存全量实时价快照(供报告管道消费,确保分析用最新数据)