From ddd2e12b38b3c6814d31899576875fe08ef916a9 Mon Sep 17 00:00:00 2001 From: hmo Date: Fri, 14 Aug 2026 21:03:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B8=AF=E8=82=A1=E9=80=9A=E4=B8=AA?= =?UTF-8?q?=E8=82=A1=E9=95=BF=E5=8E=86=E5=8F=B2=E5=9B=9E=E5=A1=AB=E8=84=9A?= =?UTF-8?q?=E6=9C=AC(=E6=AF=8F=E5=8F=AA2000=E6=A0=B9=E7=BA=A68=E5=B9=B4,?= =?UTF-8?q?=E8=85=BE=E8=AE=AFhk=E5=89=8D=E7=BC=80,INSERT=20OR=20IGNORE?= =?UTF-8?q?=E5=B9=82=E7=AD=89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/profile-scripts/backfill_hk_stocks.py | 93 ++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 deploy/profile-scripts/backfill_hk_stocks.py diff --git a/deploy/profile-scripts/backfill_hk_stocks.py b/deploy/profile-scripts/backfill_hk_stocks.py new file mode 100644 index 00000000..d7766bde --- /dev/null +++ b/deploy/profile-scripts/backfill_hk_stocks.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""backfill_hk_stocks.py — 港股通个股长历史日K回填(周末·阶段4前置) + +背景:stock_daily 港股个股只有约2年(2024-07起,522条),回测样本不足。 +本脚本对港股通名单(hk_connect_stocks 613只)每只拉 2000 根(腾讯接口单次上限,约8年), +回填入 stock_daily,供港股策略回测 + 实盘技术分析。 + +数据源:腾讯日K接口 hk 前缀(单次上限2000根,恒指回填已验证)。 + http://ifzq.gtimg.cn/appstock/app/fqkline/get?param=hk{code},day,,,2000,qfq + +跑法(heavy_run 受控,约10分钟): + flock + nice19 + ionice,见 heavy_run.sh 等效约束 +幂等:INSERT OR IGNORE(code,date 唯一键),可重复执行。 +""" +import json +import sqlite3 +import sys +import time +import urllib.request +from pathlib import Path + +DB_PATH = "/home/hmo/MoFin/data/mofin.db" +UA = "Mozilla/5.0" +COUNT = 2000 # 腾讯单次上限 +SLEEP = 0.3 # 限速防封(铁律) + + +def fetch_klines(code): + """拉单只港股 2000 根日K(腾讯 hk 前缀)""" + url = (f"http://ifzq.gtimg.cn/appstock/app/fqkline/get?" + f"param=hk{code},day,,,{COUNT},qfq") + req = urllib.request.Request(url, headers={"User-Agent": UA}) + opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) + with opener.open(req, timeout=20) as r: + text = r.read().decode("utf-8", errors="replace") + data = json.loads(text) + node = (data.get("data") or {}).get(f"hk{code}", {}) + return node.get("qfqday") or node.get("day") or [] + + +def main(): + conn = sqlite3.connect(DB_PATH, timeout=30) + conn.execute("PRAGMA busy_timeout=30000") + codes = [r[0] for r in conn.execute( + "SELECT code FROM hk_connect_stocks WHERE is_active=1 ORDER BY code").fetchall()] + print(f"港股通个股长历史回填:{len(codes)} 只,每只 {COUNT} 根", flush=True) + + total_new = 0 + ok = empty = fail = 0 + t0 = time.time() + for idx, code in enumerate(codes): + try: + bars = fetch_klines(code) + if not bars: + empty += 1 + continue + new = 0 + for b in bars: + try: + date, open_, close, high, low = b[0], b[1], b[2], b[3], b[4] + volume = float(b[5]) if len(b) > 5 and b[5] not in (None, "") else 0 + amount = float(b[6]) if len(b) > 6 and b[6] not in (None, "") else 0 + except (IndexError, ValueError): + continue + cur = conn.execute( + "INSERT OR IGNORE INTO stock_daily (code, date, open, close, high, low, volume, amount) " + "VALUES (?,?,?,?,?,?,?,?)", + (code, date, float(open_), float(close), float(high), float(low), volume, amount)) + new += cur.rowcount + total_new += new + ok += 1 + if (idx + 1) % 50 == 0: + conn.commit() + print(f" [{idx+1}/{len(codes)}] ok={ok} empty={empty} fail={fail} 新增{total_new}根 | {time.time()-t0:.0f}s", flush=True) + except Exception as e: + fail += 1 + if fail <= 5: + print(f" FAIL {code}: {str(e)[:60]}", flush=True) + time.sleep(SLEEP) + conn.commit() + + # 验证 + stat = conn.execute( + "SELECT COUNT(DISTINCT code), MIN(date), MAX(date) FROM stock_daily WHERE length(code)=5").fetchone() + conn.close() + print(f"\n完成:ok={ok} empty={empty} fail={fail} 新增{total_new}根 耗时{time.time()-t0:.0f}s", flush=True) + print(f"stock_daily 港股: {stat[0]} 只, {stat[1]} ~ {stat[2]}", flush=True) + return 0 + + +if __name__ == "__main__": + sys.exit(main())