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

115 lines
6.0 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.
# -*- coding: utf-8 -*-
"""hk_pe_oversold_v2 生成器:港股低PE超卖池 + 深跌强化 + 每日top-N截断
由果及因(23701 港股池信号验证,tp25/sl12/40日):
池基线 20d均值 -0.30%/胜率43.8%(港股低PE超卖本身不赚钱!)
alpha = 深跌+破净:
sec<-10+mkt<-10+pb<0.4: 19.15%(+19.5pp)/胜率77.2%
mkt<-10: 11.50%(+11.8pp)/胜率69.4%
出场:tp25/sl12/max40(原版出场)
"""
import sys, json
sys.path.insert(0, "/home/hmo/MoFin")
import pandas as pd
import numpy as np
TOP_N = 3
def score_of(row):
"""港股深跌评分(0-100):mkt_ret20 + sec_ret20 + ret20 + pb"""
sc = 0
m = row["mkt_ret20"]; s = row["sec_ret20"]; r = row["ret20"]; pb = row["pb_q"]
if not pd.isna(m):
sc += 40 if m < -20 else 32 if m < -10 else 20 if m < 0 else 8
if not pd.isna(s):
sc += 25 if s < -20 else 20 if s < -10 else 10 if s < 0 else 3
if not pd.isna(r):
sc += 20 if r < -25 else 14 if r < -15 else 8 if r < -5 else 2
if not pd.isna(pb):
sc += 15 if pb < 0.2 else 10 if pb < 0.4 else 5 if pb < 0.6 else 1
return sc
def gen_trades(start, end, top_n=TOP_N):
panel = pd.read_pickle("/tmp/panel_12d_hk.pkl")
panel = panel.sort_values(["code", "date"]).reset_index(drop=True)
g = panel.groupby("code", group_keys=False)
def fwd_max(s, w): return s[::-1].rolling(w, min_periods=1).max()[::-1]
def fwd_min(s, w): return s[::-1].rolling(w, min_periods=1).min()[::-1]
panel["fwd_max40"] = g["close"].transform(lambda x: fwd_max(x, 20))
panel["fwd_min40"] = g["close"].transform(lambda x: fwd_min(x, 20))
# 池 + 深跌强化(mkt<-10 是核心:11.50%/69.4%
sig = panel[
(panel["date"] >= start) & (panel["date"] <= end) &
(panel["pe_q"] < 0.2) & (panel["rsi"] < 45) & (panel["bias60"] < 0) &
(panel["mkt_ret20"] < -10)
].copy()
sig["score"] = sig.apply(score_of, axis=1)
sig = sig.sort_values(["date", "score"], ascending=[True, False]).groupby("date").head(top_n)
trades = []
for _, s in sig.iterrows():
ep = s["close"]
if ep <= 0:
continue
fmax, fmin = s["fwd_max40"], s["fwd_min40"]
hit_tp = fmax >= ep * 1.20
hit_sl = fmin <= ep * 0.88
if hit_tp: pnl, reason = 25.0, "target"
elif hit_sl: pnl, reason = -12.0, "stop"
else: pnl, reason = (fmax / ep - 1) * 100 if not pd.isna(fmax) else 0, "time"
trades.append({"code": s["code"], "name": str(s["code"]), "entry_date": s["date"],
"entry_price": round(ep, 2), "exit_price": round(ep * (1 + pnl / 100), 2),
"profit_pct": round(pnl, 2), "exit_reason": reason, "hold_days": 20,
"score": int(s["score"]), "boost": 1.0})
return trades
def build_results(trades):
import copy, random
from strategy_lab import portfolio_sim
n = len(trades)
wins = [t for t in trades if t["profit_pct"] > 0]
wr = len(wins) / n * 100 if n else 0
avg = sum(t["profit_pct"] for t in trades) / n if n else 0
sim = portfolio_sim(trades, 1000000, max_positions=10) or {}
rets = []
for seed in range(5):
t2 = copy.deepcopy(trades); rng = random.Random(seed); rng.shuffle(t2)
_r = portfolio_sim(t2, 1000000, max_positions=10) or {}
rets.append(_r.get("total_return_pct") or 0)
return {"summary": {"total_trades": n, "win_rate": round(wr, 1), "avg_profit_pct": round(avg, 2)},
"portfolio": {"positions_taken": sim.get("positions_taken"), "positions_skipped": sim.get("positions_skipped"),
"total_return_pct": sim.get("total_return_pct"), "cagr_pct": sim.get("cagr_pct"),
"portfolio_max_dd_pct": sim.get("portfolio_max_dd_pct"), "capital_final": sim.get("capital_final")},
"robustness": {"shuffle_total_return": rets, "spread_pp": round(max(rets) - min(rets), 1)},
"trades": trades}
if __name__ == "__main__":
import sqlite3
from datetime import datetime
DB = "/home/hmo/MoFin/data/mofin.db"
VERSION = "hk_pe_oversold_v2"
windows = {"1y": ("2025-07-01", "2026-07-01"), "2y": ("2024-07-01", "2026-07-01"),
"5y": ("2021-07-01", "2026-07-01"), "10y": ("2016-01-01", "2026-07-01")}
conn = sqlite3.connect(DB, timeout=30)
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
for pt, (s, e) in windows.items():
trades = gen_trades(s, e)
results = build_results(trades)
exist = conn.execute("SELECT id FROM strategy_research WHERE version=? AND period_tag=?", (VERSION, pt)).fetchone()
if exist:
conn.execute("UPDATE strategy_research SET results_json=? WHERE version=? AND period_tag=?",
(json.dumps(results, ensure_ascii=False), VERSION, pt))
else:
conn.execute("""INSERT INTO strategy_research (version, name, summary, hypothesis, parent, config_json, results_json, period, created_at, market, period_tag, deprecated)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
(VERSION, "港股深跌破净升级", "低PE超卖池+深跌强化(mkt<-10),每日top8",
"由果及因: 港股池基线-0.30%不赚钱,alpha=深跌破净(sec+mkt<-10+pb<0.4: +19.5pp/77%); 每日top-N填洞",
"港股超卖", json.dumps({"top_n": TOP_N, "entry": {"pe_q_max": 0.2, "rsi_max": 45, "bias60_max": 0, "mkt_ret20_max": -10}, "exit": {"tp": 25, "sl": 12, "max_hold": 40}}, ensure_ascii=False),
json.dumps(results, ensure_ascii=False), None, now, "hk", pt, None))
s2 = results["summary"]; p = results["portfolio"] or {}
_pt = p.get("positions_taken") or 0
print(f"[{pt}] 信号{s2['total_trades']} 成交{_pt}{s2['total_trades']/max(_pt,1):.1f} "
f"胜率{s2['win_rate']}% 年化{p.get('cagr_pct') or 0}% 回撤{p.get('portfolio_max_dd_pct') or 0}% 洗牌差{results['robustness']['spread_pp']}pp")
conn.commit(); conn.close()
print(f"\n{len(windows)} 个周期已写入 (version={VERSION})")