#!/usr/bin/env python3 """step43_negative_factor.py — 大回撤案例负面因子提取 方法:对每个信号模拟交易(信号日买→止损/止盈/40日), 分组:亏损组(ret<0) vs 盈利组(ret>0),对比信号日12维特征+市场气氛, 提取亏损交易共有的负面因子。 """ import numpy as np import pandas as pd import sqlite3 print("=== 加载 ===", flush=True) panel = pd.read_pickle("/tmp/panel_12d.pkl") panel = panel.sort_values(["code", "date"]).reset_index(drop=True) panel["_key"] = panel["code"] + "_" + panel["date"] pos_map = {k: i for i, k in enumerate(panel["_key"])} print("面板:", len(panel), flush=True) # 大盘/气氛指标 conn = sqlite3.connect("file:/home/hmo/MoFin/data/mofin.db?mode=ro", uri=True) idx_df = pd.read_sql("SELECT date, close, high FROM stock_daily WHERE code='sh000001' ORDER BY date", conn) idx_df["date"] = idx_df["date"].astype(str) idx_df["mkt_ret5"] = idx_df["close"].pct_change(5) * 100 idx_df["hi60"] = idx_df["high"].rolling(60).max() idx_df["mkt_dd60"] = (idx_df["close"] / idx_df["hi60"] - 1) * 100 close_arr = idx_df["close"].values down_days, streak, prev = [], 0, None for c in close_arr: streak = streak + 1 if (prev is not None and c < prev) else 0 down_days.append(streak) prev = c idx_df["mkt_down_days"] = down_days mkt_map = {} for r in idx_df.itertuples(): mkt_map[r.date] = (r.mkt_ret5, r.mkt_dd60, r.mkt_down_days) news_cnt = pd.read_sql("SELECT substr(date,1,10) d, COUNT(*) c FROM stock_news GROUP BY d", conn) news_cnt["d"] = news_cnt["d"].astype(str) news_cnt["mkt_news5"] = news_cnt["c"].rolling(5, min_periods=1).mean() news_map = dict(zip(news_cnt["d"], news_cnt["mkt_news5"])) news_med = news_cnt["mkt_news5"].median() print("新闻中位:", news_med, flush=True) # 信号(step37 同款) sig_cond = ( (panel["mkt_rsi"] < 50) & (panel["mcap_q"] < 0.2) & (panel["pe_q"] < 0.2) & (panel["news3"] >= 1) & (panel["sec_ret20"] < 0) & (panel["bias60"] < -20) ) panel["_dd60"] = panel["date"].map(lambda d: mkt_map[d][1] if d in mkt_map else np.nan) sig_cond = sig_cond & (panel["_dd60"] <= -5) cand = panel[sig_cond][["code", "date"]].copy() cand = cand.sort_values(["code", "date"]) cand["prev"] = cand.groupby("code")["date"].shift(1) cand["gap"] = (pd.to_datetime(cand["date"]) - pd.to_datetime(cand["prev"])).dt.days cand = cand[(cand["prev"].isna()) | (cand["gap"] > 30)] print("信号:", len(cand), flush=True) # K线 codes = cand["code"].unique().tolist() ph = ",".join("?" * len(codes)) df = pd.read_sql("SELECT code, date, close, high, low FROM stock_daily WHERE code IN ({}) ORDER BY code, date".format(ph), conn, params=codes) df["date"] = df["date"].astype(str) df["code"] = df["code"].astype(str).str.zfill(6) df = df.sort_values(["code", "date"]).reset_index(drop=True) df["_key"] = df["code"] + "_" + df["date"] dpos = {k: i for i, k in enumerate(df["_key"])} closes = df["close"].values highs = df["high"].values lows = df["low"].values cand["kidx"] = (cand["code"] + "_" + cand["date"]).map(dpos) cand = cand.dropna(subset=["kidx"]).copy() cand["kidx"] = cand["kidx"].astype(int) print("可定位:", len(cand), flush=True) # 支撑压力(用于止损止盈) import sys sys.path.insert(0, "/tmp") from sr_calculator import SRCalculator sr = SRCalculator() print("=== 计算支撑压力 + 模拟交易 ===", flush=True) STOP_BUF = 0.05 HOLD = 40 rows = [] for i, r in enumerate(cand.itertuples()): if i % 500 == 0: print(" {} / {}".format(i, len(cand)), flush=True) bars_code = sr.get_bars(r.code) d_idx = bars_code.index[bars_code["date"] == r.date] if len(d_idx) == 0: continue sr_full = sr.sr_full(r.code, d_idx[0]) pv = sr_full["pivot"] chip = sr_full["chip"] if not pv: continue sig_close = closes[r.kidx] support = pv["s2"] if chip and chip["chip_ss"] < sig_close: support = max(pv["s2"], chip["chip_ss"]) resist = pv["r2"] if chip and chip["chip_sr"] > sig_close: resist = min(pv["r2"], chip["chip_sr"]) if support >= sig_close or resist <= sig_close: continue # 模拟:信号日买,支撑下5%止损/压力止盈/40日 stop = support * (1 - STOP_BUF) tp = resist ret = None reason = None for j in range(r.kidx+1, min(r.kidx+HOLD+1, len(closes))): hi, lo = highs[j], lows[j] if hi >= tp: ret = (tp / sig_close - 1) * 100 reason = "tp" break if lo <= stop: ret = (lo / sig_close - 1) * 100 reason = "stop" break if ret is None: ret = (closes[min(r.kidx+HOLD, len(closes)-1)] / sig_close - 1) * 100 reason = "hold40" # 信号日特征(12维 + 气氛) row = {"code": r.code, "date": r.date, "ret": ret, "reason": reason, "win": 1 if ret > 0 else 0, "ret_abs": abs(ret)} feat = panel.iloc[pos_map[r.code + "_" + r.date]] for col in ["mkt_rsi", "mkt_adx", "sec_ret20", "sec_above", "rsi", "bias60", "bias20", "dist_lo20", "ret1", "ret5", "ret20", "vol_ratio", "limit_up", "hi20_new", "news3", "flow1", "flow5", "mcap_q", "pe_q", "pb_q"]: row["sig_" + col] = feat[col] # 气氛 mk = mkt_map.get(r.date, (np.nan, np.nan, np.nan)) row["mkt_ret5"] = mk[0] row["mkt_dd60"] = mk[1] row["mkt_down_days"] = mk[2] row["mkt_news5"] = news_map.get(r.date, np.nan) rows.append(row) tr = pd.DataFrame(rows) print("\n交易:", len(tr), flush=True) tr.to_csv("/tmp/step43_trades.csv", index=False) # ── 负面因子提取:亏损 vs 盈利 ── print("\n=== 亏损 vs 盈利 特征对比 ===", flush=True) win = tr[tr["win"] == 1] lose = tr[tr["win"] == 0] print("盈利: {} ({:.1f}%) 亏损: {} ({:.1f}%)".format( len(win), len(win)/len(tr)*100, len(lose), len(lose)/len(tr)*100), flush=True) print("盈利 avg={:.2f}% 亏损 avg={:.2f}%".format( win["ret"].mean(), lose["ret"].mean()), flush=True) out = ["# 大回撤负面因子提取\n", "- 盈利 {} 笔 avg{:.2f}% / 亏损 {} 笔 avg{:.2f}%".format( len(win), win["ret"].mean(), len(lose), lose["ret"].mean()), "- 对比信号日特征:亏损组显著偏离盈利组的 = 负面因子\n", "| 特征 | 亏损组 | 盈利组 | 差异 | 方向 |", "|---|---:|---:|---:|---|"] feat_cols = [c for c in tr.columns if c.startswith("sig_")] + \ ["mkt_ret5", "mkt_dd60", "mkt_down_days", "mkt_news5"] for col in feat_cols: w = win[col].dropna() l = lose[col].dropna() if len(w) < 50 or len(l) < 50: continue wm, lm = w.mean(), l.mean() diff = lm - wm rel = abs(diff) / max(abs(wm), 1e-9) if rel > 0.05: out.append("| {} | {:.3f} | {:.3f} | {:.3f} | {} |".format( col, lm, wm, diff, "↑亏高" if diff > 0 else "↓亏低")) # 大亏损(<-10%)特征 out.append("\n## 大亏损(ret<-10%)vs 盈利 特征\n") big_lose = tr[tr["ret"] < -10] out.append("- 大亏损 {} 笔 ({}%)".format(len(big_lose), len(big_lose)/len(tr)*100)) out.append("| 特征 | 大亏损组 | 盈利组 | 差异 |") out.append("|---|---:|---:|---:|") for col in feat_cols: b = big_lose[col].dropna() w = win[col].dropna() if len(b) < 20 or len(w) < 50: continue bm, wm = b.mean(), w.mean() diff = bm - wm rel = abs(diff) / max(abs(wm), 1e-9) if rel > 0.05: out.append("| {} | {:.3f} | {:.3f} | {:.3f} |".format(col, bm, wm, diff)) # 卖出原因分布 out.append("\n## 卖出原因 × 盈亏\n") out.append("| 原因 | 笔数 | 平均收益 | 胜率 |") out.append("|---|---:|---:|---:|") for rs in tr["reason"].unique(): sub = tr[tr["reason"] == rs] out.append("| {} | {} | {:.2f}% | {:.1f}% |".format( rs, len(sub), sub["ret"].mean(), (sub["ret"]>0).mean()*100)) with open("/tmp/step43_negative_report.md", "w", encoding="utf-8") as f: f.write("\n".join(out)) print("\n报告已写 /tmp/step43_negative_report.md", flush=True) print("=== 完成 ===", flush=True)