156 lines
5.8 KiB
Python
156 lines
5.8 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""hk_scanner.py — 港股通组合策略扫描器(按温区调度,2026-08-14)
|
||
|
||
基于港股 12 维面板(/tmp/panel_12d_hk.pkl,每日盘后构建),按当前温区选策略扫描:
|
||
trend_up → hk_pe_mom(低PE+小市值+行业动量)
|
||
trend_down → hk_mr1(深度超卖,trend_down 连续>5天时暂停)
|
||
choppy → hk_pe_oversold(低PE+超卖)
|
||
|
||
数据:面板为日频(盘后构建,含收盘因子)。盘中扫描用最近可用面板日。
|
||
输出:candidates 表(sector='hk_pe_mom'/'hk_mr1'/'hk_pe_oversold')
|
||
"""
|
||
import json
|
||
import sqlite3
|
||
import sys
|
||
import time
|
||
from pathlib import Path
|
||
from datetime import datetime
|
||
|
||
from hk_strategies import HK_STRATEGIES, strategies_for_regime
|
||
|
||
# ── 消息通道统一路由(broadcast/xmpp by delivery) ──
|
||
try:
|
||
from messenger import install_stdio_hook as _msh
|
||
_msh()
|
||
except Exception:
|
||
pass
|
||
|
||
DB_PATH = Path("/home/hmo/MoFin/data/mofin.db")
|
||
PANEL = "/tmp/panel_12d_hk.pkl"
|
||
TD_GUARD = 5 # trend_down 连续>5天暂停超卖(组合级防守)
|
||
|
||
|
||
def get_hk_regime():
|
||
"""港股当前温区(smoothed markets.hk,回退 market_regime 表)"""
|
||
try:
|
||
p = Path("/home/hmo/MoFin/data/market_regime_smoothed.json")
|
||
if p.exists():
|
||
d = json.loads(p.read_text(encoding="utf-8"))
|
||
mk = (d.get("markets") or {}).get("hk") or {}
|
||
if mk.get("current_regime"):
|
||
return mk["current_regime"]
|
||
except Exception:
|
||
pass
|
||
try:
|
||
conn = sqlite3.connect(str(DB_PATH), timeout=5)
|
||
conn.execute("PRAGMA busy_timeout=30000") # 2026-08-18 整点撞锁等待
|
||
row = conn.execute(
|
||
"SELECT regime FROM market_regime WHERE market='hk' ORDER BY date DESC LIMIT 1").fetchone()
|
||
conn.close()
|
||
return row[0] if row else "unknown"
|
||
except Exception:
|
||
return "unknown"
|
||
|
||
|
||
def trend_down_streak():
|
||
"""trend_down 连续天数(防守用)"""
|
||
try:
|
||
conn = sqlite3.connect(str(DB_PATH), timeout=5)
|
||
conn.execute("PRAGMA busy_timeout=30000") # 2026-08-18 整点撞锁等待
|
||
rows = conn.execute(
|
||
"SELECT date, regime FROM market_regime WHERE market='hk' ORDER BY date DESC LIMIT 30").fetchall()
|
||
conn.close()
|
||
except Exception:
|
||
return 0
|
||
run = 0
|
||
for _, reg in rows:
|
||
if reg == "trend_down":
|
||
run += 1
|
||
else:
|
||
break
|
||
return run
|
||
|
||
|
||
def scan_panel(strat):
|
||
"""用面板最新日扫描策略信号。返回命中股票列表"""
|
||
import pandas as pd
|
||
panel = pd.read_pickle(PANEL)
|
||
latest = panel["date"].max()
|
||
p = panel[panel["date"] == latest]
|
||
e = strat["entry"]
|
||
cond = pd.Series(True, index=p.index)
|
||
if "pe_q_max" in e:
|
||
cond &= p["pe_q"] < e["pe_q_max"]
|
||
if "mcap_q_max" in e:
|
||
cond &= p["mcap_q"] < e["mcap_q_max"]
|
||
if "sec_ret20_min" in e:
|
||
cond &= p["sec_ret20"] > e["sec_ret20_min"]
|
||
if "rsi_max" in e:
|
||
cond &= p["rsi"] < e["rsi_max"]
|
||
if "bias60_max" in e:
|
||
cond &= p["bias60"] < e["bias60_max"]
|
||
if "vol_ratio_min" in e:
|
||
cond &= p["vol_ratio"] > e["vol_ratio_min"]
|
||
if "rsi_delta_min" in e:
|
||
cond &= p["rsi"] - p.groupby("code")["rsi"].transform(lambda x: x.shift(1).fillna(0)) * 0 >= e["rsi_delta_min"]
|
||
hits = p[cond]
|
||
return latest, hits[["code", "close"]].to_dict("records")
|
||
|
||
|
||
def main():
|
||
print(f"[hk_scanner] {datetime.now().strftime('%H:%M:%S')} 港股组合扫描", flush=True)
|
||
regime = get_hk_regime()
|
||
td_streak = trend_down_streak()
|
||
print(f" 港股温区: {regime} (trend_down连续{td_streak}天)", flush=True)
|
||
|
||
# 按温区选策略
|
||
versions = strategies_for_regime(regime)
|
||
if not versions:
|
||
print(f" {regime} 温区无激活策略,跳过", flush=True)
|
||
return
|
||
# trend_down 连续>N天防守:暂停超卖策略
|
||
if regime == "trend_down" and td_streak > TD_GUARD:
|
||
versions = [v for v in versions if v != "hk_mr1"]
|
||
print(f" ⚠ trend_down连续{td_streak}天>守卫{TD_GUARD},暂停 hk_mr1(防守)", flush=True)
|
||
print(f" 激活策略: {versions}", flush=True)
|
||
|
||
conn = sqlite3.connect(str(DB_PATH), timeout=5)
|
||
conn.execute("PRAGMA busy_timeout=30000") # 2026-08-18 整点撞锁等待
|
||
inserted = 0
|
||
for v in versions:
|
||
strat = HK_STRATEGIES[v]
|
||
try:
|
||
latest, hits = scan_panel(strat)
|
||
except Exception as ex:
|
||
print(f" {v} 扫描失败: {ex}", flush=True)
|
||
continue
|
||
print(f" {v}({strat['name']}): 命中 {len(hits)} 只(面板日 {latest})", flush=True)
|
||
for h in hits[:10]:
|
||
code = h["code"]
|
||
price = h["close"]
|
||
ex = strat["exit"]
|
||
exists = conn.execute(
|
||
"SELECT code FROM candidates WHERE code=? AND (promoted IS NULL OR promoted=0)",
|
||
(code,)).fetchone()
|
||
if exists:
|
||
continue
|
||
conn.execute(
|
||
"INSERT INTO candidates (code, name, sector, reason, entry_range, stop_loss, target, created_at) "
|
||
"VALUES (?,?,?,?,?,?,?,datetime('now','localtime')) "
|
||
"ON CONFLICT(code) DO UPDATE SET name=excluded.name, sector=excluded.sector, "
|
||
"reason=excluded.reason, entry_range=excluded.entry_range, "
|
||
"stop_loss=excluded.stop_loss, target=excluded.target",
|
||
(code, code, v, f"{v}({regime}温区,{strat['summary'][:30]})",
|
||
f"{round(price*0.97,2)}~{round(price*1.02,2)}",
|
||
round(price*(1-ex['sl_pct']), 2), round(price*(1+ex['tp_pct']), 2)))
|
||
inserted += 1
|
||
print(f" 🟢 {code} 价{price} → {v}", flush=True)
|
||
conn.commit()
|
||
conn.close()
|
||
print(f" ✅ 新增 {inserted} 只港股候选", flush=True)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|