突破+回踩+RSI,我这套出场逻辑回测胜率超80%,建议收藏
大家好,我是花姐。今天,我们来聊一个被很多交易者忽视但却极其重要的话题:交易的出场策略。
在做交易系统设计的时候,大多数人习惯把精力都放在进场点上。可实际上——出场策略,才是真正决定盈亏分水岭的关键所在。
特别是短线或者T+0策略,如果出得不巧,利润很容易就被“回吐”回去了,甚至还可能被拉成亏损。那怎么办?别慌,这篇我们来聊一个我自己实测效果不错的组合出场策略:“突破+回踩+RSI”。
RSI指标的基础知识
相对强弱指数(RSI)是由J. Welles Wilder Jr.开发的动量指标,用于衡量价格变动的速度和幅度。RSI的取值范围在0到100之间,通常情况下,RSI超过70被视为超买,低于30被视为超卖。这些极端值常被用作潜在的反转点,指导交易者的进出场决策。
RSI的计算公式为:
RSI = 100 – [100 / (1 + RS)]
其中,RS表示一定周期内平均涨幅与平均跌幅的比率。
一、策略思路
我这个组合策略,其实就是三个经典逻辑打出一套“组合拳”:突破确认 + 回踩确认 + RSI强弱过滤,说白了就是:不追高、不抄底、只吃趋势里最确定那段利润。
说实话,这套逻辑一开始我是从趋势跟踪的思路里摸出来的,但是问题来了:趋势来了你怎么知道“这是真的”?趋势快结束了你怎么知道“我该撤了”?所以我才想着,要加点“强度验证机制”。
1、突破确认:我们不抄底,我们等它突破新高
说实话,我自己做盘也试过很多“抢反弹”之类的策略——但太难受了,经常是刚买进去,它就继续跌给你看,真的血压飙升💢。
于是我就转向只做“强者恒强”,只吃顺势的。具体怎么判断趋势开始了呢?我们用了一个很简单但实用的信号:
当前K线的收盘价,要高于过去20根K线的最高收盘价。
这等于是说,这货已经突破了一段时间以来的压力位,是不是有点意思了?但光靠这点可不行,冲一下就掉头的情况太多,所以我们需要下一招👇
2、回踩确认:它回头看你一眼,你再上车
很多人做“突破追涨”策略死得快,原因其实就在这里。突破是真的,但你追得太急了,它往往会回踩一下才继续上攻。就像谈恋爱,别人刚给你个眼神你就表白,太猛了🥹。
所以我们做了一个“等它回头看你”的动作:确认它突破之后至少回踩一次5日均线,只有真正回踩成功我们才上车。
这个逻辑其实蛮像缠论那种“第二买点”思维,回踩是对多头的一次检验。
所以流程变成:
- 先突破
- 后回踩
- 回踩成功,再确认多头延续——我们才上!
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_signal、sell_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')
七、结语
通过本篇文章,我们了解了如何利用RSI指标优化交易的出场策略,并通过Backtrader进行了实战回测。希望大家在实际交易中,能够结合自身的交易风格,灵活运用RSI,提高交易的胜率和收益