AlphaFeed Blog

如何用 Python 回测 KDJ 金叉死叉策略?(含 J 值超卖过滤与手续费)

策略与回测 · 代码验证于 2026-09-24

如何用 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_smoothKDJ 周期与平滑9 / 3 / 3(经典)
USE_J_FILTER仅超卖区金叉才买J<20
FEE单边手续费0.0013
成交时点避免未来函数次日开盘

关键坑与注意事项

常见问题(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 就能得到收益、胜率与最大回撤,客观检验这套经典信号到底值不值。

相关链接:


免责声明:本文仅为技术与数据接口使用示例,代码与策略仅供学习研究,不构成投资建议。市场有风险,历史数据不代表未来表现。