突破+回踩+RSI,我这套出场逻辑回测胜率超80%,建议收藏

1,136 阅读9分钟

突破+回踩+RSI,我这套出场逻辑回测胜率超80%,建议收藏

大家好,我是花姐。今天,我们来聊一个被很多交易者忽视但却极其重要的话题:交易的出场策略。

在做交易系统设计的时候,大多数人习惯把精力都放在进场点上。可实际上——出场策略,才是真正决定盈亏分水岭的关键所在。

特别是短线或者T+0策略,如果出得不巧,利润很容易就被“回吐”回去了,甚至还可能被拉成亏损。那怎么办?别慌,这篇我们来聊一个我自己实测效果不错的组合出场策略:“突破+回踩+RSI”

image.png

RSI指标的基础知识

相对强弱指数(RSI)是由J. Welles Wilder Jr.开发的动量指标,用于衡量价格变动的速度和幅度。RSI的取值范围在0到100之间,通常情况下,RSI超过70被视为超买,低于30被视为超卖。这些极端值常被用作潜在的反转点,指导交易者的进出场决策。

RSI的计算公式为:
RSI = 100 – [100 / (1 + RS)]
其中,RS表示一定周期内平均涨幅与平均跌幅的比率。


一、策略思路

我这个组合策略,其实就是三个经典逻辑打出一套“组合拳”:突破确认 + 回踩确认 + RSI强弱过滤,说白了就是:不追高、不抄底、只吃趋势里最确定那段利润

说实话,这套逻辑一开始我是从趋势跟踪的思路里摸出来的,但是问题来了:趋势来了你怎么知道“这是真的”?趋势快结束了你怎么知道“我该撤了”?所以我才想着,要加点“强度验证机制”。

image.png

1、突破确认:我们不抄底,我们等它突破新高

说实话,我自己做盘也试过很多“抢反弹”之类的策略——但太难受了,经常是刚买进去,它就继续跌给你看,真的血压飙升💢。

于是我就转向只做“强者恒强”,只吃顺势的。具体怎么判断趋势开始了呢?我们用了一个很简单但实用的信号:

当前K线的收盘价,要高于过去20根K线的最高收盘价

这等于是说,这货已经突破了一段时间以来的压力位,是不是有点意思了?但光靠这点可不行,冲一下就掉头的情况太多,所以我们需要下一招👇

2、回踩确认:它回头看你一眼,你再上车

很多人做“突破追涨”策略死得快,原因其实就在这里。突破是真的,但你追得太急了,它往往会回踩一下才继续上攻。就像谈恋爱,别人刚给你个眼神你就表白,太猛了🥹。

所以我们做了一个“等它回头看你”的动作:确认它突破之后至少回踩一次5日均线,只有真正回踩成功我们才上车。

这个逻辑其实蛮像缠论那种“第二买点”思维,回踩是对多头的一次检验。

所以流程变成:

  1. 先突破
  2. 后回踩
  3. 回踩成功,再确认多头延续——我们才上!

3、RSI强度确认:它有力气,才值得你陪跑

趋势起来了,回踩也确认了,最后一步是啥?就是“它是不是还有力气冲”。

很多人做趋势跟踪会忽略动能,结果行情在高位横着、没量没力,你还死守着。我的方法就是:

RSI要大于60,说明买盘动能还在,行情还有上冲空间。

这就相当于一个“动能过滤器”,有点像MACD的柱子还在拉升的感觉。但RSI更灵敏,适合我们做5分钟级别那种比较频繁的交易。

而且我还发现一个点:

  • RSI大于60的时候出场,胜率、收益都明显比传统70要好。
  • 这相当于你不是等行情“彻底衰竭”再走,而是在动能刚开始减弱的时候就先撤了,懂得止盈。

4、出场逻辑:RSI跌破 + 均线跌破,双保险落袋为安

我本来试过几种出场方法,什么“固定止盈止损”“均线反转”这些都不太灵。最后我选了这个双保险法:

  • RSI从高位下穿60(动能削弱)
  • 同时价格跌破EMA5(短期趋势破坏)

只有这两个都满足,我们才出场。为什么?就是为了防止被假震荡吓出去。单看一个信号太容易来回打脸,而组合确认可以过滤掉大部分“假动作”。

你应该能看出来,我这个策略它不是靠“猜底部”,而是靠“等你确认了、回头看我一眼、我再决定上不上”。

说白了就是——我不贪心,我只吃那段你确定有肉的行情。

而且最妙的地方在于:这套逻辑非常适合 T+0 高频ETF品种,比如 513020 这种流动性好、波动大的标的。我们用 5 分钟级别来回测,正好打磨这种趋势确认的交易节奏。


下面内容,就以“513020 港股科技ETF 的 5 分钟数据”为例,手把手演示从数据拉取到全量回测、优化,再到结果分析。

二、数据获取与预处理

1. 安装依赖

pip install akshare backtrader pandas numpy matplotlib

2. 拉分钟线数据并缓存

import akshare as ak
import pandas as pd
import os

