xiaowei-system/scripts/stock_enhance_validate.py

248 lines
10 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
"""
股票策略增强收益验证 — 纯 MA20 vs MA20+行业动量过滤
=================================================
目的:量化证明 2026-08-01 加的"行业动量过滤"是否真的提高策略表现。
方法(同股同区间对比):
A. 纯 MA20金叉买死叉卖原策略
B. MA20+行业过滤:金叉时若行业动量>0 才买,否则跳过(增强策略)
* 行业动量:行业代表股过去 252 日动量12m-1m动量≤0 时跳过买入
注意:行业过滤的效果在"下跌行业"里最明显——白酒近2年下跌过滤后
应减少亏损交易、提高胜率。
"""
import sys, json, urllib.request
from datetime import datetime, timedelta
from pathlib import Path
import numpy as np
import pandas as pd
OUTPUT = Path.home() / ".hermes" / "stock_backtest"
# 行业代表股映射(行业动量代理)
INDUSTRY_PROXY = {
"白酒": "600519", # 贵州茅台
"银行": "600036", # 招商银行
"保险": "601318", # 中国平安
"新能源": "300750", # 宁德时代
"科技": "002415", # 海康威视
"煤炭": "601088", # 中国神华
"半导体": "688981", # 中芯国际
"证券": "600030", # 中信证券
"家电": "000333", # 美的集团
"医药": "600276", # 恒瑞医药
"通信": "600941", # 中国移动
"汽车": "601633", # 长城汽车
"地产": "000002", # 万科A
}
# 测试股票(跨行业:白酒下跌 + 新能源上涨 + 银行中性)
TEST_STOCKS = [
("000858", "五粮液", "白酒"),
("600519", "贵州茅台", "白酒"),
("000568", "泸州老窖", "白酒"),
("300750", "宁德时代", "新能源"),
("002415", "海康威视", "科技"),
("600036", "招商银行", "银行"),
]
def get_data(code, count=500):
"""腾讯行情日线(前复权)"""
mc = f"sh{code}" if code.startswith("6") else f"sz{code}"
end = datetime.now().strftime("%Y-%m-%d")
start = (datetime.now() - timedelta(days=int(count * 1.5))).strftime("%Y-%m-%d")
url = (f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get"
f"?_var=kline_dayqfq&param={mc},day,{start},{end},{count},qfq")
try:
text = urllib.request.urlopen(url, timeout=12).read().decode("utf-8")
data = json.loads(text.replace("kline_dayqfq=", "", 1))
qfq = (data.get("data", {}).get(mc, {}).get("qfqday") or
data.get("data", {}).get(mc, {}).get("day") or [])
rows = []
for item in qfq:
if len(item) < 6:
continue
rows.append({"date": item[0], "open": float(item[1]),
"close": float(item[2]), "high": float(item[3]),
"low": float(item[4]), "volume": float(item[5])})
df = pd.DataFrame(rows)
df["date"] = pd.to_datetime(df["date"])
return df.sort_values("date").reset_index(drop=True)
except Exception:
return pd.DataFrame()
def compute_industry_momentum(proxy_code):
"""计算行业动量:代理股过去 252 日动量12m-1m返回时间序列
返回 DataFrame 带 momentum 列(每日滚动 12m-1m 动量)
"""
df = get_data(proxy_code, count=400)
if df.empty or len(df) < 60:
return None
df["momentum"] = df["close"].shift(21) / df["close"].shift(252) - 1
return df[["date", "momentum"]]
def run_backtest(df, industry_filter=None):
"""
通用 MA20 回测
industry_filter: None=纯MA20; DataFrame(date, momentum)=动量过滤
返回: (strat_return, hold_return, alpha, max_dd, n_trades, win_rate, trades)
"""
if df.empty or len(df) < 30:
return None
d = df.copy()
d["ma20"] = d["close"].rolling(window=20).mean()
d["prev_close"] = d["close"].shift(1)
d["prev_ma20"] = d["ma20"].shift(1)
d["golden_cross"] = (d["prev_close"] < d["prev_ma20"]) & (d["close"] > d["ma20"])
d["death_cross"] = (d["prev_close"] > d["prev_ma20"]) & (d["close"] < d["ma20"])
# 合并行业动量(如果启用过滤)
if industry_filter is not None:
d = d.merge(industry_filter[["date", "momentum"]], on="date", how="left")
cash = 100000
position = 0
in_position = False
buy_price = 0
trades = []
equity = []
for i, row in d.iterrows():
if pd.isna(row["ma20"]):
continue
price = row["close"]
date_str = row["date"].strftime("%Y-%m-%d")
# 金叉买入(可被行业动量过滤)
if row["golden_cross"] and not in_position:
if industry_filter is not None:
mom = row.get("momentum", np.nan)
# 动量过滤动量≤0 或缺失时跳过
if pd.isna(mom) or mom <= 0:
trades.append({"type": "SKIP", "date": date_str, "price": price, "reason": f"行业动量{mom:+.1%}过滤"})
equity.append({"date": date_str, "value": cash, "price": price})
continue
shares = int(cash / price)
if shares > 0:
cash -= shares * price
position = shares
buy_price = price
in_position = True
trades.append({"type": "BUY", "date": date_str, "price": price, "shares": shares, "reason": "金叉"})
# 死叉卖出
elif row["death_cross"] and in_position:
proceeds = position * price
cash += proceeds
profit_pct = (price - buy_price) / buy_price * 100
trades.append({"type": "SELL", "date": date_str, "price": price, "shares": position, "profit_pct": profit_pct, "reason": "死叉"})
position = 0
in_position = False
buy_price = 0
equity.append({"date": date_str, "value": cash + position * price, "price": price})
final_price = d.iloc[-1]["close"]
final_value = cash + position * final_price
# 买入持有
buy_price_hold = d.iloc[19]["close"] if len(d) > 19 else d.iloc[0]["close"]
hold_return = (final_price - buy_price_hold) / buy_price_hold * 100 if buy_price_hold else 0
strat_return = (final_value - 100000) / 100000 * 100
alpha = strat_return - hold_return
# 最大回撤
eq = [e["value"] for e in equity] or [100000]
peak = eq[0]
max_dd = 0
for v in eq:
peak = max(peak, v)
max_dd = max(max_dd, (peak - v) / peak * 100)
# 胜率
sells = [t for t in trades if t["type"] == "SELL"]
wins = [t for t in sells if t.get("profit_pct", 0) > 0]
win_rate = len(wins) / len(sells) * 100 if sells else 0
return {
"strat_return": strat_return, "hold_return": hold_return,
"alpha": alpha, "max_dd": max_dd,
"n_trades": len(sells), "n_skips": sum(1 for t in trades if t["type"] == "SKIP"),
"win_rate": win_rate, "final_value": final_value,
"in_position": in_position, "trades": trades[-15:],
}
def main():
print("=" * 72)
print("策略增强收益验证 — 纯 MA20 vs MA20+行业动量过滤")
print("=" * 72)
results = []
for code, name, industry in TEST_STOCKS:
df = get_data(code, 500)
if df.empty:
print(f" ⚠️ {name} 数据获取失败")
continue
# A. 纯 MA20
r_plain = run_backtest(df, industry_filter=None)
# B. MA20 + 行业动量过滤
proxy = INDUSTRY_PROXY.get(industry)
ind_mom = compute_industry_momentum(proxy) if proxy else None
r_filtered = run_backtest(df, industry_filter=ind_mom) if ind_mom is not None else None
if not r_plain:
continue
print(f"\n📊 {name}({code}) [{industry}] {df['date'].min().date()}~{df['date'].max().date()}")
print(f" {'指标':<12} {'A.纯MA20':>12} {'B.动量过滤':>12} {'差异':>10}")
print(f" {'-'*46}")
print(f" {'策略收益':<12} {r_plain['strat_return']:>+11.2f}% {r_filtered['strat_return']:>+11.2f}% {r_filtered['strat_return']-r_plain['strat_return']:>+9.2f}%")
print(f" {'买入持有':<12} {r_plain['hold_return']:>+11.2f}% {'':>12}")
print(f" {'超额收益α':<12} {r_plain['alpha']:>+11.2f}% {r_filtered['alpha']:>+11.2f}% {r_filtered['alpha']-r_plain['alpha']:>+9.2f}%")
print(f" {'最大回撤':<12} {r_plain['max_dd']:>11.2f}% {r_filtered['max_dd']:>11.2f}% {r_filtered['max_dd']-r_plain['max_dd']:>+9.2f}%")
print(f" {'交易次数':<12} {r_plain['n_trades']:>12} {r_filtered['n_trades']:>12} {r_filtered['n_trades']-r_plain['n_trades']:>+10}")
print(f" {'胜率':<12} {r_plain['win_rate']:>11.1f}% {r_filtered['win_rate']:>11.1f}% {r_filtered['win_rate']-r_plain['win_rate']:>+9.1f}%")
if r_filtered and r_filtered["n_skips"] > 0:
print(f" (过滤跳过 {r_filtered['n_skips']} 次金叉)")
results.append({
"code": code, "name": name, "industry": industry,
"plain": {k: v for k, v in r_plain.items() if k != "trades"},
"filtered": {k: v for k, v in r_filtered.items() if k != "trades"} if r_filtered else None,
})
# 汇总
print("\n" + "=" * 72)
print("📈 汇总(过滤增强 vs 纯MA20")
print("=" * 72)
valid = [r for r in results if r["filtered"]]
if valid:
avg_alpha_gain = np.mean([r["filtered"]["alpha"] - r["plain"]["alpha"] for r in valid])
avg_dd_gain = np.mean([r["filtered"]["max_dd"] - r["plain"]["max_dd"] for r in valid])
avg_win_gain = np.mean([r["filtered"]["win_rate"] - r["plain"]["win_rate"] for r in valid])
avg_trade_reduce = np.mean([r["plain"]["n_trades"] - r["filtered"]["n_trades"] for r in valid])
print(f" 平均 α 增益: {avg_alpha_gain:+.2f}%")
print(f" 平均回撤变化: {avg_dd_gain:+.2f}% (负=回撤减小更好)")
print(f" 平均胜率变化: {avg_win_gain:+.2f}%")
print(f" 平均交易减少: {avg_trade_reduce:.1f} 次/股 (过滤噪音)")
print(f"\n 结论: {'✅ 行业动量过滤显著增强策略(α↑/回撤↓/胜率↑)' if avg_alpha_gain > 0 and avg_dd_gain <= 0 else '⚠️ 过滤效果不显著,需调整动量阈值'}")
out = OUTPUT / "enhance_validation.json"
with open(out, "w", encoding="utf-8") as f:
json.dump({"generated": datetime.now().strftime("%Y-%m-%d %H:%M"),
"method": "同股同区间对比: 纯MA20 vs MA20+行业动量过滤(12m-1m>0)",
"results": results}, f, ensure_ascii=False, indent=2)
print(f"\n📁 已保存: {out}")
if __name__ == "__main__":
main()