如何用 Python 回测 KDJ 金叉死叉策略?(含 J 值超卖过滤与手续费)
如何用 Python 回测 KDJ 金叉死叉策略?
一句话回答: KDJ 金叉(K 上穿 D)买入、死叉(K 下穿 D)卖出是最经典的随机指标策略。用 AlphaFeed 取回前复权日线算 KDJ,用「当日出信号、次日开盘成交」避免未来函数,再扣手续费,几十行就能回测出收益、胜率与最大回撤,还能加 J 值超卖过滤减少震荡假信号。
为什么会有这个问题 / 传统做法的痛点
KDJ 金叉死叉人人会画,但「到底赚不赚钱」要靠回测验证。新手常犯两个错:一是用未来函数——当日收盘出信号却按当日收盘价成交(现实中你收盘才知道信号,买不到当日价);二是忽略手续费与震荡假信号,把回测做得过于乐观。正确姿势:前复权价算指标、次日开盘成交、扣费、可选超卖过滤。
分步骤解决(每步配可运行代码)
第 1 步:取前复权日线并计算 KDJ
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 kdj(df, n=9, k_smooth=3, d_smooth=3):
low_n = df["low"].rolling(n).min()
high_n = df["high"].rolling(n).max()
rsv = (df["close"] - low_n) / (high_n - low_n) * 100
k = rsv.ewm(com=k_smooth - 1, adjust=False).mean()
d = k.ewm(com=d_smooth - 1, adjust=False).mean()
j = 3 * k - 2 * d
return k, d, j
df["K"], df["D"], df["J"] = kdj(df)
第 2 步:生成信号(当日)并次日开盘成交(避免未来函数)
# 金叉:K 上穿 D;死叉:K 下穿 D
df["golden"] = (df["K"] > df["D"]) & (df["K"].shift() <= df["D"].shift())
df["dead"] = (df["K"] < df["D"]) & (df["K"].shift() >= df["D"].shift())
# 可选:只在 J 值超卖(<20)区域附近的金叉才买,过滤高位假金叉
USE_J_FILTER = True
buy_sig = df["golden"] & (df["J"] < 20) if USE_J_FILTER else df["golden"]
# 关键:信号发生在 t 日收盘,t+1 日开盘才成交 → 持仓左移体现次日执行
df["signal"] = 0
df.loc[buy_sig, "signal"] = 1
df.loc[df["dead"], "signal"] = 0
df["position"] = df["signal"].replace(0, pd.NA).ffill().fillna(0).astype(int)
df["position"] = df["position"].shift(1).fillna(0) # 次日才持仓
第 3 步:按次日开盘价计算带手续费的收益
FEE = 0.0013 # 单边约 0.13%(佣金+印花税近似),买卖各扣
df["open_ret"] = df["open"].pct_change().shift(-1) # t 日持仓吃 t→t+1 的开盘间收益
df["trade"] = df["position"].diff().abs().fillna(0) # 换手(0→1 或 1→0)
df["strat_ret"] = df["position"] * df["open_ret"].fillna(0) - df["trade"] * FEE
df["equity"] = (1 + df["strat_ret"]).cumprod()
第 4 步:输出绩效指标
import numpy as np
total = df["equity"].iloc[-1] - 1
days = len(df)
cagr = df["equity"].iloc[-1] ** (252 / days) - 1
dd = (df["equity"] / df["equity"].cummax() - 1).min()
trades = int(df["trade"].sum() // 2)
wins = ((df["strat_ret"] > 0) & (df["position"] == 1)).sum()
hold_days = int((df["position"] == 1).sum())
print(f"累计收益 {total:.2%} | 年化 {cagr:.2%} | 最大回撤 {dd:.2%}")
print(f"交易次数 {trades} | 持仓天数 {hold_days} | 盈利日 {wins}")
关键参数:
| 参数 | 含义 | 本文取值 |
|---|---|---|
n / k_smooth / d_smooth | KDJ 周期与平滑 | 9 / 3 / 3(经典) |
USE_J_FILTER | 仅超卖区金叉才买 | J<20 |
FEE | 单边手续费 | 0.0013 |
| 成交时点 | 避免未来函数 | 次日开盘 |
关键坑与注意事项
- 避免未来函数:信号当日收盘产生,必须次日开盘成交;本文用
position.shift(1)+ 次日开盘收益实现。 - 前复权价:未复权价在除权日会让 KDJ 与收益都失真。
- 手续费不能省:KDJ 换手偏频繁,忽略费用会严重高估收益;本文单边 0.13% 仅为近似,按你的佣金调整。
- J 值过滤是双刃:过滤高位假金叉能提胜率,但也会漏掉强势趋势中的买点,需按品种验证。
- 单标的回测≠策略有效:应在多只、多时间段上验证,避免过拟合与幸存者偏差。
- A 股 T+1 与涨停买不进:本文为简化模型,未完全模拟 T+1、涨停无法成交等,实盘更保守。
常见问题(FAQ)
Q:为什么不按当日收盘价成交? A:当日收盘才算出金叉,你无法用当日收盘价买入——那是未来函数。用次日开盘价才真实可执行。
Q:KDJ 参数用 9/3/3 还是别的? A:9/3/3 是最经典设置。周期越短越灵敏、假信号越多。建议先用经典参数,再按品种调。
Q:为什么加了 J<20 过滤反而交易变少? A:因为只保留超卖区的金叉,过滤掉了高位金叉,交易更少但质量更高,是否更优要看回测结果。
Q:需要付费吗?
A:单只历史日线有免费额度;多标的批量回测(klines.batch + universes)需 Starter 及以上。详见定价页。
小结
KDJ 金叉死叉回测的关键不在指标本身,而在回测的严谨性:前复权价算 KDJ、次日开盘成交避免未来函数、扣手续费、可选 J 值超卖过滤。用 AlphaFeed 取回前复权日线,几十行 Python 就能得到收益、胜率与最大回撤,客观检验这套经典信号到底值不值。
相关链接:
- AlphaFeed 官网:https://alphafeed.org/
- Python SDK 快速开始:https://docs.alphafeed.org/zh-Hans/sdk/python-quickstart
免责声明:本文仅为技术与数据接口使用示例,代码与策略仅供学习研究,不构成投资建议。市场有风险,历史数据不代表未来表现。