From bc284b1503f85bf50b4d3fa4e38bc43f0fb380fe Mon Sep 17 00:00:00 2001 From: hmo Date: Sat, 15 Aug 2026 04:15:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B8=AF=E8=82=A112=E7=BB=B4=E9=9D=A2?= =?UTF-8?q?=E6=9D=BF=E6=9E=84=E5=BB=BA=E8=84=9A=E6=9C=AC=E6=AD=A3=E5=BC=8F?= =?UTF-8?q?=E5=8C=96(=E8=A1=A5=E8=A1=8C=E4=B8=9A=E5=8A=A8=E9=87=8F/?= =?UTF-8?q?=E4=BC=B0=E5=80=BC=E5=88=86=E4=BD=8D/=E8=B5=84=E9=87=91?= =?UTF-8?q?=E6=B5=81/=E5=B8=82=E5=80=BC=E5=88=86=E4=BD=8D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/profile-scripts/build_panel_hk.py | 156 +++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 deploy/profile-scripts/build_panel_hk.py diff --git a/deploy/profile-scripts/build_panel_hk.py b/deploy/profile-scripts/build_panel_hk.py new file mode 100644 index 00000000..0b027972 --- /dev/null +++ b/deploy/profile-scripts/build_panel_hk.py @@ -0,0 +1,156 @@ +#!/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 +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) + + # 构建面板 + 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) + for col in ["sec_above","news3","flow5","mcap_q","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(当日资金流)", flush=True) + print("仍缺: 资金流flow5(港股无历史)/新闻news3/mcap_q/行业above(后续补)", flush=True) + +if __name__ == "__main__": + main()