Python量化策略实验004:单股专属!RSI超买超卖策略搭建与回测

Python量化策略实验004:单股专属!RSI超买超卖策略搭建与回测

大家好,我是量化老王。上一篇(实验003)咱们用网格搜索优化了均线策略的参数,让单只股票(贵州茅台)的交易胜率大幅提升。有朋友留言说不需要多股票版本,希望聚焦单股做更精准的策略,今天安排!实验004咱们开启一个全新策略——RSI超买超卖策略,这是针对单股震荡行情的“利器”,和之前的趋势型均线策略形成互补,帮你搞定单股不同行情下的交易逻辑。

先搞懂核心逻辑:RSI全称是相对强弱指标,核心是衡量股票在一段时间内的上涨和下跌力度,判断股价是否“涨过头”(超买)或“跌过头”(超卖)。常见的RSI周期是14日,取值范围0~100:当RSI>70时,认为股票超买,后续下跌概率大,发出卖出信号;当RSI<30时,认为股票超卖,后续上涨概率大,发出买入信号。这个策略特别适合单股的震荡整理行情,比均线策略在震荡市中更灵活。

实验前提:延续前几篇的环境(Python 3.8+),数据源还是用akshare获取贵州茅台2023年日线数据(单股聚焦),核心库新增“ta”(专门计算技术指标,无需手动写RSI公式,新手友善),其他库保持pandas、matplotlib不变。

第一步:安装新增库,获取并预处理数据。数据部分和前几篇保持一致,确保收盘价、成交量等核心字段齐全,方便后续策略对比。

# 导入核心库
import akshare as ak
import pandas as pd
import matplotlib.pyplot as plt
import ta  # 用于计算RSI指标(需安装)

# 安装ta库(若未安装)
# pip install ta

# 1. 获取贵州茅台2023年日线数据(前复权,含成交量)
df = ak.stock_zh_a_hist(symbol="600519", period="daily", 
                        start_date="20230101", end_date="20231231", 
                        adjust="qfq")

# 2. 数据预处理(和前几篇保持一致,保证连贯性)
df["日期"] = pd.to_datetime(df["日期"])
df.set_index("日期", inplace=True)
df = df.sort_index()
# 重命名字段为英文,方便后续处理
df.rename(columns={"开盘":"open", "收盘":"close", "最高":"high", "最低":"low", "成交量":"volume"}, inplace=True)
# 只保留核心字段
df = df[["open", "close", "high", "low", "volume"]]

print("数据预处理完成,预览前5行:")
print(df.head())

这里重点说明:用ta库计算RSI能大幅简化代码,避免手动写复杂的求和公式,且ta库支持多种技术指标(后续实验会用到),提前安装好很有必要。

第二步:计算RSI指标,生成基础交易信号。我们用行业通用的14日RSI,同时设定超买阈值70、超卖阈值30,生成基础买卖信号。

# 1. 计算14日RSI指标(核心步骤,用ta库一行搞定)
# ta.momentum.RSIIndicator的参数:close=收盘价,window=周期
rsi_indicator = ta.momentum.RSIIndicator(close=df["close"], window=14)
df["rsi_14"] = rsi_indicator.rsi()  # 得到14日RSI数值

# 2. 设定超买超卖阈值(行业通用:超买>70,超卖<30)
OVER_BUY = 70
OVER_SELL = 30

# 3. 生成基础交易信号
# 买入信号:RSI从低于30回升到30以上(避免在超卖区间内多次触发)
df["buy_signal_base"] = (df["rsi_14"].shift(1) < OVER_SELL) & (df["rsi_14"] >= OVER_SELL)
# 卖出信号:RSI从高于70回落至70以下(避免在超买区间内多次触发)
df["sell_signal_base"] = (df["rsi_14"].shift(1) > OVER_BUY) & (df["rsi_14"] <= OVER_BUY)

# 4. 生成统一信号列:1=买入,0=卖出,None=无信号
df["signal_base"] = None
df.loc[df["buy_signal_base"], "signal_base"] = 1
df.loc[df["sell_signal_base"], "signal_base"] = 0

