Files
MoFin/deploy/profile-scripts/daily_kline_collector.py
T

178 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""daily_kline_collector.py — 全市场日K采集(stock_daily 日常刷新)
背景(2026-08-12 架构补缺):
stock_daily 全市场 4272 只数据来自历史回填,日常无采集 cron——
仅 refresh_mtf_cache 顺带写持仓+自选+指数(122只),7月起全市场数据断层。
本脚本补齐:收盘后全市场日K刷新,供 12 维重评/策略扫描直接读本地。
数据源:腾讯前复权日Kmarket_data.fetch_tx_klines,与 stock_daily 零偏差)
调度:分 2 个 cronsh/sz 分批,600s 护栏内,0.05s 间隔避免被封)
sh: 10 16 * * 1-5 → daily_kline_collector.py --prefix sh
sz: 25 16 * * 1-5 → daily_kline_collector.py --prefix sz
港股批(阶段2B 新增,2026-08-14):
python3 daily_kline_collector.py hk (或 --prefix hk
股票源 = get_stock_pool(market='hk')hk_connect_stocks 名单 + 港股持仓/自选),
fetch_tx_klines 经 kline_symbol 自动加 hk 前缀(hk00700),接口已兼容港股
qfqday/day 双键)。hk_connect_stocks 表不存在时优雅降级:打印提示并跳过。
规范:单例守卫(5.3) + INSERT OR REPLACE 幂等 + 限速防封 + 分批 commit
"""
import sys, os, json, time, sqlite3, fcntl
from pathlib import Path
from datetime import datetime
sys.path.insert(0, str(Path(__file__).parent))
from market_data import fetch_tx_klines, get_stock_pool
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
SLEEP = 0.05 # 请求间隔(≈5 请求/秒,避免被腾讯封)
DATALEN = 75 # 拉 75 根(>70 满足 fetch_tx_klines 下限),只写最新 5 根
WRITE_RECENT = 5 # 只写最新 5 根(历史已有,增量刷新)
BATCH_COMMIT = 50 # 每 50 只 commit 一次
def _singleton_guard(script_tag):
"""单例守卫(规范5.3):防重复实例并发写"""
lock_dir = Path("/tmp/mofin_locks")
lock_dir.mkdir(exist_ok=True)
lock_path = lock_dir / f"{script_tag}.lock"
try:
fd = os.open(str(lock_path), os.O_CREAT | os.O_RDWR)
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
return fd
except OSError:
print(f"[{script_tag}] 已有实例在运行,退出", flush=True)
sys.exit(0)
def get_codes(conn, prefix):
"""全市场 A 股代码(stock_daily distinct,与回测/扫描同口径)"""
rows = conn.execute(
"SELECT DISTINCT code FROM stock_daily WHERE length(code)=6"
).fetchall()
codes = [str(r[0]) for r in rows]
if prefix == "sh":
# 6/9/5 开头 → 上海(fetch_tx_klines 自动加 sh 前缀)
return sorted([c for c in codes if c[0] in ("5", "6", "9")])
elif prefix == "sz_a":
# 深圳前半批(0 开头)——2026-08-13 分批:sz 2180只超时,分 sz_a/sz_b
return sorted([c for c in codes if c[0] == "0"])
elif prefix == "sz_b":
# 深圳后半批(3 开头)
return sorted([c for c in codes if c[0] == "3"])
else:
# 0/3 开头 → 深圳(全部,兼容旧调用)
return sorted([c for c in codes if c[0] in ("0", "3")])
def get_hk_codes():
"""港股代码:get_stock_pool(market='hk')hk_connect_stocks 名单 + 港股持仓/自选)
hk_connect_stocks 表不存在(阶段2A 并行开发中)→ 优雅降级:
打印提示后跳过港股批,不报错;表存在但港股池为空同样跳过。
"""
# 先探测表是否存在,给精确的降级提示(get_stock_pool 内部也容错,双保险)
try:
with sqlite3.connect(str(DB_PATH), timeout=5) as conn:
conn.execute("SELECT 1 FROM hk_connect_stocks LIMIT 1").fetchone()
except sqlite3.OperationalError as e:
if "no such table" in str(e).lower():
print(" hk_connect_stocks 表不存在,跳过港股批", flush=True)
return []
raise
codes, _ = get_stock_pool(market="hk")
codes = sorted(codes)
if not codes:
print(" 港股池为空,跳过港股批", flush=True)
return []
return codes
def main():
prefix = "sz"
limit = 0
i = 1
while i < len(sys.argv):
a = sys.argv[i]
if a.startswith("--prefix"):
prefix = a.split("=")[-1] if "=" in a else sys.argv[i + 1]
if a.startswith("--limit"):
limit = int(a.split("=")[-1] if "=" in a else sys.argv[i + 1])
i += 1
# 位置子命令:python3 daily_kline_collector.py hk(与 --prefix hk 等价)
if len(sys.argv) > 1 and sys.argv[1] == "hk":
prefix = "hk"
tag = f"daily_kline_collector_{prefix}"
_fd = _singleton_guard(tag)
t0 = time.time()
print(f"[{tag}] {datetime.now().strftime('%H:%M:%S')} 全市场日K采集({prefix}) 开始", flush=True)
conn = sqlite3.connect(str(DB_PATH), timeout=30)
if prefix == "hk":
codes = get_hk_codes() # 港股池:hk_connect_stocks + 港股持仓/自选
else:
codes = get_codes(conn, prefix) # A股池:stock_daily distinct 分批
if limit > 0:
codes = codes[:limit]
print(f" [测试模式] 只跑前 {limit} 只", flush=True)
print(f" 股票池: {len(codes)} 只", flush=True)
cur = conn.cursor()
ok = fail = empty = written = 0
for i, code in enumerate(codes, 1):
try:
bars = fetch_tx_klines(code, datalen=DATALEN)
if not bars:
empty += 1
continue
# 只写最新 WRITE_RECENT 根(历史已有,增量刷新,减少写入)
for b in bars[-WRITE_RECENT:]:
cur.execute(
"INSERT OR REPLACE INTO stock_daily (code, date, open, close, high, low, volume) "
"VALUES (?,?,?,?,?,?,?)",
(code, b["date"], b["open"], b["close"], b["high"], b["low"], b["volume"]),
)
written += 1
ok += 1
except Exception as e:
# 2026-08-18 database is locked 指数退避重试(工单t_17ba6f5d:sz_b整点调度撞锁致2只失败)
if "database is locked" in str(e).lower() or "locked" in str(e).lower():
import random as _rd
_retry = 0
_ok = False
while _retry < 3 and not _ok:
try:
time.sleep(0.5 * (2 ** _retry) + _rd.uniform(0, 0.3))
for b in bars[-WRITE_RECENT:]:
cur.execute(
"INSERT OR REPLACE INTO stock_daily (code, date, open, close, high, low, volume) "
"VALUES (?,?,?,?,?,?,?)",
(code, b["date"], b["open"], b["close"], b["high"], b["low"], b["volume"]),
)
conn.commit()
_ok = True
except Exception:
_retry += 1
if _ok:
ok += 1
continue
fail += 1
if fail <= 5:
print(f" FAIL {code}: {e}", flush=True)
if i % BATCH_COMMIT == 0:
conn.commit()
print(f" [{i}/{len(codes)}] ok={ok} empty={empty} fail={fail} written={written} | {time.time()-t0:.0f}s", flush=True)
time.sleep(SLEEP)
conn.commit()
conn.close()
dt = time.time() - t0
print(f"[{tag}] 完成: {ok}/{len(codes)} 成功, {empty} 空, {fail} 失败, 写入 {written} 根, 耗时 {dt:.0f}s", flush=True)
if __name__ == "__main__":
main()