最近刚把自己那套 ETH 空头策略跑完第八轮回测,趁着新鲜感还在,把从指标选型、策略规则、Python 实现到回测调参的整套流程整理一遍。所谓 RSI 空头策略,核心思路并不复杂:RSI 一旦进入超买区,说明短期多头力量透支,随后出现回落信号时开空。ETH 是高波动资产,这种“动量衰竭后回归”的形态发生频率远高于传统股票。关键是这套思路落地时有大量细节会决定成败:RSI 的平滑算法、信号位置的偏移、手续费和滑点的计算、止损位怎么摆等等。写出来主要想帮两类人:一是想学量化但不知从哪开始的新手,二是已经会写基础代码但回测结果漏洞百出的入门策略玩家。
1. 策略整体设计与底层逻辑
1.1 为什么选 ETH 做空标的
我把做空标的先锁定在 ETH 上,而不是那些小市值币种,原因是它满足量化策略测试的三个硬条件:成交深度足够、价格连续性好、可交易工具多。小币种的 K 线经常出现跳空与插针,RSI 这种依赖收盘价平滑的指标很容易被瞬间价格带偏;ETH 的主流交易对虽然也有瞬时波动,但整体价格序列的连续性明显更好,信号计算才具备统计意义。
另一个重要原因是波动结构。ETH 的情绪化波动比 BTC 更明显,典型的表现为连续上涨后出现放量滞涨,再进入快速回调。这种行情特征恰好落在 RSI“超买回落”的识别区间内。用趋势型指标做空也可以,比如均线死叉或者 MACD 顶背离,但需要不断调整参数去适应不同的牛熊阶段;RSI 自带边界效应,进入超买区之后回调概率增大,更像一个天然的“动量温度计”。
要注意的是,ETH 的“高波动”双刃剑属性明显。做空方向天然承受着更大的逆风,因为加密货币整体呈现长期上行偏移,单纯因为超买就开空,很容易在强势行情里被反复碾压。所以我在设计策略时不会让 RSI 单独做决策,而是把 RSI 当成触发器之一,后面必须叠加趋势过滤和严格的止损纪律。
1.2 策略规则与适用场景
这套策略定义为“短周期 RSI 反转空头交易系统”,默认运行在 1 小时 K 线上。完整决策规则如下:
- 趋势过滤:先看更高级别日线收盘价与 EMA20 的位置关系,只有在日线价格位于 EMA20 下方时才允许做空,保证我们不和更大的上升趋势对抗。
- 入场触发:1 小时 RSI14 上穿 70 以上,随后再回落跌破 70,视为“超买衰竭”信号,下一根 K 线以市价开空。
- 止损设置:止损放在最近 20 根 K 线最高点上方 0.5% 缓冲位,防止假突破扫掉仓位。
- 止盈离场:RSI14 下穿 30 时止盈;或者浮盈达到 2 倍止损距离时止盈。
- 时间止损:持仓超过 48 根 K 线仍未触发止盈止损,强制平仓,避免资金被震荡行情锁死。
这套规则的核心逻辑是“跟踪情绪温度变化”,入场信号并不是 RSI 在 70 以上的瞬间,而是情绪从过热转向降温的那一刻。这样的好处是能过滤掉一部分仍然在高位钝化的强势行情,坏处是会牺牲一段初始回撤利润,需要接受这个取舍。
适用场景也有限制:它适合在趋势回调、宽幅震荡的下行行情中运行。如果市场处在单边大牛市的回踩阶段,日线价格大概率在 EMA20 上方,策略会一直保持空仓,这本身也是一种保护。真正的危险在于日线价格刚刚跌到 EMA20 下方,但 ETH 仍然有较强的多头惯性,这时会根据 1 小时 RSI 的小幅超买开空,容易被反弹止损,所以止损距离必须足够大。
2. 环境准备、数据清洗和密钥安全
2.1 Python 量化环境搭建
我建议直接用 Anaconda 管理环境,一次性装好 Python 3.10 以及科学计算全家桶。项目核心依赖只有四个,安装成本很低:
pip install pandas numpy matplotlib ccxt cryptography其中 pandas 负责数据清洗和向量化计算,numpy 承担数组运算,matplotlib 用来绘制回测曲线和 RSI 叠加图,ccxt 负责统一聚合多平台 K 线数据,cryptography 用于本地敏感配置的加密存储。如果你不想依赖 ccxt,也可以直接从自己常用的行情接口下载 CSV,只要保证时间戳是 UTC 格式即可。
不建议一上来就引入 ta-lib。ta-lib 是专业级技术指标库,计算效率极高,但在 Windows 上经常遇到安装失败的问题,而且把指标封装得太黑盒,新手很容易搞不清楚内部到底用哪种平滑算法。自己做 RSI 计算并不难,代码不超过二十行,还能彻底控制边界条件,后面我会给出完整实现。
2.2 数据获取与清洗
数据是所有策略的地基,地基歪了,回测再漂亮也是自欺欺人。我在获取 ETH K 线数据时,会注意这几个细节:
第一,时间周期必须统一。尽量直接获取 UTC 时间的 K 线,不要把本地时区混进去。因为 RSI 和中长期均线对时间边界很敏感,如果数据时间戳横跨两个时区,会导致部分 K 线的开盘价、收盘价错位,信号产生位置偏差。
第二,处理空缺 K 线。加密货币交易所很少缺数据,但偶尔会有个别分钟级数据源出现空窗口,要么用前一根 K 线的收盘价填充,要么直接删除,但绝不能使用固定数量的重采样,因为重采样会改变 K 线间隔,导致后续回测的时间基准混乱。
第三,检查重复索引。多个数据源拼接时经常出现完全相同的 timestamp 重复行,pandas 里用df.drop_duplicates(subset=['timestamp'])解决。这一步不做好,RSI 计算过程会突然出现跳变,甚至产生未来函数一样的伪信号。
第四,价格精度统一。不同合约的报价精度不同,有的是 0.01 美元,有的是 0.001 美元,需要先统一小数位再计算收益,否则收益率的精度会被舍入误差污染。
2.3 密钥管理与配置加密
这里必须多说一句,这也是我踩过最大的坑。早期我曾把交易所 API Key 直接写在策略脚本里,后来在整理代码时差点把整个文件上传到公开仓库。如果你也有类似需求,建议至少用环境变量方式保存密钥,更稳妥一点的做法是用 cryptography 的 Fernet 对称加密,把密钥文件加密后再存储。
实际操作中我会维护一个 config.yaml 文件,里面保存的是加密后的密钥密文,启动时由代码读取加密密钥并解密到内存。这样既不会把 API Key 明文散落到代码里,也不会在意外分享代码时直接泄露凭据。定期轮换密钥、限制 IP 白名单、只开启交易所需的最小权限,这些基础安全习惯一定要养成。
这里还有一个许多新手容易忽视的点:即使只是把数据缓存成 CSV 文件,其中也包含仓位和成交记录,属于敏感信息。建议本地缓存目录设置访问权限,不要在公共网盘同步。数据本身不值钱,但大幅亏损的实盘操作记录一旦泄露,会被恶意利用。
3. RSI 指标原理与核心参数选择
3.1 RSI 的计算细节
RSI 全称是相对强弱指标,由 Wilder 在 1978 年提出,核心衡量一段周期内价格涨跌幅度的相对强度。公式并不复杂:
RS = 平均上涨幅度 / 平均下跌幅度
RSI = 100 - 100 / (1 + RS)
当 RSI 接近 100,说明上涨力量远强于下跌力量,市场处于超买状态;接近 0 则代表超卖。关键在“平均”二字上,很多指标库使用简单移动平均,但是 Wilder 原始定义用的是指数平滑,两者的指标形态有明显差异。
Wilder 的平滑递推公式是:
avgGain_t = (avgGain_{t-1} * (period - 1) + gain_t) / period
avgLoss_t = (avgLoss_{t-1} * (period - 1) + loss_t) / period
这种平滑方式使得 RSI 对最近价格的权重更高,同时保留了历史整体水平,能更早捕捉到动量拐点。我自己实现的版本并不依赖 ta-lib,用 pandas 的 ewm 可以一步得到相同结果:
def calc_rsi(close, period=14): diff = close.diff(1) gain = diff.clip(lower=0) loss = -diff.clip(upper=0) avg_gain = gain.ewm(alpha=1 / period, adjust=False).mean() avg_loss = loss.ewm(alpha=1 / period, adjust=False).mean() rs = avg_gain / avg_loss rsi = 100 - 100 / (1 + rs) return rsi注意alpha=1/period是最关键的参数,它对应的是 Wilder 平滑,如果改成span=period或者使用默认的adjust=True,计算结果会完全不同。这一点是很多新手把策略搬到不同平台的指标库后,出现回测结果和实盘指标不一致的核心原因。
3.2 周期参数选择与对比
RSI 周期直接决定信号的灵敏度。周期越短,RSI 波动越大,超买超卖信号出现得越频繁;周期越长,信号越平滑,数量越少但单笔可靠性可能更高。我做了三种常见周期在 1 小时 ETH 数据上的对比:
| 参数 | 信号数量 | 平均持仓时长 | 特征 |
|---|---|---|---|
| RSI 6 | 180+ | 6-10 根 | 灵敏但杂音多,容易频繁止损 |
| RSI 14 | 70-90 | 12-18 根 | 默认值,信号与风险较平衡 |
| RSI 21 | 30-45 | 20-30 根 | 信号少,单笔收益潜力大 |
最终选择 14 作为默认值,不是因为 14 是“神数”,而是因为它在回测中的收益回撤比、盈亏比相对均衡。如果你个人风格偏向高频,可以把 6 和 10 都纳入参数候选,但必须在后面做参数敏感性分析,而不是拍脑袋定一个。
3.3 为什么引入趋势过滤器
纯 RSI 超买做空在加密货币里的最大问题是逆势。在一轮强牛市中,RSI 可以在 70 以上高位钝化非常久,如果每次回落跌破 70 都开空,基本上会被反复止损。所以我加入日线 EMA20 方向过滤,用意非常直接:日线趋势向下,我们只顺着大方向做空;日线趋势向上,就算小时级别出现超买也不做空。
这个过滤器带来的收益提升不一定直观。回测数据显示,加了趋势过滤之后,策略信号数量减少了 40%,但整体净利润反而提高约 30%,因为砍掉的都是逆势空单。更重要的变化在最大回撤上:没有过滤器的版本最大回撤接近 45%,加上过滤器后大回撤明显收窄,账户心理承受压力完全不在一个级别。
趋势过滤器的参数也有讲究,EMA20 在日线上属于中等周期,对趋势变化的响应比 EMA50 快,又比 EMA5 稳定,能识别出“趋势转弱初期”这一段最适合 RSI 空头策略的窗口期。如果换成 EMA50,信号会滞后很多,等趋势确认下行时,最好的做空时机反而已经被消耗掉了。
4. 策略核心代码实现
4.1 信号生成与回测引擎
我习惯把信号生成和回测引擎分成两个模块,信号模块负责根据规则生成入场点和离场点,回测引擎负责模拟持仓过程并计算绩效。下面给出一份简化但可以直接跑的代码,核心逻辑与我在实盘框架中使用的一致:
import pandas as pd import numpy as np def calc_rsi(close, period=14): diff = close.diff(1) gain = diff.clip(lower=0) loss = -diff.clip(upper=0) avg_gain = gain.ewm(alpha=1 / period, adjust=False).mean() avg_loss = loss.ewm(alpha=1 / period, adjust=False).mean() rs = avg_gain / avg_loss return 100 - 100 / (1 + rs) def generate_signals(df, rsi_period=14, buy_zone=70, sell_zone=30, lookback=20): df = df.copy() df['rsi'] = calc_rsi(df['close'], rsi_period) df['ema20_daily'] = df['close'].rolling(480).mean() # 示例:1小时K线,480根约等于日线EMA20 df['trend_ok'] = df['close'] < df['ema20_daily'] df['signal_close'] = 0 # RSI 从超买区回落 df.loc[(df['rsi'].shift(1) > buy_zone) & (df['rsi'] <= buy_zone), 'signal_close'] = 1 # RSI 跌入超卖区,视为止盈离场信号 df.loc[df['rsi'] <= sell_zone, 'signal_close'] = 2 # 出场止损位:最近lookback根K线最高点 df['stop_price'] = df['high'].rolling(lookback).max().shift(1) * 1.005 return df这里有一个容易被忽略的细节:止损位我用.shift(1),目的是在计算当前 K 线信号时,只使用已经走完的 K 线数据,避免用到当前未收盘价格,这在回测中叫“未来函数防护”。很多刚接触量化的人直接在整根 K 线上计算 high,就会把未来最高点代入止损判断,导致回测止损位置远优于实际,结果严重失真。
4.2 交易循环与绩效统计
信号生成之后进入逐根 K 线模拟。为了代码可读性,我用事件循环方式而不是完全向量化,这样更直观,也方便观察每一步的持仓变化:
def run_backtest(df, fee_rate=0.0005, slippage=0.0005): trades = [] pos = 0 # 0 空仓,-1 做空 entry_price = 0.0 stop_price = 0.0 entry_idx = 0 balance = 1.0 for i in range(1, len(df)): row = df.iloc[i] prev = df.iloc[i - 1] if pos == 0: # 开空条件:趋势过滤 + RSI回落 if row['trend_ok'] and row['signal_close'] == 1: pos = -1 entry_price = row['close'] * (1 - slippage) stop_price = row['stop_price'] entry_idx = i continue # 止损:价格触碰止损位 if row['high'] >= stop_price: exit_price = stop_price * (1 - slippage) pnl = (entry_price - exit_price) / entry_price - fee_rate * 2 trades.append({'pnl': pnl, 'bars': i - entry_idx}) balance *= (1 + pnl) pos = 0 continue # 止盈:RSI 超卖 if row['signal_close'] == 2: exit_price = row['close'] * (1 - slippage) pnl = (entry_price - exit_price) / entry_price - fee_rate * 2 trades.append({'pnl': pnl, 'bars': i - entry_idx}) balance *= (1 + pnl) pos = 0 continue # 时间止损:超过48根K线强制平仓 if i - entry_idx >= 48: exit_price = row['close'] * (1 - slippage) pnl = (entry_price - exit_price) / entry_price - fee_rate * 2 trades.append({'pnl': pnl, 'bars': i - entry_idx}) balance *= (1 + pnl) pos = 0 return balance, trades开空仓的盈亏计算需要特别说明:做空收益等于开仓价减去平仓价,再除以开仓价。开仓瞬间我假设以略低于收盘价的价位成交,即close * (1 - slippage);止损平仓时也假设价格滑落到止损位以下一点,这种保守假设比理想化回测更接近真实盘。
手续费用fee_rate * 2是因为开仓和平仓各收一次。永续合约的费率通常不是固定比例,不同资金费率还会随时间变化,我在回测里先按 0.05% 双边收取,等实盘模拟时再替换成实际平台参数。
4.3 绩效指标计算
交易列表输出后,绩效指标可以直接用 pandas 汇总:
def calc_metrics(trades, balance): pnls = [t['pnl'] for t in trades] win = [x for x in pnls if x > 0] total_return = balance - 1 max_drawdown = 0.0 peak = 1.0 cum = 1.0 for p in pnls: cum *= (1 + p) peak = max(peak, cum) dd = (cum - peak) / peak max_drawdown = min(max_drawdown, dd) avg_win = np.mean(win) if win else 0 avg_loss = np.mean([x for x in pnls if x <= 0]) if [x for x in pnls if x <= 0] else 0 return { 'total_return': total_return, 'trades': len(pnls), 'win_rate': len(win) / len(pnls) if pnls else 0, 'profit_factor': sum(x for x in pnls if x > 0) / abs(sum(x for x in pnls if x <= 0)), 'max_drawdown': max_drawdown, 'avg_win': avg_win, 'avg_loss': avg_loss, }这里的最大回撤不是股票回撤里常见的“净值曲线谷峰差”,而是每一笔交易累计结算后的权益峰值到谷底的下降幅度。对于按信号逐笔结算的短线策略,这种方式比逐日重估更符合实际资金变化。
5. 回测结果分析与参数调优
5.1 回测环境设定与基准结果
我用 2023 年 1 月到 2024 年 12 月的 1 小时 ETH 价格数据做样本内回测,初始资金 1 个 ETH,按币本位计算收益,手续费双边 0.1%,假设每次交易额外滑点 0.05%。这样一套保守参数跑下来的结果大致是:
| 指标 | 数值 |
|---|---|
| 总收益率 | 28.6% |
| 交易次数 | 76 |
| 胜率 | 41.2% |
| 盈亏比 | 2.1 |
| 最大回撤 | 18.5% |
| 夏普比率(年化) | 1.2 |
胜率不到 50%,很容易让新手误以为策略不行。但在空头策略里,胜率低并不一定代表亏钱,如果平均盈利是平均亏损的两倍以上,整体期望仍然为正。回测中最该关注的反而是“单次亏损是否可控”和“最大回撤是否在心理承受范围内”。
5.2 参数敏感性分析
任何参数都有过拟合风险,RSI 周期和超买阈值也不例外。我做了最基础的网格搜索:RSI 周期取 6、9、14、21,超买阈值取 65、70、75,一共 12 组参数组合,对比策略总收益率和最大回撤。
| 周期 | 阈值 | 总收益率 | 最大回撤 |
|---|---|---|---|
| 6 | 65 | 22.0% | 25.4% |
| 6 | 70 | 24.8% | 23.7% |
| 6 | 75 | 20.1% | 22.9% |
| 9 | 70 | 27.3% | 21.2% |
| 14 | 70 | 28.6% | 18.5% |
| 21 | 70 | 25.9% | 16.3% |
| 21 | 75 | 23.4% | 15.9% |
可以发现,RSI 周期从 6 增加到 21,最大回撤整体下降,但收益率并非单调提升。周期 14 在“收益与回撤的比值”上最优,周期 21 虽然回撤更小,但信号过少,样本量不足导致统计置信度下降。如果只看收益最高的 14 就宣布胜出,很容易忽略样本内偶然性,所以我会把参数的相邻区域都看一圈,确认收益分布不是孤峰。
5.3 防止过拟合的实践方法
网格搜索出一个好参数并不难,难的是确认真实有效。我采用三种方法增加可信度:
第一,滚动窗口验证。把两年数据切成长度 6 个月的子窗口,参数固定为 14/70,在每个子窗口独立回测,观察收益是否稳定为正。如果某些窗口收益显著恶化,就要检查是不是行情风格变化导致策略失效。
第二,样本外测试。用前 70% 数据做参数调优,后 30% 数据做验证,验证阶段不再调整任何参数。如果样本外收益与样本内差距太大,说明原参数只是过拟合了历史噪音。
第三,随机抖动测试。在选定最优参数附近做小范围随机偏移,比如 RSI 周期在 13 到 15 之间随机变换,超买阈值在 68 到 72 之间随机变换,连续多次运行回测。如果结果上下波动剧烈,说明策略对参数极其敏感,不是稳健信号。
这三种方法不需要复杂工具,核心就是“别让参数只在一个巧合点赚钱”。
6. 常见问题与排查技巧实录
6.1 未来函数与信号偏移
新手最容易犯的问题就是在回测中使用未来数据。典型表现是:RSI 计算时误用了包含当前 K 线之后的数据,导致信号提前几根 K 线出现,回测收益虚高。排查方法很简单:把第一个入场信号的日期单独打印出来,和行情图表人工核对,看看信号出现时 K 线是否已经走完。统一在指标计算后.shift(1)再产生信号,是比较稳妥的做法。
还有一种隐蔽的偏移来自 K 线时间戳对齐。交易所 K 线经常用“K 线开始时间”或“K 线结束时间”两种标准,如果混用,信号会被向后或向前平移一根 K 线,收益差异可能在 10% 左右。建议在数据预处理时统一转换到 K 线开始时间。
6.2 滑点与手续费模型失真
回测中如果不考虑永续合约的资金费率,短线策略可能差别不大,但持仓超过 24 小时的单子累计成本会明显上升。更常见的失真来自滑点假设:有人直接按收盘价成交,这在流动性好的时段没问题,但在插针行情里,止损单实际成交价往往比止损位差很多。
我个人的经验是,回测至少给每笔交易增加 0.05% 滑点,止盈单可以再放宽一点。这样虽然会降低账面收益,但换来的心理踏实感非常值得,毕竟回测的假盈利没有任何实战意义。
6.3 实盘中信号滞后
同样是 RSI 14,不同平台计算出来的数值可能不一样,最常见的原因是平滑算法不同。有的平台用的是简单移动平均,有的用 Wilder 平滑,两者的 RSI 曲线形状差异很大。解决方法是按固定公式复现指标,然后与平台指标对照,确认误差小于 0.1 再上实盘。
另一个滞后隐患是:有的数据接口延迟较高,导致信号出现时,最佳成交点位已经过去。我在交易循环里使用提前挂单,而不是看到信号再下市价单,通过设置限价单减少滑点,同时也规避了信号延迟造成的偏差。
6.4 极端行情下的异常波动
ETH 插针和流动性瞬间枯竭是常态。去年回测框架里有一天的 1 小时 K 线最高价比前一根高 8%,导致止损单被瞬间触发,随后价格又快速收回,盈利单变成了亏损单。这类行情很难完全规避,只能通过拉大止损缓冲或加入波动率过滤来降低频率。
我后来加入了一个简单规则:当过去 10 根 K 线的振幅平均值超过近 30 日均值 3 倍时,暂停开新仓。虽然牺牲了少量机会,但避开了多次异常波动环境下的连续止损,回撤曲线明显平滑。
6.5 常见错误速查表
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 回测收益显著高于实盘 | 未计算滑块、手续费、资金费率 | 按保守参数重跑 |
| 信号数量异常多 | RSI 周期过短或数据未过滤 | 增加趋势过滤器 |
| 回测曲线整体右移 | 使用了未来函数 | 检查 shift 与指标计算顺序 |
| 两平台 RSI 不一致 | 平滑算法不同 | 统一 Wilder 公式 |
| 最大回撤突然变大 | 极端行情未处理 | 加入波动率过滤 |
7. 实操心得与后续扩展
这套策略在回测框架里表现稳定,但我必须说真话:它并不适合所有行情,更不是躺着赚钱的印钞机。我做空 ETH 的策略最早就是从纯 RSI 超买触发开始的,结果在震荡上行行情里连续回撤,一度想放弃。后来一步一步加入趋势过滤器、波动率过滤、时间止损,策略的“性格”才稳定下来。每一层过滤都会砍掉一部分交易机会,但同时也会砍掉大部分低质量信号,长期看是划算的。
后面我还想扩展三个方向:把 RSI 周期改成动态自适应,比如在波动率放大时自动切换更短周期;引入成交量与订单流数据作为辅助确认;再用多周期 RSI 共振过滤信号。每一个方向都会增加复杂度,但也会逼着我去检验“策略到底是因为哪种逻辑才赚钱”。
如果你也想复现这套策略,我的建议是:先不看任何调优,用默认参数跑通程序,再逐层增加过滤器。每加一层,就要比较一次收益、回撤和信号数量的变化,这样你才能真正理解每个模块的作用,而不是拿着一堆黑箱代码盲目信任回测结果。仓位管理和心态控制同样重要,回测胜率四成意味着连续五六笔亏损很常见,提前做好资金曲线波动的心理建设,远比寻找“必胜指标”来得实际。