181 lines
6.5 KiB
Python
181 lines
6.5 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
止损机制回测验证 — 无止损 vs 8%止损 vs 5%止损 vs 10%止损
|
||
======================================================
|
||
在 MA20 策略基础上叠加止损线,对比收益/回撤/胜率。
|
||
|
||
用法:python3 stock_stop_loss_validate.py
|
||
"""
|
||
import sys, json, urllib.request
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
import numpy as np
|
||
import pandas as pd
|
||
|
||
sys.path.insert(0, str(Path(__file__).parent))
|
||
from stock_ma20_backtest import get_data
|
||
|
||
OUTPUT = Path.home() / ".hermes" / "stock_backtest"
|
||
|
||
# 测试股票(白酒下跌趋势最能体现止损价值)
|
||
TEST_STOCKS = [
|
||
("000858", "五粮液", "白酒"),
|
||
("600519", "贵州茅台", "白酒"),
|
||
("000568", "泸州老窖", "白酒"),
|
||
("300750", "宁德时代", "新能源"),
|
||
("002415", "海康威视", "科技"),
|
||
]
|
||
|
||
|
||
def backtest_with_stoploss(df, stop_loss_pct=0.0, initial_cash=100000):
|
||
"""MA20 + 可选止损回测
|
||
stop_loss_pct: 0=无止损,0.08=8%止损
|
||
"""
|
||
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"])
|
||
|
||
cash = initial_cash
|
||
position = 0
|
||
in_position = False
|
||
buy_price = 0
|
||
buy_date = None
|
||
trades = []
|
||
equity = []
|
||
stop_count = 0
|
||
|
||
for i, row in d.iterrows():
|
||
if pd.isna(row["ma20"]):
|
||
continue
|
||
price = row["close"]
|
||
date_str = row["date"].strftime("%Y-%m-%d")
|
||
|
||
# 止损检查(持仓时)
|
||
if in_position and stop_loss_pct > 0:
|
||
dd = (price - buy_price) / buy_price * 100
|
||
if dd <= -stop_loss_pct * 100:
|
||
cash += position * price
|
||
trades.append({"type": "STOP_LOSS", "date": date_str, "price": price,
|
||
"shares": position, "profit_pct": dd, "reason": f"止损{dd:.1f}%"})
|
||
position = 0
|
||
in_position = False
|
||
buy_price = 0
|
||
stop_count += 1
|
||
equity.append({"date": date_str, "value": cash})
|
||
continue
|
||
|
||
# 金叉买入
|
||
if row["golden_cross"] and not in_position:
|
||
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})
|
||
|
||
# 死叉卖出
|
||
elif row["death_cross"] and in_position:
|
||
cash += position * price
|
||
profit_pct = (price - buy_price) / buy_price * 100
|
||
trades.append({"type": "SELL", "date": date_str, "price": price,
|
||
"shares": position, "profit_pct": profit_pct})
|
||
position = 0
|
||
in_position = False
|
||
buy_price = 0
|
||
|
||
equity.append({"date": date_str, "value": cash + position * price})
|
||
|
||
final_price = d.iloc[-1]["close"]
|
||
final_value = cash + position * final_price
|
||
|
||
# 买入持有
|
||
bph = d.iloc[19]["close"] if len(d) > 19 else d.iloc[0]["close"]
|
||
hold_return = (final_price - bph) / bph * 100 if bph else 0
|
||
strat_return = (final_value - initial_cash) / initial_cash * 100
|
||
alpha = strat_return - hold_return
|
||
|
||
# 最大回撤
|
||
eq = [e["value"] for e in equity] or [initial_cash]
|
||
peak = eq[0]
|
||
max_dd = 0
|
||
for v in eq:
|
||
peak = max(peak, v)
|
||
max_dd = max(max_dd, (peak - v) / peak * 100)
|
||
|
||
# 胜率(含止损单)
|
||
exits = [t for t in trades if t["type"] in ("SELL", "STOP_LOSS")]
|
||
wins = [t for t in exits if t.get("profit_pct", 0) > 0]
|
||
win_rate = len(wins) / len(exits) * 100 if exits else 0
|
||
|
||
return {
|
||
"strat_return": strat_return, "hold_return": hold_return,
|
||
"alpha": alpha, "max_dd": max_dd,
|
||
"n_trades": len(exits), "win_rate": win_rate,
|
||
"stop_count": stop_count, "final_value": final_value,
|
||
}
|
||
|
||
|
||
def main():
|
||
stop_levels = [0.0, 0.05, 0.08, 0.10]
|
||
print("=" * 76)
|
||
print("止损机制回测验证 — MA20 + 止损线")
|
||
print("=" * 76)
|
||
|
||
all_results = []
|
||
for code, name, industry in TEST_STOCKS:
|
||
df = get_data(code, 500)
|
||
if df.empty:
|
||
print(f" ⚠️ {name} 数据获取失败")
|
||
continue
|
||
|
||
print(f"\n📊 {name}({code}) [{industry}]")
|
||
print(f" {'止损线':<8} {'策略收益':>10} {'α':>8} {'最大回撤':>10} {'胜率':>7} {'止损次':>6}")
|
||
print(f" {'-'*52}")
|
||
|
||
stock_results = {}
|
||
for sl in stop_levels:
|
||
r = backtest_with_stoploss(df, stop_loss_pct=sl)
|
||
if not r:
|
||
continue
|
||
stock_results[f"{sl:.0%}"] = r
|
||
label = f"{sl:.0%}" if sl > 0 else "无止损"
|
||
print(f" {label:<8} {r['strat_return']:>+9.1f}% {r['alpha']:>+7.1f}% "
|
||
f"{r['max_dd']:>9.1f}% {r['win_rate']:>6.1f}% {r['stop_count']:>6}")
|
||
|
||
all_results.append({"code": code, "name": name, "industry": industry, "results": stock_results})
|
||
|
||
# 汇总:8%止损 vs 无止损
|
||
print("\n" + "=" * 76)
|
||
print("📈 汇总(8%止损 vs 无止损)")
|
||
print("=" * 76)
|
||
alphas_none, alphas_8, dd_none, dd_8 = [], [], [], []
|
||
for r in all_results:
|
||
if "0%" in r["results"] and "8%" in r["results"]:
|
||
alphas_none.append(r["results"]["0%"]["alpha"])
|
||
alphas_8.append(r["results"]["8%"]["alpha"])
|
||
dd_none.append(r["results"]["0%"]["max_dd"])
|
||
dd_8.append(r["results"]["8%"]["max_dd"])
|
||
if alphas_none:
|
||
print(f" 平均 α: 无止损 {np.mean(alphas_none):+.2f}% → 8%止损 {np.mean(alphas_8):+.2f}% "
|
||
f"(差 {np.mean(alphas_8)-np.mean(alphas_none):+.2f}%)")
|
||
print(f" 平均回撤: 无止损 {np.mean(dd_none):.2f}% → 8%止损 {np.mean(dd_8):.2f}% "
|
||
f"(差 {np.mean(dd_8)-np.mean(dd_none):+.2f}%)")
|
||
print(f"\n 结论: {'✅ 8%止损有效(α↑或回撤↓)' if np.mean(dd_8) < np.mean(dd_none) else '⚠️ 止损收益影响需权衡'}")
|
||
|
||
out = OUTPUT / "stoploss_validation.json"
|
||
with open(out, "w", encoding="utf-8") as f:
|
||
json.dump({"generated": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||
"results": all_results}, f, ensure_ascii=False, indent=2, default=str)
|
||
print(f"\n📁 已保存: {out}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|