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

157 lines
6.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
# -*- 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()