#!/usr/bin/env python3 """pool_news_collector.py — 池子新闻采集(自选+持仓,盘中高频) 2026-08-13 新增(老莫架构):盘前全量跑(选股),盘中只跑池子里的(自选+持仓),增加频率。 - 盘前:news_collector_full b0-b7(全市场 4272 只,8批) - 盘中:pool_news_collector(池子 82 只,*/30 盘中) 池子 = 持仓(holdings shares>0) + 自选(holding_strategies 自选策略) """ import sys, os, json, time, sqlite3 from datetime import datetime, timedelta from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from news_collector import fetch_stock_news, init_table # ── 消息通道统一路由(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") SLEEP = 0.4 # 请求间隔(防东财封) RECENT_DAYS = 3 # 只存近 3 天新闻(news3 因子口径) def _singleton_guard(tag): lock_dir = Path("/tmp/mofin_locks") lock_dir.mkdir(exist_ok=True) try: import fcntl 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: return None def get_pool_codes(conn): """池子:持仓 + 自选(去重)""" codes = set() # 持仓 for r in conn.execute("SELECT DISTINCT code FROM holdings WHERE is_active=1 AND shares>0"): codes.add(r[0]) # 自选 for r in conn.execute("SELECT DISTINCT code FROM holding_strategies WHERE status='active' AND decision_type='自选策略'"): codes.add(r[0]) return sorted(codes) def main(): tag = "pool_news_collector" guard = _singleton_guard(tag) if guard is None: print(f"[{tag}] 已有实例在运行,跳过", file=sys.stderr) return conn = sqlite3.connect(DB_PATH) init_table(conn) cur = conn.cursor() codes = get_pool_codes(conn) print(f"[{tag}] {datetime.now().strftime('%H:%M:%S')} 池子新闻采集 开始", flush=True) print(f" 池子: {len(codes)} 只(持仓+自选)", flush=True) since = (datetime.now() - timedelta(days=RECENT_DAYS)).strftime("%Y-%m-%d") t0 = time.time() ok = empty = fail = new = 0 for idx, code in enumerate(codes, 1): try: arts = fetch_stock_news(code, max_pages=1) if not arts: empty += 1 continue n = 0 for a in arts: dstr = (a.get("date") or "")[:10] if dstr and dstr >= since: try: cur.execute( "INSERT OR IGNORE INTO stock_news (code, title, content, source, date, url) " "VALUES (?,?,?,?,?,?)", (code, a.get("title", ""), a.get("content", ""), a.get("source", "eastmoney"), a.get("date", ""), a.get("url", ""))) if cur.rowcount: n += 1 except Exception: pass new += n ok += 1 except Exception as e: fail += 1 if fail <= 5: print(f" FAIL {code}: {str(e)[:60]}", flush=True) if idx % 20 == 0: conn.commit() print(f" [{idx}/{len(codes)}] ok={ok} empty={empty} fail={fail} 新增={new} | {time.time()-t0:.0f}s", flush=True) time.sleep(SLEEP) conn.commit() dt = time.time() - t0 print(f"[{tag}] 完成: {ok}/{len(codes)} 成功, {empty} 无新闻, {fail} 失败, 新增 {new} 条, 耗时 {dt:.0f}s", flush=True) # ── 2026-08-17 老莫:推荐必须带所依据的策略 ── # 打印池子内每只股票的当前策略快照(SSOT:holding_strategies 最新记录) # 知微执行本 job 时据此引用真实策略(版本/重评时间/止损/止盈/买入区/信号/操作), # 不得凭 strategy_history 旧快照或记忆自由发挥。 print("\n===== 当前策略快照(推荐必须引用以下真实策略) =====", flush=True) try: _cols = [d[1] for d in conn.execute("PRAGMA table_info(holding_strategies)").fetchall()] for _code in codes: _r = conn.execute( "SELECT * FROM holding_strategies WHERE code=? AND status='active' ORDER BY rowid DESC LIMIT 1", (_code,)).fetchone() if not _r: continue _d = dict(zip(_cols, _r)) _sig = _d.get("timing_signal") or "?" _sl = _d.get("stop_loss"); _tp = _d.get("take_profit") _el = _d.get("entry_low"); _eh = _d.get("entry_high") _ver = _d.get("version") or _d.get("decision_type") or "?" _ra = (_d.get("reassessed_at") or _d.get("updated_at") or "")[:19] _act = str(_d.get("action") or "")[:120] print(f"[策略] {_code} {_d.get('name','')} | 版本={_ver} 重评={_ra} | " f"信号={_sig} 止损={_sl} 止盈={_tp} 买入区={_el}~{_eh} | 操作: {_act}", flush=True) print("===== 策略快照结束 =====", flush=True) except Exception as _e: print(f"[策略快照输出失败] {_e}", file=sys.stderr) conn.close() if __name__ == "__main__": main()