symbol = "513020"
cache_file = f"{symbol}_5min.csv"

# 时间,开盘,收盘,最高,最低,涨跌幅,涨跌额,成交量,成交额,振幅,换手率
if os.path.exists(cache_file):
    df = pd.read_csv(cache_file, parse_dates=['时间'])
else:
    df = ak.fund_etf_hist_min_em(symbol=symbol,start_date="2025-05-01 09:30:00",end_date="2025-05-22 15:30:00", period="5", adjust="qfq")
    df.to_csv(cache_file,index=False)

print(df.head())

三、Backtrader 策略框架

1. 基础策略模板

先构建一个继承 bt.Strategy 的基础类,准备用来混入各种逻辑。

import backtrader as bt

class BaseStrategy(bt.Strategy):
    params = dict(
        atr_period=14,
        atr_mult=1.5,
        rsi_period=14,
        rsi_overbought=70,
        rsi_oversold=30,
        macd_fast=12,
        macd_slow=26,
        macd_signal=9,
    )

    def __init__(self):
        # Common indicators
        self.atr = bt.indicators.ATR(self.data, period=self.p.atr_period)
        self.rsi = bt.indicators.RSI(self.data.close, period=self.p.rsi_period)
        self.macd = bt.indicators.MACD(self.data.close,
                                       period_me1=self.p.macd_fast,
                                       period_me2=self.p.macd_slow,
                                       period_signal=self.p.macd_signal)
        self.highest20 = bt.indicators.Highest(self.data.high, period=20)
        self.lowest20 = bt.indicators.Lowest(self.data.low, period=20)
        self.order = None

自嘲一下:我当初把 ATR 和 RSI 都写成全局,用时才发现有些策略并不依赖 ATR,少算一堆指标也好 😂。

2. 进场+出场逻辑混入

next() 中,先写个通用判断框架。

    def next(self):
        if self.order:
            return
        
        # 进场逻辑
        if not self.position:
            if self.buy_signal():
                size = int(self.broker.getcash() / self.data.close[0] * 0.2)  # 20%仓位
                self.order = self.buy(size=size)
        else:
            if self.sell_signal():
                self.order = self.sell()

然后分别实现 buy_signalsell_signal

    def buy_signal(self):
        # 突破 + 回踩
        broke = self.data.close[0] > self.highest20[-1]
        dip_back = self.data.close[-1] < self.highest20[-1] and self.data.close[0] >= self.highest20[-1]
        return broke and dip_back

    def sell_signal(self):
        # 1. RSI 拐头
        rsi_turn = self.rsi[0] < self.rsi[-1] and self.rsi[-1] > self.p.rsi_overbought
        # 2. MACD 死叉
        macd_cross = self.macd.macd[0] < self.macd.signal[0] and self.macd.macd[-1] > self.macd.signal[-1]
        # 3. ATR 止损
        stop_price = self.position.price - self.p.atr_mult * self.atr[0]
        atr_stop = self.data.close[0] < stop_price

        return rsi_turn or macd_cross or atr_stop

细节坑

  • self.position.price 返回的是开仓均价;多单止损要减;空单要加。
  • ATR 停利如果放在 sell_signal,注意回测初期 ATR 可能为 nan,要加判断。

四、回测执行与分析

1. Cerebro 配置

cerebro = bt.Cerebro(stdstats=False)
cerebro.broker.setcash(100000)
cerebro.broker.setcommission(commission=0.0003)
cerebro.addsizer(bt.sizers.PercentSizer, percents=20)
cerebro.addstrategy(BaseStrategy)

# 添加分析器
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')

data_feed = bt.feeds.PandasData(dataname=df)
cerebro.adddata(data_feed)

result = cerebro.run()[0]

2. 输出指标

# 夏普比率
print("Sharpe Ratio:", result.analyzers.sharpe.get_analysis()['sharperatio'])
# 最大回撤
dd = result.analyzers.drawdown.get_analysis()
print(f"Max Drawdown: {dd['max']['drawdown']}%, Duration: {dd['max']['len']} bars")
# 交易统计
ta = result.analyzers.trades.get_analysis()
print(f"Total Trades: {ta.total.closed}, Win Rate: {ta.won.total/ta.total.closed:.2%}")
print(f"Average PnL: {ta.pnl.net.total/ta.total.closed:.4f}")

上手提醒

  • 夏普比率要看你的周期,5 分钟策略一般用无风险利率 0 来计算。
  • 回撤 len 单位是 bar 数,5 分钟图的话,len=50 就是大约 4 个小时。

五、参数优化(Optimization)

为了找到最优参数,我们用 Backtrader 的参数优化功能。

cerebro = bt.Cerebro(optreturn=False, stdstats=False)
cerebro.optstrategy(
    BaseStrategy,
    rsi_period=range(10, 21, 2),
    atr_mult=[1.0, 1.5, 2.0],
    macd_fast=[8, 12],
    macd_slow=[20, 26]
)
cerebro.broker.setcash(100000)
cerebro.addsizer(bt.sizers.PercentSizer, percents=20)
cerebro.broker.setcommission(0.0003)
cerebro.adddata(data_feed)
# 只加一个简单的净值分析
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')