# 查看基础信号分布
print(f"
2023年RSI基础买入信号数:{df['buy_signal_base'].sum()}个")
print(f"2023年RSI基础卖出信号数:{df['sell_signal_base'].sum()}个")
# 查看包含信号的数据
print("
基础交易信号详情:")
print(df[df["signal_base"].notna()][["close", "rsi_14", "signal_base"]])

关键细节:生成信号时用“shift(1)”判断前一天的RSI状态,避免在超买/超卖区间内多次触发信号(列如RSI连续3天低于30,只在首次回升到30以上时触发买入),减少无效交易。运行后会发现,2023年茅台的RSI信号聚焦在几次震荡行情中,趋势行情中信号较少——这符合RSI策略的特性。

第三步:优化信号!加入成交量过滤,避免虚假信号。和均线策略类似,RSI信号也需要量能配合:超卖反弹时成交量放大,说明买盘真实;超买下跌时成交量放大,说明卖盘强劲。

# 1. 计算5日成交量均值(复用实验002的过滤逻辑,保持一致性)
df["volume_ma5"] = df["volume"].rolling(window=5).mean()

# 2. 叠加成交量过滤条件:信号当天成交量>5日量均(量能放大)
df["buy_signal_valid"] = df["buy_signal_base"] & (df["volume"] > df["volume_ma5"])
df["sell_signal_valid"] = df["sell_signal_base"] & (df["volume"] > df["volume_ma5"])

# 3. 生成最终有效信号
df["signal"] = None
df.loc[df["buy_signal_valid"], "signal"] = 1
df.loc[df["sell_signal_valid"], "signal"] = 0

# 对比基础信号和有效信号数量,看过滤效果
print(f"
过滤后买入信号数:{df['buy_signal_valid'].sum()}个")
print(f"过滤后卖出信号数:{df['sell_signal_valid'].sum()}个")
print(f"被过滤的虚假信号数:{ (df['buy_signal_base'].sum() + df['sell_signal_base'].sum()) - (df['buy_signal_valid'].sum() + df['sell_signal_valid'].sum()) }个")

# 查看有效信号详情
print("
有效交易信号详情(含量能过滤):")
print(df[df["signal"].notna()][["close", "rsi_14", "volume", "volume_ma5", "signal"]])

优化逻辑:震荡行情中,若RSI达到超卖但成交量萎缩,可能是“假反弹”(后续仍会下跌);只有伴随成交量放大的超卖信号,才是真实的买入机会。从结果能看到,部分基础信号被过滤掉,有效信号的质量更高。

第四步:策略回测(含手续费+滑点,贴近实盘)。复用实验003的回测逻辑,初始资金10万元,加入A股常见的手续费和滑点,对比RSI策略和之前均线策略的收益,看单股不同策略的表现。

# 封装回测函数(复用实验003的成熟逻辑,保证对比公平)
def backtest_strategy(df, signal_col, initial_capital=100000):
    df_backtest = df.copy()
    # 实盘成本参数(和实验003一致)
    commission_rate = 0.0003  # 佣金率0.03%
    slippage_rate = 0.001     # 滑点率0.1%
    
    # 初始化回测字段
    df_backtest["position"] = 0  # 0=空仓,1=满仓
    df_backtest["cash"] = initial_capital
    df_backtest["value"] = initial_capital
    share_num = 0  # 持股数量
    
    for i in range(len(df_backtest)):
        current_signal = df_backtest.iloc[i][signal_col]
        current_position = df_backtest.iloc[i]["position"]
        current_close = df_backtest.iloc[i]["close"]
        
        # 买入逻辑(含成本)
        if current_signal == 1 and current_position == 0:
            df_backtest.iloc[i, df_backtest.columns.get_loc("position")] = 1
            buy_price = current_close * (1 + slippage_rate)  # 滑点成本
            available_cash = df_backtest.iloc[i]["cash"]
            # 计算可买股数(扣除手续费)
            share_num = available_cash // (buy_price * (1 + commission_rate))
            total_cost = share_num * buy_price * (1 + commission_rate)
            df_backtest.iloc[i, df_backtest.columns.get_loc("cash")] = available_cash - total_cost
        
        # 卖出逻辑(含成本)
        elif current_signal == 0 and current_position == 1:
            df_backtest.iloc[i, df_backtest.columns.get_loc("position")] = 0
            sell_price = current_close * (1 - slippage_rate)  # 滑点成本
            total_income = share_num * sell_price * (1 - commission_rate)  # 扣除手续费
            df_backtest.iloc[i, df_backtest.columns.get_loc("cash")] += total_income
            share_num = 0
        
        # 无信号:延续前一天状态
        else:
            df_backtest.iloc[i, df_backtest.columns.get_loc("position")] = df_backtest.iloc[i-1]["position"]
            df_backtest.iloc[i, df_backtest.columns.get_loc("cash")] = df_backtest.iloc[i-1]["cash"]
        
        # 计算总资产
        if df_backtest.iloc[i]["position"] == 1:
            df_backtest.iloc[i, df_backtest.columns.get_loc("value")] = df_backtest.iloc[i]["cash"] + share_num * current_close
        else:
            df_backtest.iloc[i, df_backtest.columns.get_loc("value")] = df_backtest.iloc[i]["cash"]
    
    # 计算回测指标
    final_value = df_backtest.iloc[-1]["value"]
    total_return = (final_value - initial_capital) / initial_capital * 100
    return total_return, df_backtest

# 回测RSI优化后策略
rsi_return, df_rsi = backtest_strategy(df, "signal")

# 对比实验003的最优均线策略收益(这里直接用003的典型结果,也可自行跑003代码获取准确值)
optimal_ma_return = 18.5  # 实验003最优均线策略典型收益率(示例值,以实际跑出来为准)

# 输出回测结果
print("="*60)
print("RSI超买超卖策略(含量能过滤)回测结果:")
print(f"初始资金:100000元")
print(f"最终总资产:{df_rsi.iloc[-1]['value']:.2f}元")
print(f"总收益率:{rsi_return:.2f}%")
print("="*60)
print(f"实验003最优均线策略收益率:{optimal_ma_return:.2f}%")
print(f"同期贵州茅台股价涨幅:{ ((df.iloc[-1]['close'] - df.iloc[0]['close']) / df.iloc[0]['close'] * 100):.2f}%")
print("="*60)

关键对比:RSI策略和均线策略的互补性体目前行情适配性上——2023年茅台既有趋势行情也有震荡行情,均线策略在趋势行情中表现好,RSI策略在震荡行情中更灵活。从回测结果看,RSI策略收益率大致率能接近甚至超过最优均线策略,为单股交易提供了另一种有效思路。

第五步:可视化RSI策略信号,直观验证效果。将股价、RSI曲线、超买超卖阈值和有效信号放在同一张图上,清晰看到信号触发的时机。

# 设置中文字体,避免乱码
plt.rcParams["font.sans-serif"] = ["SimHei"]
plt.rcParams["axes.unicode_minus"] = False

# 创建画布(上下分栏:上为股价+信号,下为RSI曲线)
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8), gridspec_kw={"height_ratios": [3, 1]})

# 上半部分:股价+有效交易信号
ax1.plot(df_rsi["close"], label="贵州茅台收盘价", color="blue", linewidth=1.5)
# 买入信号(红色上箭头)
ax1.scatter(df_rsi[df_rsi["signal"] == 1].index, df_rsi[df_rsi["signal"] == 1]["close"], 
           color="red", marker="^", s=120, label="有效买入信号", zorder=5)
# 卖出信号(绿色下箭头)
ax1.scatter(df_rsi[df_rsi["signal"] == 0].index, df_rsi[df_rsi["signal"] == 0]["close"], 
           color="green", marker="v", s=120, label="有效卖出信号", zorder=5)
ax1.set_title("Python量化策略实验004:RSI超买超卖策略信号图(含量能过滤)", fontsize=14)
ax1.set_ylabel("价格(元)", fontsize=12)
ax1.legend(loc="upper left")
ax1.grid(True, alpha=0.3)

# 下半部分:RSI曲线+超买超卖阈值
ax2.plot(df_rsi["rsi_14"], label="14日RSI", color="purple", linewidth=1.5)
ax2.axhline(y=OVER_BUY, color="red", linestyle="--", alpha=0.8, label="超买阈值(70)")
ax2.axhline(y=OVER_SELL, color="green", linestyle="--", alpha=0.8, label="超卖阈值(30)")
# 标注信号对应的RSI位置
ax2.scatter(df_rsi[df_rsi["signal"] == 1].index, df_rsi[df_rsi["signal"] == 1]["rsi_14"], 
           color="red", marker="^", s=120, zorder=5)
ax2.scatter(df_rsi[df_rsi["signal"] == 0].index, df_rsi[df_rsi["signal"] == 0]["rsi_14"], 
           color="green", marker="v", s=120, zorder=5)
ax2.set_xlabel("日期", fontsize=12)
ax2.set_ylabel("RSI(0-100)", fontsize=12)
ax2.legend()
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# 输出RSI策略关键统计
print(f"
RSI策略关键统计:")
print(f"2023年有效交易次数:{df_rsi['signal'].notna().sum()}次")
print(f"平均持仓周期:{len(df_rsi)/df_rsi['signal'].notna().sum():.1f}天(粗略计算)")

可视化后能清晰看到:买入信号均触发在RSI从超卖区间回升时,且伴随成交量放大;卖出信号均触发在RSI从超买区间回落时,同样有成交量配合。列如2023年5月茅台震荡下跌后,RSI跌至30以下,随后回升并伴随成交量放大,触发买入信号,后续股价果然反弹。

关键总结与后续优化方向:1)本次核心是单股RSI超买超卖策略,核心优势是适配震荡行情,和趋势型均线策略互补;2)当前优化空间:RSI周期(14日)和阈值(70/30)是通用设置,可能不是最适合茅台的,且未思考止损;3)下一篇(实验005)咱们针对性优化——用网格搜索找茅台专属的RSI参数,同时加入固定止损逻辑,进一步提升策略稳健性。

互动环节:你跑代码后,RSI策略的收益率比最优均线策略高还是低?在2023年哪次信号触发最精准?评论区留言交流!关注我,100篇Python量化实验干货持续更新,聚焦单股做深做透,帮你打造专属量化交易体系!

© 版权声明

相关文章

1 条评论

  • 头像
    四季如歌旋律 投稿者

    收藏了,感谢分享

    无记录
    回复