Files
MoFin/scripts/research/step40_mkt_atmosphere.py
T
hmo b9c68a83a7 docs: 预测超跌反弹策略研究成果归档(方法论/策略文档/研究记录/脚本)
- 新增 strategy_research_methodology.md(由果及因/12维/铁律/支撑压力规范)
- 新增 predictive_oversold_strategy.md(v5定稿,年化18.57%)
- 新增 deployment-plan-predictive-oversold.md(整合部署计划)
- 归档 docs/research/(63份研究过程文档)+ scripts/research/(19个研究脚本)
- 更新 docs/README.md 文档中心(策略研究章节)
2026-08-10 14:37:21 +08:00

105 lines
5.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.
#!/usr/bin/env python3
"""step40_mkt_atmosphere.py — 市场气氛 vs 信号质量(底部确认依据)
老莫洞察:2024-02 全场下跌,气氛不对——超跌信号可能是"下跌中继"而非"底部"
构建气氛指标(无前视):
- 大盘: mkt_ret5(短期加速跌) / mkt_dd60(深度) / mkt_adx(趋势强度)
- 消息面: 全市场每日新闻总量(气氛冷热)
- 行业共振: 行业也在深跌 vs 个股独立超跌
扫描这些 vs 信号后续收益,找"底部确认"的数据依据
"""
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["fwd_ret60"] = panel.groupby("code")["close"].transform(lambda x: x.shift(-60)/x - 1) * 100
panel["is_big"] = (panel["fwd_ret60"] >= 50).astype(int)
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["mkt_ret10"] = idx_df["close"].pct_change(10) * 100
idx_df["mkt_ret20"] = idx_df["close"].pct_change(20) * 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 = 0
for c in close_arr:
if c < close_arr[len(down_days)-1] if len(down_days) > 0 else False:
streak += 1
else:
streak = 0
down_days.append(streak)
idx_df["mkt_down_days"] = down_days
mkt_map = dict(zip(idx_df["date"], zip(idx_df["mkt_ret5"], idx_df["mkt_ret10"], idx_df["mkt_ret20"], idx_df["mkt_dd60"], idx_df["mkt_down_days"])))
panel["mkt_ret5"] = panel["date"].map(lambda d: mkt_map[d][0] if d in mkt_map else np.nan)
panel["mkt_ret10"] = panel["date"].map(lambda d: mkt_map[d][1] if d in mkt_map else np.nan)
panel["mkt_ret20"] = panel["date"].map(lambda d: mkt_map[d][2] if d in mkt_map else np.nan)
panel["mkt_dd60"] = panel["date"].map(lambda d: mkt_map[d][3] if d in mkt_map else np.nan)
panel["mkt_down_days"] = panel["date"].map(lambda d: mkt_map[d][4] if d in mkt_map else np.nan)
print("大盘指标合并", flush=True)
# 全市场每日新闻量(消息面气氛)
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_news"] = news_cnt["c"].rolling(5, min_periods=1).mean() # 5日均量
news_map = dict(zip(news_cnt["d"], news_cnt["mkt_news"]))
panel["mkt_news5"] = panel["date"].map(lambda d: news_map.get(d, np.nan))
print("新闻气氛合并", flush=True)
# 信号
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["mkt_dd60"] <= -5)
)
sig = panel[sig_cond].dropna(subset=["fwd_ret60"]).copy()
print("信号:", len(sig), flush=True)
base_rate = sig["is_big"].mean() * 100
base_avg = sig["fwd_ret60"].mean()
print("信号基线: 大涨率={:.2f}% avg60={:.2f}%".format(base_rate, base_avg), flush=True)
def scan(cond, label, min_n=50):
m = sig[cond]
if len(m) < min_n:
print("{}: n={} 不足".format(label, len(m)), flush=True)
return
print("{}: n={} 大涨率={:.2f}% avg60={:.2f}% wr={:.1f}% 超额={:+.2f}pp".format(
label, len(m), m["is_big"].mean()*100, m["fwd_ret60"].mean(),
(m["fwd_ret60"]>0).mean()*100, m["is_big"].mean()*100 - base_rate), flush=True)
print("\n=== 大盘短期加速下跌(mkt_ret5===", flush=True)
scan(sig["mkt_ret5"] > -2, "大盘5日跌>-2%(企稳)")
scan((sig["mkt_ret5"] <= -2) & (sig["mkt_ret5"] > -5), "大盘5日跌-2~-5%")
scan(sig["mkt_ret5"] <= -5, "大盘5日跌<-5%(加速)")
print("\n=== 大盘连续下跌天数 ===", flush=True)
scan(sig["mkt_down_days"] <= 2, "大盘连跌<=2天")
scan((sig["mkt_down_days"] > 2) & (sig["mkt_down_days"] <= 5), "连跌3-5天")
scan(sig["mkt_down_days"] > 5, "连跌>5天")
print("\n=== 消息面气氛(mkt_news5 分位)===", flush=True)
sig["news_q"] = sig["mkt_news5"].rank(pct=True)
scan(sig["news_q"] < 0.33, "新闻量低(冷清)")
scan((sig["news_q"] >= 0.33) & (sig["news_q"] < 0.67), "新闻量中")
scan(sig["news_q"] >= 0.67, "新闻量高(热闹)")
print("\n=== 行业共振(行业是否也在深跌)===", flush=True)
scan(sig["sec_ret20"] < -10, "行业深跌<-10%(共振)")
scan((sig["sec_ret20"] >= -10) & (sig["sec_ret20"] < 0), "行业跌-10~0%")
scan(sig["sec_ret20"] < -15, "行业极深跌<-15%")
print("\n=== 组合:底部确认信号 ===", flush=True)
scan((sig["mkt_ret5"] > -3) & (sig["mkt_down_days"] <= 3), "大盘企稳+连跌<=3")
scan((sig["mkt_ret5"] > -3) & (sig["mkt_down_days"] <= 3) & (sig["news_q"] >= 0.33), "企稳+连跌<=3+新闻中高")
scan((sig["mkt_ret5"] > 0), "大盘5日已转正")
print("\n=== 完成 ===", flush=True)