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

120 lines
4.7 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
"""fundamentals_full_refresh.py — 全市场基本面刷新(stock_fundamentals 日常刷新)
背景(2026-08-12 架构补缺):
stock_fundamentals 全市场靠回填(3979 只旧),fundamentals_refresh 只刷持仓/自选。
p_oversold 的 mcap_q/pe_q 分位需要全市场 PE/PB/市值新鲜数据。
数据源:腾讯批量行情 APIqt.gtimg.cn/q=code,每批 100 只)
parts[39]=PE, parts[44]=总市值, parts[45]=流通市值, parts[46]=PB
老莫改直连后实测可用。全市场 4013 只 / 100 = 41 批,~2 分钟,单 cron 护栏内。
调度:盘后 35 16 * * 1-5stock_daily 采集完成后)
规范:单例守卫(5.3) + INSERT OR REPLACE 幂等 + 批量限速防封 + eps=price/pe
"""
import sys, os, re, time, sqlite3, fcntl, urllib.request
from pathlib import Path
from datetime import datetime
# ── 消息通道统一路由(broadcast/xmpp by delivery) ──
try:
from messenger import install_stdio_hook as _msh
_msh()
except Exception:
pass
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
BATCH = 100 # 腾讯推荐上限 100/批
SLEEP = 0.2 # 批间隔(防封)
def _singleton_guard(tag="fundamentals_full_refresh.py"):
lock_dir = Path("/tmp/mofin_locks")
lock_dir.mkdir(exist_ok=True)
try:
fd = os.open(str(lock_dir / f"{tag}.lock"), os.O_CREAT | os.O_RDWR)
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
return fd
except OSError:
print(f"[{tag}] 已有实例在运行,退出", flush=True)
sys.exit(0)
def prefix_of(code):
return "sh" if code.startswith(("5", "6", "9")) else "sz"
def fetch_qq_batch(symbols):
"""腾讯批量行情(每批100只),返回 {code: {pe,pb,price,mcap_total,mcap_flow}}"""
if not symbols:
return {}
results = {}
url = f"http://qt.gtimg.cn/q={','.join(symbols)}"
try:
req = urllib.request.Request(url, headers={"User-Agent": UA})
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
with opener.open(req, timeout=15) as r:
text = r.read().decode("gbk", errors="ignore")
for line in text.strip().split("\n"):
if "~" not in line:
continue
parts = line.split("~")
if len(parts) < 47:
continue
code = parts[2]
price = float(parts[3]) if parts[3] else 0
pe = float(parts[39]) if parts[39] else 0
mcap_t = float(parts[44]) if parts[44] else 0 # 总市值(亿)
mcap_f = float(parts[45]) if parts[45] else 0 # 流通市值(亿)
pb = float(parts[46]) if parts[46] else 0
if price > 0:
results[code] = {"price": price, "pe": pe, "pb": pb,
"mcap_total": mcap_t, "mcap_flow": mcap_f}
except Exception as e:
print(f" batch fetch error: {str(e)[:60]}", flush=True)
return results
def main():
_fd = _singleton_guard()
t0 = time.time()
print(f"[fundamentals_full_refresh] {datetime.now().strftime('%H:%M:%S')} 全市场基本面刷新开始", flush=True)
conn = sqlite3.connect(str(DB_PATH), timeout=30)
codes = [str(r[0]) for r in conn.execute(
"SELECT DISTINCT code FROM stock_daily WHERE length(code)=6 ORDER BY code").fetchall()]
print(f" 股票池: {len(codes)} 只", flush=True)
cur = conn.cursor()
ok = fail = written = 0
symbols = [f"{prefix_of(c)}{c}" for c in codes]
for i in range(0, len(symbols), BATCH):
batch = symbols[i:i + BATCH]
data = fetch_qq_batch(batch)
for code, d in data.items():
eps = round(d["price"] / d["pe"], 2) if d["pe"] > 0 else 0
try:
cur.execute(
"INSERT OR REPLACE INTO stock_fundamentals (code, pe, pb, eps, mcap_total, mcap_flow, updated_at) "
"VALUES (?,?,?,?,?,?,datetime('now','localtime'))",
(code, d["pe"], d["pb"], eps, d["mcap_total"], d["mcap_flow"]))
written += 1
ok += 1
except Exception:
fail += 1
conn.commit()
done = min(i + BATCH, len(symbols))
print(f" [{done}/{len(symbols)}] ok={ok} fail={fail} written={written} | {time.time()-t0:.0f}s", flush=True)
time.sleep(SLEEP)
conn.commit()
total = conn.execute("SELECT COUNT(*) FROM stock_fundamentals").fetchone()[0]
conn.close()
dt = time.time() - t0
print(f"[fundamentals_full_refresh] 完成: {written} 只写入, 表总数 {total}, 耗时 {dt:.0f}s", flush=True)
if __name__ == "__main__":
main()