xiaowei-system/scripts/stock_adaptive.py

369 lines
14 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
"""
小唯自适应策略 — 智能切换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&param={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用MA20ADX<=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)