feat: 港股通个股长历史回填脚本(每只2000根约8年,腾讯hk前缀,INSERT OR IGNORE幂等)
This commit is contained in:
@@ -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())
|
||||
Reference in New Issue
Block a user