feat: 港股12维面板构建脚本正式化(补行业动量/估值分位/资金流/市值分位)
This commit is contained in:
@@ -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()
|
||||
Reference in New Issue
Block a user