#!/usr/bin/env python3 # -*- coding: utf-8 -*- """build_panel_hk_v2.py — 港股 12维面板 v2(补行业动量+估值分位) 在 v1(大盘+个股技术)基础上补: - sec_ret20/sec_above:港股行业动量(行业归属 stock_sectors + 日K等权均值) - pe_q/pb_q/mcap_q:估值分位(stock_fundamentals_history,每日截面分位) 输出:/tmp/panel_12d_hk.pkl(覆盖 v1) """ import sys, sqlite3 import numpy as np import pandas as pd # ── 消息通道统一路由(broadcast/xmpp by delivery) ── try: from messenger import install_stdio_hook as _msh _msh() except Exception: pass sys.path.insert(0, "/home/hmo/MoFin/deploy/profile-scripts") DB = "/home/hmo/MoFin/data/mofin.db" def calc_rsi(closes, n=14): if len(closes) < n + 1: return [None] * len(closes) out = [None] * len(closes) for i in range(n, len(closes)): gains, losses = [], [] for j in range(i - n + 1, i + 1): ch = closes[j] - closes[j - 1] gains.append(max(ch, 0)); losses.append(max(-ch, 0)) ag, al = sum(gains) / n, sum(losses) / n out[i] = 100 if al == 0 else 100 - 100 / (1 + ag / al) return out def main(): conn = sqlite3.connect(DB) hsi = pd.read_sql("SELECT date, close FROM stock_daily WHERE code='hkHSI' ORDER BY date", conn) codes = [r[0] for r in conn.execute( "SELECT code FROM hk_connect_stocks WHERE is_active=1 ORDER BY code").fetchall()] sector = dict(conn.execute( "SELECT code, sector_name FROM stock_sectors WHERE source='hk_em'").fetchall()) conn.close() # 大盘指标(同v1) hsi = hsi.sort_values("date").reset_index(drop=True) hsi["mkt_ret20"] = hsi["close"].pct_change(20) * 100 ma20 = hsi["close"].rolling(20).mean() hsi["mkt_above"] = (hsi["close"] > ma20).astype(float) hsi["mkt_rsi"] = calc_rsi(list(hsi["close"].values)) hsi["mkt_adx"] = hsi["close"].rolling(14).apply( lambda x: abs(x.iloc[-1]-x.iloc[0])/(x.max()-x.min()+1e-9)*100 if len(x)>1 else 0, raw=False) mkt = hsi.set_index("date")[["mkt_above","mkt_adx","mkt_rsi","mkt_ret20"]] # 行业指数(等权日收益 → 20日动量) print("构建港股行业指数...", flush=True) sec_daily = {} # sector -> DataFrame(date, ret1) for idx, code in enumerate(codes): sec = sector.get(code) if not sec: continue conn = sqlite3.connect(DB) df = pd.read_sql("SELECT date, close FROM stock_daily WHERE code=? ORDER BY date", conn, params=(code,)) conn.close() if len(df) < 90: continue df = df.sort_values("date").reset_index(drop=True) df["ret1"] = df["close"].pct_change() * 100 sec_daily.setdefault(sec, []).append(df[["date","ret1"]]) if (idx+1) % 150 == 0: print(f" 行业收集 {idx+1}/{len(codes)}", flush=True) sec_index = {} for sec, parts in sec_daily.items(): allp = pd.concat(parts, ignore_index=True) g = allp.groupby("date")["ret1"].mean().reset_index() # 等权行业日收益 g["sec_ret20"] = g["ret1"].rolling(20).sum() # 20日累计 sec_index[sec] = g.set_index("date")["sec_ret20"] print(f"行业指数: {len(sec_index)} 个行业", flush=True) # 估值分位(每日截面) print("加载历史估值...", flush=True) conn = sqlite3.connect(DB) hist = pd.read_sql( "SELECT code, date, pe_ttm, pb FROM stock_fundamentals_history WHERE length(code)=5", conn) conn.close() if len(hist) > 0: hist["pe_q"] = hist.groupby("date")["pe_ttm"].rank(pct=True) hist["pb_q"] = hist.groupby("date")["pb"].rank(pct=True) hist = hist[["code","date","pe_q","pb_q"]] else: hist = pd.DataFrame(columns=["code","date","pe_q","pb_q"]) print(f"估值历史: {len(hist)} 条", flush=True) # 资金流(当日主力净流入,万元) print("加载资金流...", flush=True) conn = sqlite3.connect(DB) flow = pd.read_sql( "SELECT code, date, flow_in, flow_out FROM hk_flow_daily", conn) conn.close() if len(flow) > 0: flow["flow1"] = (flow["flow_in"] - flow["flow_out"]) / 10000.0 # 万元→亿 flow = flow[["code","date","flow1"]] else: flow = pd.DataFrame(columns=["code","date","flow1"]) print(f"资金流: {len(flow)} 条", flush=True) # 市值(当前市值×价格比反推历史,总股本短期不变) print("加载市值...", flush=True) conn = sqlite3.connect(DB) fund_mc = pd.read_sql( "SELECT code, mcap_total FROM stock_fundamentals WHERE length(code)=5", conn) conn.close() fund_mc = fund_mc.dropna() # 每只股票最近收盘价(面板尾部)作为反推基准——在面板构建后统一计算 # 构建面板 rows = [] for idx, code in enumerate(codes): sec = sector.get(code) conn = sqlite3.connect(DB) df = pd.read_sql( "SELECT date, open, close, high, low, volume FROM stock_daily WHERE code=? ORDER BY date", conn, params=(code,)) conn.close() if len(df) < 90: continue df = df.sort_values("date").reset_index(drop=True) c = df["close"].values df["rsi"] = calc_rsi(list(c)) ma60 = df["close"].rolling(60).mean() ma20 = df["close"].rolling(20).mean() df["bias60"] = (df["close"]/ma60 - 1) * 100 df["bias20"] = (df["close"]/ma20 - 1) * 100 df["ret1"] = df["close"].pct_change(1) * 100 df["ret5"] = df["close"].pct_change(5) * 100 df["ret20"] = df["close"].pct_change(20) * 100 lo20 = df["low"].rolling(20).min() df["dist_lo20"] = (df["close"]/lo20 - 1) * 100 df["hi20_new"] = (df["close"] >= df["high"].rolling(20).max()).astype(int) v5 = df["volume"].rolling(5).mean() v20 = df["volume"].rolling(20).mean() df["vol_ratio"] = (v5/v20).round(3) df["code"] = code df = df.merge(mkt, left_on="date", right_index=True, how="left") # 行业动量 if sec and sec in sec_index: df = df.merge(sec_index[sec].rename("sec_ret20"), left_on="date", right_index=True, how="left") else: df["sec_ret20"] = np.nan # 估值分位 df = df.merge(hist[hist["code"]==code][["date","pe_q","pb_q"]], on="date", how="left") # 资金流 df = df.merge(flow[flow["code"]==code][["date","flow1"]], on="date", how="left") rows.append(df[["code","date","close","mkt_above","mkt_adx","mkt_rsi","mkt_ret20", "sec_ret20","rsi","bias60","bias20","dist_lo20","ret1","ret5","ret20", "vol_ratio","hi20_new","pe_q","pb_q","flow1"]]) if (idx+1) % 100 == 0: print(f" 面板 {idx+1}/{len(codes)}", flush=True) panel = pd.concat(rows, ignore_index=True) # 市值:当前市值×价格比反推历史(总股本短期不变)+ 每日市值分位 fund_mc = fund_mc.dropna() last_px = panel.sort_values("date").groupby("code")["close"].last().rename("last_close") fm = fund_mc.merge(last_px, on="code", how="inner") fm = fm[fm["last_close"] > 0] panel = panel.merge(fm[["code", "mcap_total", "last_close"]], on="code", how="left") panel = panel[panel["last_close"] > 0] panel["mcap"] = panel["mcap_total"] * panel["close"] / panel["last_close"] panel["mcap_q"] = panel.groupby("date")["mcap"].rank(pct=True) for col in ["sec_above", "news3", "flow5", "limit_up"]: panel[col] = np.nan panel.to_pickle("/tmp/panel_12d_hk.pkl") print(f"\n港股面板v2: {len(panel)} 行 × {len(panel.columns)} 列", flush=True) print("新增: sec_ret20(行业动量) + pe_q/pb_q(估值分位) + flow1(资金流) + mcap_q(市值分位)", flush=True) print("仍缺: flow5(港股无历史)/news3/行业above(后续补)", flush=True) if __name__ == "__main__": main()