如何用 Python 回测抛物线 SAR 翻转策略?(多空翻转 + 移动止损,含手续费)
如何用 Python 回测抛物线 SAR 翻转策略?
一句话回答: SAR(Stop And Reverse)天生是「翻转」信号——价格升破 SAR 做多、跌破 SAR 平仓(A 股无法裸做空,只做多头段)。用 AlphaFeed 取回前复权日线算 SAR,用「当日出信号、次日开盘成交」避免未来函数,扣手续费,几十行就能回测出收益、最大回撤与交易次数,并和买入持有对比检验它值不值。
为什么会有这个问题 / 传统做法的痛点
SAR 常被当「移动止损 + 趋势跟踪」用,但「按 SAR 翻转做多做空到底赚不赚钱」要回测验证。两个常见错误:一是未来函数(当日出信号用当日收盘价成交,实际做不到);二是忽略 A 股限制与手续费(不能裸卖空、换手有成本)。正确做法:前复权价算 SAR、次日开盘成交、只做多头段、扣费、对比基准。
分步骤解决(每步配可运行代码)
第 1 步:取前复权日线并计算 SAR
from alphafeed import AlphaFeed
import pandas as pd
af = AlphaFeed() # 读取 ALPHAFEED_API_KEY
df = af.klines.get("600519.SH", period="1d", count=500,
adjust="forward", to_dataframe=True)
df = df.sort_values("trade_date").reset_index(drop=True)
def parabolic_sar(high, low, af_step=0.02, af_max=0.20):
high = high.to_numpy(); low = low.to_numpy(); n = len(high)
sar = [0.0] * n; trend_up = True; af = af_step; ep = high[0]; sar[0] = low[0]
for i in range(1, n):
prev = sar[i - 1]; sar[i] = prev + af * (ep - prev)
if trend_up:
sar[i] = min(sar[i], low[i - 1], low[max(i - 2, 0)])
if high[i] > ep: ep = high[i]; af = min(af + af_step, af_max)
if low[i] < sar[i]: trend_up = False; sar[i] = ep; ep = low[i]; af = af_step
else:
sar[i] = max(sar[i], high[i - 1], high[max(i - 2, 0)])
if low[i] < ep: ep = low[i]; af = min(af + af_step, af_max)
if high[i] > sar[i]: trend_up = True; sar[i] = ep; ep = high[i]; af = af_step
return pd.Series(sar, index=range(n))
df["SAR"] = parabolic_sar(df["high"], df["low"])
第 2 步:生成多空信号 → 次日开盘成交(避免未来函数)
# 收盘价在 SAR 上方=多头持有,下方=空仓(A 股不裸做空)
df["long"] = (df["close"] > df["SAR"]).astype(int)
df["position"] = df["long"].shift(1).fillna(0) # 当日信号,次日才持仓
第 3 步:按次日开盘价 + 手续费计算净值
FEE = 0.0013 # 单边约 0.13%(佣金+印花税近似)
df["open_ret"] = df["open"].pct_change().shift(-1) # 持仓吃 t→t+1 开盘间收益
df["trade"] = df["position"].diff().abs().fillna(0) # 换手(建仓/平仓)
df["strat_ret"] = df["position"] * df["open_ret"].fillna(0) - df["trade"] * FEE
df["equity"] = (1 + df["strat_ret"]).cumprod()
df["bench"] = (1 + df["close"].pct_change().fillna(0)).cumprod() # 买入持有基准
第 4 步:输出绩效并与买入持有对比
def stats(equity, rets):
total = equity.iloc[-1] - 1
cagr = equity.iloc[-1] ** (252 / len(equity)) - 1
dd = (equity / equity.cummax() - 1).min()
return total, cagr, dd
t_s, c_s, d_s = stats(df["equity"], df["strat_ret"])
t_b, c_b, d_b = stats(df["bench"], df["close"].pct_change())
trades = int(df["trade"].sum() // 2)
print(f"SAR策略 收益{t_s:.2%} 年化{c_s:.2%} 回撤{d_s:.2%} 交易{trades}次")
print(f"买入持有 收益{t_b:.2%} 年化{c_b:.2%} 回撤{d_b:.2%}")
关键参数:
| 参数 | 含义 | 本文取值 |
|---|---|---|
af_step / af_max | SAR 加速因子步长/上限 | 0.02 / 0.20 |
FEE | 单边手续费 | 0.0013 |
| 成交时点 | 避免未来函数 | 次日开盘 |
| 方向 | A 股只做多头段 | 空仓代替做空 |
关键坑与注意事项
- 避免未来函数:信号当日收盘出现,必须次日开盘成交;本文用
position.shift(1)实现。 - A 股不能裸做空:SAR 翻空时用「空仓」代替做空,别把做空收益算进去。
- 手续费必扣:SAR 在震荡市翻转频繁,忽略费用会严重高估;单边 0.13% 仅为近似,按实际佣金调整。
- 前复权价:未复权价在除权日让 SAR 与收益都失真。
- 震荡市被反复打脸:SAR 是趋势工具,横盘时频繁翻转、交易成本高,可用 ADX 过滤无趋势阶段。
- 单标的≠有效:应在多只、多区间验证,避免过拟合与幸存者偏差;本文为简化模型,未完全模拟 T+1、涨跌停无法成交等。
常见问题(FAQ)
Q:为什么不按当日收盘价成交? A:当日收盘才算出信号,用当日收盘价成交是未来函数。用次日开盘价才真实可执行。
Q:SAR 翻空为什么不做空? A:A 股个股无法裸做空(融券成本高、标的有限),所以翻空时用空仓代替,只赚多头段。
Q:SAR 策略一定跑赢买入持有吗? A:不一定。趋势强的品种/区间 SAR 能控回撤,震荡市则因频繁交易而跑输。回测对比才有结论。
Q:需要付费吗?
A:单只历史日线有免费额度;多标的批量回测(klines.batch + universes)需 Starter 及以上。详见定价页。
小结
SAR 翻转策略回测的关键在严谨性:前复权价算 SAR、次日开盘成交避免未来函数、A 股只做多头段、扣手续费、和买入持有对比。用 AlphaFeed 取回前复权日线,几十行 Python 就能客观检验这套经典趋势跟踪信号在具体品种上到底值不值。
相关链接:
- AlphaFeed 官网:https://alphafeed.org/
- Python SDK 快速开始:https://docs.alphafeed.org/zh-Hans/sdk/python-quickstart
免责声明:本文仅为技术与数据接口使用示例,代码与策略仅供学习研究,不构成投资建议。市场有风险,历史数据不代表未来表现。