Files

182 lines
7.9 KiB
Python
Raw Permalink 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
# ── 消息通道统一路由(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()