#!/usr/bin/env python3 # -*- coding: utf-8 -*- """backfill_hk_index.py — 恒生指数(hkHSI)历史日K回填(一次性脚本) 背景(阶段3 港股接入):stock_daily 的 hkHSI 仅 2024 起(643 条),温区回填 需要更长历史(对齐 A 股约 10 年)。本脚本从腾讯日K接口拉取 hkHSI 全量历史 (约 3000 根 ≈ 12 年),写入 stock_daily。 数据源(与 market_data.fetch_tx_klines 同一腾讯接口,hkHSI 已验证返回 day 键): http://ifzq.gtimg.cn/appstock/app/fqkline/get?param=hkHSI,day,,,{count},qfq 用法: python3 backfill_hk_index.py # 拉 3000 根(约12年) python3 backfill_hk_index.py --count 4000 写入:INSERT OR IGNORE((code,date) 唯一键防重复,已有日期跳过)。 """ import json import sqlite3 import sys import urllib.request from pathlib import Path _SCRIPT_DIR = Path(__file__).resolve().parent _MOFIN_ROOT = _SCRIPT_DIR.parent.parent DB_PATH = Path(_MOFIN_ROOT) / "data" / "mofin.db" UA = "Mozilla/5.0" INDEX_CODE = "hkHSI" PAGE_SIZE = 2000 # 腾讯接口单次上限(实测 count=2000 ok, 2500 报 param error) def _fetch_page(count, end_date=""): """拉一页:param=hkHSI,day,start,end,count,qfq。end_date 空=截至最新""" url = (f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get?" f"param={INDEX_CODE},day,,{end_date},{count},qfq") req = urllib.request.Request(url, headers={"User-Agent": UA}) opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) with opener.open(req, timeout=30) as r: text = r.read().decode("utf-8", errors="replace") data = json.loads(text) node = (data.get("data") or {}).get(INDEX_CODE, {}) return node.get("qfqday") or node.get("day") or [] def fetch_hsi(count=3000): """分页拉取 hkHSI 日K(单次上限 2000,超过则向前翻页)""" from datetime import datetime as _dt, timedelta as _td import time as _t uniq = {} end_date = "" while len(uniq) < count: want = min(PAGE_SIZE, count - len(uniq) + 5) page = _fetch_page(want, end_date) if not page: break new_cnt = 0 for b in page: if b[0] not in uniq: uniq[b[0]] = b new_cnt += 1 if new_cnt == 0: break # 没有新数据,历史到头 # 向前翻页:以本页最早日期的前一天为 end earliest = page[0][0] end_date = (_dt.strptime(earliest, "%Y-%m-%d") - _td(days=1)).strftime("%Y-%m-%d") if len(page) < 100: # 不足一页说明历史拉完 break _t.sleep(1) # 翻页间隔,遵守访问频率 return [uniq[d] for d in sorted(uniq)] def main(): count = 3000 for a in sys.argv[1:]: if a.startswith("--count"): count = int(a.split("=")[-1] if "=" in a else sys.argv[sys.argv.index(a) + 1]) print(f"拉取 {INDEX_CODE} 最近 {count} 根日K...") bars = fetch_hsi(count) if not bars: print("拉取失败/为空,终止") return 1 print(f"拉到 {len(bars)} 根,时间范围 {bars[0][0]} ~ {bars[-1][0]}") conn = sqlite3.connect(str(DB_PATH), timeout=30) conn.execute("PRAGMA busy_timeout=30000") # stock_daily 列:code, date, open, close, high, low, volume(与 daily_kline_collector 一致) inserted = 0 skipped = 0 for b in bars: # 格式: [date, open, close, high, low, volume, ...] 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 except (IndexError, ValueError): skipped += 1 continue cur = conn.execute( "INSERT OR IGNORE INTO stock_daily (code, date, open, close, high, low, volume) " "VALUES (?,?,?,?,?,?,?)", (INDEX_CODE, date, float(open_), float(close), float(high), float(low), volume) ) inserted += cur.rowcount conn.commit() # 验证 total = conn.execute( "SELECT COUNT(*), MIN(date), MAX(date) FROM stock_daily WHERE code=?", (INDEX_CODE,) ).fetchone() conn.close() print(f"写入 {inserted} 根(跳过 {skipped} 根格式异常 / 已有日期自动 IGNORE)") print(f"stock_daily 的 {INDEX_CODE}: 共 {total[0]} 条,{total[1]} ~ {total[2]}") return 0 if __name__ == "__main__": sys.exit(main())