opt_results = cerebro.run()
best = None
best_sharpe = -float('inf')
for res in opt_results:
    sharpe = res.analyzers.sharpe.get_analysis().get('sharperatio', 0)
    if sharpe and sharpe > best_sharpe:
        best_sharpe = sharpe
        best = res

print("Best Sharpe:", best_sharpe)
print("Params:", best.params)

踩坑细节

  • 优化时不要一口气跑太多 combination,否则内存、CPU 会炸。可以分批跑。
  • optreturn=False 确保每个结果都可以拿到 analyzer。

六、一个可以执行的完整代码

import akshare as ak
import pandas as pd
import backtrader as bt
from datetime import datetime
import os

# 获取数据函数
def fetch_data(symbol='513020', start_date='2025-05-01 09:30:00', end_date='2025-05-22 15:30:00'):
    cache_file = f"{symbol}_5min.csv"
    # 获取数据(使用缓存机制)
    if os.path.exists(cache_file):
        df = pd.read_csv(cache_file, parse_dates=['时间'])
    else:
        df = ak.fund_etf_hist_min_em(
            symbol=symbol,
            start_date=start_date,
            end_date=end_date,
            period="5",
            adjust="qfq"
        )
        df.to_csv(cache_file, index=False)

    # 格式化为 backtrader 格式
    df = df.rename(columns={
        '时间': 'datetime',
        '开盘': 'open',
        '收盘': 'close',
        '最高': 'high',
        '最低': 'low',
        '成交量': 'volume'
    })
    df['datetime'] = pd.to_datetime(df['datetime'])
    df.set_index('datetime', inplace=True)
    df['openinterest'] = 0  # Backtrader 必须字段
    df = df[['open', 'high', 'low', 'close', 'volume', 'openinterest']]
    return df
    

# 转换为backtrader可识别格式
class PandasData5Min(bt.feeds.PandasData):
    lines = ('datetime',)
    params = (
        ('datetime', None),
        ('open', -1),
        ('high', -1),
        ('low', -1),
        ('close', -1),
        ('volume', -1),
        ('openinterest', -1),
    )

# 策略主体
class BreakoutRSIStrategy(bt.Strategy):
    params = (
        ('ma_period', 20),
        ('rsi_period', 14),
        ('rsi_exit_level', 60),  # RSI高于此值则卖出
        ('atr_period', 14),
        ('atr_multiplier', 1.2),
    )

    def __init__(self):
        self.ma = bt.indicators.SimpleMovingAverage(self.data.close, period=self.p.ma_period)
        self.rsi = bt.indicators.RSI(self.data.close, period=self.p.rsi_period)
        self.atr = bt.indicators.ATR(self.data, period=self.p.atr_period)
        self.order = None
        self.buy_price = 0

    def log(self, txt):
        dt = self.datas[0].datetime.datetime(0)
        print(f'{dt.isoformat()}, {txt}')

    def next(self):
        if self.order:
            return

        # 持仓逻辑
        if not self.position:
            # 策略买入逻辑:
            # 当前价格向上突破均线,且价格大于前一根K线高点,且波动幅度不大(ATR约束)
            if (self.data.close[0] > self.ma[0] and
                self.data.close[-1] < self.ma[-1] and
                self.data.close[0] > self.data.high[-1] and
                self.atr[0] < self.p.atr_multiplier * self.atr[-1]):
                self.order = self.buy()
                self.buy_price = self.data.close[0]
                self.log(f'BUY at {self.buy_price:.2f}')

        else:
            # 出场逻辑:RSI高于设定值时止盈
            if self.rsi[0] > self.p.rsi_exit_level:
                self.order = self.sell()
                self.log(f'SELL at {self.data.close[0]:.2f}, RSI: {self.rsi[0]:.2f}')

# 主函数
if __name__ == '__main__':
    df = fetch_data()
    df_bt = df.copy()
    df_bt['openinterest'] = 0

    cerebro = bt.Cerebro()
    data = PandasData5Min(dataname=df_bt)
    cerebro.adddata(data)

    cerebro.addstrategy(BreakoutRSIStrategy)
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')

    print('Starting Backtest...')
    results = cerebro.run()
    strat = results[0]

    # 输出分析指标
    print('\n===== 回测指标 =====')
    print(f"夏普比率: {strat.analyzers.sharpe.get_analysis()}")
    print(f"交易统计: {strat.analyzers.trades.get_analysis()}")
    print(f"最大回撤: {strat.analyzers.drawdown.get_analysis()['max']['drawdown']:.2f}%")

    # 可视化结果
    cerebro.plot(style='candlestick')

image.png


七、结语

通过本篇文章,我们了解了如何利用RSI指标优化交易的出场策略,并通过Backtrader进行了实战回测。希望大家在实际交易中,能够结合自身的交易风格,灵活运用RSI,提高交易的胜率和收益