2. RSI超买超卖策略

任务目标

实现一个基于RSI(14)的均值回归策略:当RSI低于30时买入,高于70时卖出。使用A股历史日线数据回测,输出策略累计收益并与同期基准(买入持有)对比。

环境与依赖

需要 pandas、numpy、matplotlib。数据使用 akshare 拉取(若网络受限,可替换为本地CSV,字段需包含 date/open/high/low/close)。

完整实现代码

import pandas as pd
import numpy as np
import akshare as ak

# ---------- 1. 获取数据 ----------
def fetch_data(symbol="000001", start="2020-01-01", end="2023-12-31"):
    df = ak.stock_zh_a_hist(symbol=symbol, period="daily",
                            start_date=start, end_date=end, adjust="qfq")
    df = df.rename(columns={"日期": "date", "开盘": "open", "最高": "high",
                            "最低": "low", "收盘": "close", "成交量": "volume"})
    df = df[["date", "open", "high", "low", "close"]].copy()
    df["date"] = pd.to_datetime(df["date"])
    df = df.set_index("date").sort_index()
    return df

# ---------- 2. RSI 计算 ----------
def calc_rsi(close, period=14):
    delta = close.diff()
    gain = delta.clip(lower=0)
    loss = -delta.clip(upper=0)
    avg_gain = gain.ewm(alpha=1/period, min_periods=period).mean()
    avg_loss = loss.ewm(alpha=1/period, min_periods=period).mean()
    rs = avg_gain / avg_loss
    rsi = 100 - (100 / (1 + rs))
    return rsi

# ---------- 3. 策略信号 ----------
def generate_signals(df, rsi_period=14, buy_th=30, sell_th=70):
    df = df.copy()
    df["rsi"] = calc_rsi(df["close"], rsi_period)
    df["signal"] = 0
    df.loc[df["rsi"] < buy_th, "signal"] = 1   # 超卖买入
    df.loc[df["rsi"] > sell_th, "signal"] = -1  # 超买卖出
    df["position"] = df["signal"].replace(0, np.nan).ffill().fillna(0)
    df["position"] = df["position"].shift(1).fillna(0)  # 次日开盘执行
    return df

# ---------- 4. 回测 ----------
def backtest(df, initial_capital=100000):
    df = df.copy()
    df["daily_ret"] = df["close"].pct_change().fillna(0)
    df["strategy_ret"] = df["position"] * df["daily_ret"]
    df["strategy_equity"] = initial_capital * (1 + df["strategy_ret"]).cumprod()
    df["benchmark_equity"] = initial_capital * (1 + df["daily_ret"]).cumprod()
    return df

# ---------- 5. 运行与输出 ----------
if __name__ == "__main__":
    data = fetch_data()
    sig_df = generate_signals(data)
    result = backtest(sig_df)

    # 打印关键指标
    total_days = len(result)
    in_market_days = int(result["position"].sum())
    strategy_return = result["strategy_equity"].iloc[-1] / 100000 - 1
    benchmark_return = result["benchmark_equity"].iloc[-1] / 100000 - 1

    print(f"回测区间: {result.index[0].date()} ~ {result.index[-1].date()}")
    print(f"总交易日: {total_days}, 持仓天数: {in_market_days}")
    print(f"策略累计收益: {strategy_return:.2%}")
    print(f"基准累计收益: {benchmark_return:.2%}")

    # 绘制净值曲线
    import matplotlib.pyplot as plt
    plt.rcParams["font.sans-serif"] = ["SimHei"]
    plt.rcParams["axes.unicode_minus"] = False
    plt.figure(figsize=(10, 5))
    plt.plot(result.index, result["strategy_equity"], label="RSI策略", linewidth=1.5)
    plt.plot(result.index, result["benchmark_equity"], label="买入持有", linewidth=1.5, alpha=0.7)
    plt.legend()
    plt.title("RSI超买超卖策略回测")
    plt.xlabel("日期")
    plt.ylabel("账户净值")
    plt.grid(alpha=0.3)
    plt.tight_layout()
    plt.show()

运行方式

pip install pandas numpy akshare matplotlib
python rsi_strategy.py

验证方法与通过条件

  • 数据验证:打印的日期区间应为2020-01-01至2023-12-31,总交易日约970天(A股交易日历)。
  • 信号验证:持仓天数应显著小于总交易日(均值回归策略大部分时间空仓),且持仓天数大于0。
  • 逻辑验证:RSI低于30次日买入、高于70次日卖出的信号已通过 position.shift(1) 实现,避免未来函数。
  • 结果判定:程序正常输出两组收益百分比并弹出净值曲线图即视为运行成功。策略收益与基准收益的大小关系取决于行情,不预设优劣;若RSI长期钝化(如单边牛市),策略跑输基准属正常现象。

常见问题

  • akshare数据拉取失败:检查网络或更换 symbol(如"600519")。也可将本地CSV的 date/open/high/low/close 列读入后替换 fetch_data 返回的DataFrame。
  • RSI初始值为NaN:前14个交易日无RSI值,对应 signal=0,策略自动空仓,不影响回测正确性。
  • 信号闪烁:本策略在RSI穿越阈值时立即反转仓位,未做平滑处理。若需减少交易频率,可引入持仓状态锁存(如连续N日低于阈值才买入),但当前最小实现已完整覆盖目标。