#!/usr/bin/env python3 """ 小唯自适应策略 — 智能切换MACD/MA20/买入持有 ========================================== 根据市场环境自动选择最优策略 判断逻辑: - 趋势强度(ADX) > 25 → 趋势市场 → MA20突破(追涨) - ADX <= 25 → 震荡市场 → MACD(区间波动) - 持仓时ADX突然下降 → 快速离场 用法: python3 stock_adaptive.py <代码> [起始] [结束] python3 stock_adaptive.py --batch # 批量测5只股票 """ import json, sys, urllib.request from datetime import datetime from pathlib import Path import numpy as np import pandas as pd OUTPUT = Path.home() / ".hermes" / "stock_backtest" OUTPUT.mkdir(exist_ok=True) FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/446db983-e392-4d2c-bfb8-f9060e5df3ad" def send_feishu(msg): payload = json.dumps({"msg_type": "text", "content": {"text": msg}}).encode() req = urllib.request.Request(FEISHU_WEBHOOK, data=payload, headers={"Content-Type": "application/json"}) try: with urllib.request.urlopen(req, timeout=10): pass except Exception: pass def get_data(code, start, end): mc = f"sh{code}" if code.startswith("6") else f"sz{code}" url = (f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get" f"?_var=kline_dayqfq¶m={mc},day,{start},{end},500,qfq") try: text = urllib.request.urlopen(url, timeout=10).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 try: 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])}) except (ValueError, IndexError): continue df = pd.DataFrame(rows) df["date"] = pd.to_datetime(df["date"]) df.set_index("date", inplace=True) df.sort_index(inplace=True) return df except Exception: return None def compute_adx(df, n=14): """计算ADX指标(趋势强度),>25表示趋势市场""" high = df["high"] low = df["low"] close = df["close"] # +DM, -DM plus_dm = high.diff() minus_dm = -low.diff() plus_dm[plus_dm < 0] = 0 minus_dm[minus_dm < 0] = 0 # True Range tr1 = high - low tr2 = abs(high - close.shift()) tr3 = abs(low - close.shift()) tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1) # ADX atr = tr.rolling(n).mean() plus_di = (plus_dm.rolling(n).mean() / atr * 100).fillna(0) minus_di = (minus_dm.rolling(n).mean() / atr * 100).fillna(0) dx = (abs(plus_di - minus_di) / (plus_di + minus_di + 1e-9) * 100).fillna(0) adx = dx.rolling(n).mean() return adx def adaptive_backtest(df, cash=100000, stop_loss_pct=8.0, max_position_pct=30.0): """ 自适应策略: ADX>25用MA20,ADX<=25用MACD 新增风险管理: - 止损: 单笔回撤超过stop_loss_pct%强制平仓 - 仓位: 最高不超过max_position_pct%的资金 - 最大回撤监控: 实时追踪Equity Peak """ closes = df["close"].values n = len(closes) # 预计算所有指标 ma20 = pd.Series(closes).rolling(20).mean() adx = compute_adx(df, 14) adx_vals = adx.fillna(0).values ema12 = pd.Series(closes).ewm(span=12).mean() ema26 = pd.Series(closes).ewm(span=26).mean() macd = ema12 - ema26 signal = macd.ewm(span=9).mean() macd_vals = macd.values signal_vals = signal.values # 持仓状态 shares = 0 c = cash peak = cash max_dd = 0.0 trades = [] regime_changes = [] stop_loss_triggered = 0 # 止损次数 for i in range(26, n): price = closes[i] regime = "TREND" if adx_vals[i] > 25 else "RANGE" # === 止损检查(任何持仓状态都检查)=== if shares > 0: cost = trades[-1][3] if trades and trades[-1][0] == "BUY" else 0 if cost > 0: drawdown_pct = (cost - price) / cost * 100 if drawdown_pct >= stop_loss_pct: c += shares * price trades.append(("STOP_LOSS", df.index[i], shares, price, f"止损-{drawdown_pct:.1f}%")) shares = 0 stop_loss_triggered += 1 continue # 本周期不再处理其他信号 # === MA20突破信号(趋势市场) === if regime == "TREND": if shares == 0: if closes[i] > ma20.iloc[i] and closes[i-1] <= ma20.iloc[i-1]: # 仓位限制 invest = min(c * (max_position_pct / 100), c) s = int(invest / price) if s > 0: c -= s * price shares = s trades.append(("BUY", df.index[i], s, price, "MA20_TREND")) elif shares > 0: if closes[i] < ma20.iloc[i] and closes[i-1] >= ma20.iloc[i-1]: c += shares * price trades.append(("SELL", df.index[i], shares, price, "MA20_TREND")) shares = 0 # === MACD信号(震荡市场) === else: # RANGE if shares == 0: if macd_vals[i] > signal_vals[i] and macd_vals[i-1] <= signal_vals[i-1]: invest = min(c * (max_position_pct / 100), c) s = int(invest / price) if s > 0: c -= s * price shares = s trades.append(("BUY", df.index[i], s, price, "MACD_RANGE")) elif shares > 0: if macd_vals[i] < signal_vals[i] and macd_vals[i-1] >= signal_vals[i-1]: c += shares * price trades.append(("SELL", df.index[i], shares, price, "MACD_RANGE")) shares = 0 equity = c + shares * price peak = max(peak, equity) dd = (peak - equity) / peak * 100 if peak > 0 else 0 max_dd = max(max_dd, dd) final = c + shares * closes[-1] ret = (final - cash) / cash * 100 bh = (closes[-1] - closes[0]) / closes[0] * 100 # 统计 total_trades = len([t for t in trades if t[0] == "SELL"]) ma20_trades = len([t for t in trades if t[0] == "SELL" and t[4] == "MA20_TREND"]) macd_trades = len([t for t in trades if t[0] == "SELL" and t[4] == "MACD_RANGE"]) wins = len([t for t in trades if t[0] in ("SELL", "STOP_LOSS") and len(t) > 3 and t[3] > 0]) return { "final": final, "return": ret, "buyhold": bh, "alpha": ret - bh, "max_dd": max_dd, "total_trades": total_trades, "ma20_trades": ma20_trades, "macd_trades": macd_trades, "winrate": wins / total_trades * 100 if total_trades > 0 else 0, "stop_loss_triggered": stop_loss_triggered, "trades": [(t[0], str(t[1].date()), t[2], t[3], t[4]) for t in trades], } def run_benchmark(df, cash=100000): """跑三个基准策略用于对比""" closes = df["close"].values # MACD ema12 = pd.Series(closes).ewm(span=12).mean() ema26 = pd.Series(closes).ewm(span=26).mean() macd = ema12 - ema26 signal = macd.ewm(span=9).mean() shares = 0; c = cash; trades = 0 for i in range(26, len(closes)): p = closes[i] if macd.iloc[i] > signal.iloc[i] and macd.iloc[i-1] <= signal.iloc[i-1]: if shares == 0: n = int(c / p); c -= n * p; shares = n elif macd.iloc[i] < signal.iloc[i] and macd.iloc[i-1] >= signal.iloc[i-1]: if shares > 0: c += shares * p; shares = 0; trades += 1 final = c + shares * closes[-1] macd_ret = (final - cash) / cash * 100 # MA20 ma20 = pd.Series(closes).rolling(20).mean() shares = 0; c = cash; trades = 0 for i in range(20, len(closes)): p = closes[i] if closes[i] > ma20.iloc[i] and closes[i-1] <= ma20.iloc[i-1]: if shares == 0: n = int(c / p); c -= n * p; shares = n elif closes[i] < ma20.iloc[i] and closes[i-1] >= ma20.iloc[i-1]: if shares > 0: c += shares * p; shares = 0; trades += 1 final = c + shares * closes[-1] ma20_ret = (final - cash) / cash * 100 bh = (closes[-1] - closes[0]) / closes[0] * 100 return {"macd": macd_ret, "ma20": ma20_ret, "buyhold": bh} def analyze(code, start, end=None): end = end or datetime.now().strftime("%Y-%m-%d") df = get_data(code, start, end) if df is None or len(df) < 60: print(f"数据获取失败"); return result = adaptive_backtest(df) bench = run_benchmark(df) print(f"\n{'='*60}") print(f"自适应策略 vs 基准 | {code} | {start} ~ {end}") print(f"{'='*60}") print(f"{'策略':<22} {'收益':>10} {'α':>10} {'最大回撤':>10} {'交易':>6}") print(f"{'-'*60}") print(f"{'自适应(ADX切换)':<22} {result['return']:>+9.1f}% {result['alpha']:>+9.1f}% {result['max_dd']:>9.1f}% {result['total_trades']:>6}") print(f"{'MA20突破(固定)':<22} {bench['ma20']:>+9.1f}% {bench['ma20']-bench['buyhold']:>+9.1f}%") print(f"{'MACD(固定)':<22} {bench['macd']:>+9.1f}% {bench['macd']-bench['buyhold']:>+9.1f}%") print(f"{'买入持有':<22} {bench['buyhold']:>+9.1f}% {'基准':>9}") print(f"{'='*60}") print(f"自适应内: MA20触发{result['ma20_trades']}次, MACD触发{result['macd_trades']}次") print(f"胜率: {result['winrate']:.0f}%") # 飞书 msg = f"""📊 自适应策略对比 代码: {code} | {start} ~ {end} | {len(df)}条 策略收益对比: 自适应(ADX切换): {result['return']:+.1f}% (α={result['alpha']:+.1f}%, 回撤{result['max_dd']:.1f}%, {result['total_trades']}笔) MA20突破(固定): {bench['ma20']:+.1f}% MACD(固定): {bench['macd']:+.1f}% 买入持有基准: {bench['buyhold']:+.1f}% 风险管理: - 止损线: 8%强制平仓(触发{result['stop_loss_triggered']}次) - 仓位上限: 30%单笔 - 最大回撤: {result['max_dd']:.1f}% 自适应内: MA20触发{result['ma20_trades']}次, MACD触发{result['macd_trades']}次 胜率: {result['winrate']:.0f}% 生成: {datetime.now().strftime('%Y-%m-%d %H:%M')} 小唯股票投研 · 自适应策略""" send_feishu(msg) result_file = OUTPUT / f"adaptive_{code}.json" with open(result_file, "w") as f: json.dump({"code": code, "start": start, "end": end, "adaptive": result, "benchmark": bench}, f, ensure_ascii=False, indent=2, default=str) print(f"数据存: {result_file}") return result def batch_test(): """批量测5只股票""" stocks = [ ("600519", "贵州茅台", "2023-01-01"), ("000858", "五粮液", "2023-01-01"), ("000001", "平安银行", "2023-01-01"), ("300750", "宁德时代", "2023-01-01"), ("510300", "沪深300ETF", "2023-01-01"), ] end = datetime.now().strftime("%Y-%m-%d") results = [] print(f"\n{'='*70}") print(f"{'股票':<12} {'基准':>8} {'MA20':>8} {'MACD':>8} {'自适应α':>10} {'自适应回撤':>10} {'自适应胜率':>8}") print(f"{'-'*70}") for code, name, start in stocks: df = get_data(code, start, end) if df is None: print(f"{name:<12} 数据获取失败") continue r = adaptive_backtest(df) b = run_benchmark(df) results.append({ "name": name, "code": code, "buyhold": b["buyhold"], "ma20": b["ma20"], "macd": b["macd"], "adaptive": r["return"], "adaptive_alpha": r["alpha"], "adaptive_dd": r["max_dd"], "adaptive_wr": r["winrate"], "ma20_trades": r["ma20_trades"], "macd_trades": r["macd_trades"], }) print(f"{name:<12} {b['buyhold']:>+7.1f}% {b['ma20']:>+7.1f}% {b['macd']:>+7.1f}% {r['alpha']:>+9.1f}% {r['max_dd']:>9.1f}% {r['winrate']:>7.0f}%") print(f"{'='*70}") # 汇总 avg_alpha = np.mean([x["adaptive_alpha"] for x in results]) win_count = sum(1 for x in results if x["adaptive_alpha"] > 0) print(f"\n自适应策略平均α: {avg_alpha:+.1f}% | 跑赢基准: {win_count}/{len(results)}只") msg = f"""📊 自适应策略批量测试(5只) | 股票 | 基准 | MA20 | MACD | 自适应α | 回撤 | 胜率 | |------|------|------|------|--------|------|------| """ for x in results: msg += f"| {x['name']} | {x['buyhold']:+.0f}% | {x['ma20']:+.0f}% | {x['macd']:+.0f}% | **{x['adaptive_alpha']:+.1f}%** | {x['adaptive_dd']:.0f}% | {x['adaptive_wr']:.0f}% |\n" msg += f""" 平均超额收益(α): **{avg_alpha:+.1f}%** 跑赢基准: {win_count}/{len(results)}只 自适应策略: ADX>25用MA20突破, ADX≤25用MACD 结论: {'有效 ✅' if avg_alpha > 0 else '无效 ❌'} 生成: {datetime.now().strftime('%Y-%m-%d %H:%M')}""" send_feishu(msg) print("\n✅ 批量测试完成,已推送飞书") if __name__ == "__main__": if "--batch" in sys.argv: batch_test() elif len(sys.argv) < 2: print("用法: python3 stock_adaptive.py <代码> [起始] [结束]") print(" python3 stock_adaptive.py --batch") else: code = sys.argv[1] start = sys.argv[2] if len(sys.argv) > 2 else "2023-01-01" end = sys.argv[3] if len(sys.argv) > 3 else datetime.now().strftime("%Y-%m-%d") analyze(code, start, end)