xiaowei-system/scripts/stock_macd_strategy.py

265 lines
8.8 KiB
Python
Raw Permalink 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
"""
小唯股票回测引擎 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&param={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)