#!/usr/bin/env python3 """ 小唯股票回测引擎 v3 — MACD策略 ============================== 真实数据源: 腾讯/ifzq K线API (akshare备用) 策略: MACD金叉买/死叉卖 用法: python3 stock_macd_strategy.py <股票代码> [起始日期] [结束日期] python3 stock_macd_strategy.py --demo # 模拟数据 python3 stock_macd_strategy.py 600519 2023-01-01 2026-07-11 """ import json, re, subprocess, sys, urllib.request from datetime import datetime from pathlib import Path import pandas as pd import numpy as np HOME = Path.home() OUTPUT = 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 as e: print(f"飞书推送失败: {e}") # ===================== 真实数据获取 ===================== def get_real_data(stock_code, start_date, end_date): """ 腾讯/ifzq K线API,格式: sh600519 -> 返回前复权日K 返回 DataFrame 或 None(失败时) """ # 转换代码 if stock_code.startswith("6"): mc = f"sh{stock_code}" else: mc = f"sz{stock_code}" url = (f"https://web.ifzq.gtimg.cn/appstock/app/fqkline/get" f"?_var=kline_dayqfq¶m={mc},day,{start_date},{end_date},500,qfq") try: text = urllib.request.urlopen(url, timeout=10).read().decode("utf-8") text = text.replace("kline_dayqfq=", "", 1) data = json.loads(text) qfq = data.get("data", {}).get(mc, {}).get("qfqday") or data.get("data", {}).get(mc, {}).get("day") or [] if not qfq: return None 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 as e: print(f" ⚠️ 真实数据获取失败: {e}") return None # ===================== 模拟数据 ===================== def generate_synthetic(stock_code, start_date, end_date): """生成模拟数据(当真实数据不可用时)""" dates = pd.date_range(start_date, end_date, freq="B") n = len(dates) closes = 10 * np.exp(np.cumsum(np.random.normal(0.0003, 0.02, n))) for i in range(0, n, 20): burst = np.random.choice([-1, 1]) * np.random.uniform(0.005, 0.015) for j in range(min(10, n - i)): closes[i + j] *= (1 + burst) df = pd.DataFrame({"close": closes}, index=dates) df["open"] = df["close"] * (1 + np.random.uniform(-0.005, 0.005, n)) df["high"] = df["close"] * (1 + np.abs(np.random.normal(0, 0.01, n))) df["low"] = df["close"] * (1 - np.abs(np.random.normal(0, 0.01, n))) df["volume"] = np.random.uniform(1e6, 5e6, n) return df # ===================== MACD 计算 ===================== def compute_macd(closes, fast=12, slow=26, signal=9): ema_fast = pd.Series(closes).ewm(span=fast).mean() ema_slow = pd.Series(closes).ewm(span=slow).mean() macd = ema_fast - ema_slow signal_line = macd.ewm(span=signal).mean() hist = macd - signal_line return macd, signal_line, hist # ===================== 回测引擎 ===================== def run_backtest(df, initial_cash=100000, trade_log=None): """纯Python MACD回测""" fast, slow, sig = 12, 26, 9 macd, signal_line, _ = compute_macd(df["close"]) cash = initial_cash shares = 0 peak = initial_cash max_dd = 0.0 trades = [] total_trades = 0 wins = 0 close_arr = df["close"].values for i in range(slow, len(close_arr)): price = close_arr[i] # 金叉 if (macd.iloc[i] > signal_line.iloc[i] and macd.iloc[i-1] <= signal_line.iloc[i-1]): if shares == 0: s = int(cash / price) if s > 0: shares = s cash -= s * price trades.append(("BUY", df.index[i], s, price, cash)) # 死叉 elif (macd.iloc[i] < signal_line.iloc[i] and macd.iloc[i-1] >= signal_line.iloc[i-1]): if shares > 0: proceeds = shares * price won = proceeds > (initial_cash if not trades else 0) if won: wins += 1 trades.append(("SELL", df.index[i], shares, price, cash + proceeds)) cash += proceeds total_trades += 1 shares = 0 equity = cash + shares * price peak = max(peak, equity) dd = (peak - equity) / peak * 100 if peak > 0 else 0 max_dd = max(max_dd, dd) final = cash + shares * close_arr[-1] strat_ret = (final - initial_cash) / initial_cash * 100 bh_ret = (close_arr[-1] - close_arr[0]) / close_arr[0] * 100 return { "initial_cash": initial_cash, "final_value": final, "strategy_return": strat_ret, "buyhold_return": bh_ret, "alpha": strat_ret - bh_ret, "max_drawdown": max_dd, "total_trades": total_trades, "win_rate": wins / total_trades * 100 if total_trades > 0 else 0, "trades": trades, } # ===================== 主流程 ===================== def analyze(stock_code, start, end=None): end = end or datetime.now().strftime("%Y-%m-%d") print(f"\n{'='*50}") print(f"小唯股票回测 — MACD策略") print(f"代码: {stock_code} | 时间: {start} ~ {end}") print(f"{'='*50}") # 获取数据 df = get_real_data(stock_code, start, end) data_src = "真实" if df is not None else "模拟" if df is None or len(df) < 60: print(f" 📊 使用模拟数据") df = generate_synthetic(stock_code, start, end) else: print(f" ✅ 使用{data_src}数据,共 {len(df)} 条") print(f" 运行MACD回测...") result = run_backtest(df) # 输出 print(f"\n{'='*50} 回测结果") print(f" 初始资金: {result['initial_cash']:.0f}") print(f" 最终资产: {result['final_value']:.0f}") print(f" 策略收益: {result['strategy_return']:+.2f}%") print(f" 买入持有: {result['buyhold_return']:+.2f}%") print(f" 超额收益(α):{result['alpha']:+.2f}%") print(f" 最大回撤: {result['max_drawdown']:.2f}%") print(f" 交易次数: {result['total_trades']}") print(f" 胜率: {result['win_rate']:.1f}%") if result["trades"]: print(f"\n 最近5笔交易:") for action, date, qty, price, cash_bal in result["trades"][-5:]: print(f" [{date.date()}] {action} {qty}股@{price:.2f} 余额{cash_bal:.0f}") # 保存 result_file = OUTPUT / f"result_{stock_code}.json" with open(result_file, "w") as f: json.dump({k: str(v) if k == "trades" else v for k, v in result.items()}, f, ensure_ascii=False, indent=2, default=str) # 飞书 msg = f"""📈 小唯股票回测报告 代码: {stock_code} 策略: MACD金叉/死叉 时间: {start} ~ {end} 数据: {data_src} | {len(df)}条 初始资金: {result['initial_cash']:.0f} 最终资产: {result['final_value']:.0f} 策略收益: {result['strategy_return']:+.1f}% 买入持有: {result['buyhold_return']:+.1f}% 超额收益: {result['alpha']:+.1f}% 最大回撤: {result['max_drawdown']:.1f}% 交易次数: {result['total_trades']} 胜率: {result['win_rate']:.0f}% 生成: {datetime.now().strftime('%Y-%m-%d %H:%M')} 小唯股票投研 · 模拟阶段""" send_feishu(msg) print(f"\n✅ 报告已推送,数据存 {result_file}") return result if __name__ == "__main__": if "--demo" in sys.argv: analyze("DEMO", "2023-01-01", "2026-07-11") elif len(sys.argv) < 2: print("用法:") print(" python3 stock_macd_strategy.py <代码> [起始] [结束]") print(" python3 stock_macd_strategy.py --demo") else: code = sys.argv[1] s = sys.argv[2] if len(sys.argv) > 2 else "2023-01-01" e = sys.argv[3] if len(sys.argv) > 3 else datetime.now().strftime("%Y-%m-%d") analyze(code, s, e)