RSI Eraser 策略
RSI Eraser 策略直接移植自 Vladimir Karputov 编写的 MetaTrader 5 专家顾问。 策略使用小时级别的 K 线来计算相对强弱指标 (RSI),并在 RSI 穿越 50 的过程中寻找均值回归式的入场点。 在进入市场之前会检查上一交易日的高低点区间,同时按照账户权益的一定百分比计算下单手数。
策略要点
- 主级别:1 小时 K 线负责生成指标数值和交易信号。
- 过滤级别:通过日线数据获取昨日的最高价和最低价。
- 指标:经典 RSI,周期可配置。
- 方向判定:RSI 高于中轴则做多,RSI 低于中轴则做空。
- 仓位管理:根据入场价到止损价的距离和风险百分比计算下单数量。
入场逻辑
- 等待小时 K 线收盘后计算 RSI。
- 确认已经有至少一根完整的日线数据。
- 多头条件:
- RSI 必须严格大于中轴值(默认 50)。
- 计划止损价(入场价 − 止损距离)不能低于昨日低点减去缓冲点数。
- 如果当天已经开过一笔多头,则拒绝新的多头信号。
- 空头条件:
- RSI 必须严格小于中轴值。
- 计划止损价(入场价 + 止损距离)不能高于昨日高点加上缓冲点数。
- 如果当天已经开过一笔空头,则拒绝新的空头信号。
- 当条件满足时发送市价单并按风险计算手数。 若当前持有反向仓位,会在同一次操作中平掉旧仓并反向开仓。
离场逻辑
- 初始止损与止盈由设置的点数和利润倍数计算得到。
- 策略在每根收盘 K 线上检查:
- 多头:价格触及止损或止盈时平仓。
- 空头:价格触及止损或止盈时平仓。
- 保本机制:一旦浮动利润达到初始止损距离,止损价会移动到入场价,实现无风险持仓。
- 当没有持仓时会清理所有内部的止损/止盈记录,防止沿用旧数据。
风险控制
RiskPercent参数指定每笔交易愿意承担的账户权益比例。- 手数计算公式为
risk_amount / stop_distance,若无法获取权益信息,则退回到基础Volume参数。 - 日线缓冲区为昨日区间提供额外安全边界,避免止损过于靠近最近的波动极值。
默认参数
RsiPeriod= 14RsiNeutralLevel= 50StopLossPips= 50TakeProfitMultiplier= 3DailyBufferPips= 10RiskPercent= 5%CandleType= 1 小时DailyCandleType= 1 天
实现细节
- 策略通过 StockSharp 的高级 API 同时订阅小时与日线 K 线。
- 所有代码注释与日志信息均为英文,以符合仓库规范。
- 保本移动和“每天每个方向仅一笔交易”的限制完全复现了原始 MT5 版本的行为。
using System;
using System.Linq;
using System.Collections.Generic;
using Ecng.Common;
using Ecng.Collections;
using Ecng.Serialization;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
using StockSharp.Algo;
using StockSharp.Algo.Candles;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// RSI based strategy that mirrors the logic of the original "RSI Eraser" Expert Advisor.
/// It trades on hourly candles, checks the previous daily range, and uses fixed risk sizing.
/// </summary>
public class RsiEraserStrategy : Strategy
{
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _rsiNeutralLevel;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<decimal> _takeProfitMultiplier;
private readonly StrategyParam<decimal> _dailyBufferPips;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<DataType> _dailyCandleType;
private RelativeStrengthIndex _rsi;
private decimal? _previousDailyLow;
private decimal? _previousDailyHigh;
private DateTime? _lastBuyDate;
private DateTime? _lastSellDate;
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal _stopDistance;
private bool _isBreakEvenActivated;
private decimal _pipSize;
private decimal _entryPrice;
/// <summary>
/// RSI averaging period.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// Neutral level for RSI comparisons.
/// </summary>
public decimal RsiNeutralLevel
{
get => _rsiNeutralLevel.Value;
set => _rsiNeutralLevel.Value = value;
}
/// <summary>
/// Stop-loss size in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Risk per trade in percent of equity.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Take-profit multiplier relative to stop size.
/// </summary>
public decimal TakeProfitMultiplier
{
get => _takeProfitMultiplier.Value;
set => _takeProfitMultiplier.Value = value;
}
/// <summary>
/// Additional buffer applied to yesterday's high/low in pips.
/// </summary>
public decimal DailyBufferPips
{
get => _dailyBufferPips.Value;
set => _dailyBufferPips.Value = value;
}
/// <summary>
/// Working candle type (hourly by default).
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Daily candle type used to capture previous highs and lows.
/// </summary>
public DataType DailyCandleType
{
get => _dailyCandleType.Value;
set => _dailyCandleType.Value = value;
}
/// <summary>
/// Initialize <see cref="RsiEraserStrategy"/>.
/// </summary>
public RsiEraserStrategy()
{
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "Number of periods used for RSI calculation", "Indicators");
_rsiNeutralLevel = Param(nameof(RsiNeutralLevel), 50m)
.SetRange(0m, 100m)
.SetDisplay("RSI Neutral", "Neutral level used to detect direction", "Indicators");
_stopLossPips = Param(nameof(StopLossPips), 500m)
.SetRange(1m, 5000m)
.SetDisplay("Stop Loss (pips)", "Stop-loss distance expressed in pips", "Risk Management");
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetRange(0.1m, 50m)
.SetDisplay("Risk %", "Risk percentage applied to equity for sizing", "Risk Management");
_takeProfitMultiplier = Param(nameof(TakeProfitMultiplier), 3m)
.SetGreaterThanZero()
.SetDisplay("TP Multiplier", "Take-profit multiple of stop distance", "Risk Management");
_dailyBufferPips = Param(nameof(DailyBufferPips), 10m)
.SetRange(0m, 100m)
.SetDisplay("Daily Buffer (pips)", "Extra pips added to yesterday's range", "Filters");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe for signals", "General");
_dailyCandleType = Param(nameof(DailyCandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Daily Candle", "Timeframe used to read yesterday's range", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
if (Security == null)
yield break;
yield return (Security, CandleType);
if (DailyCandleType != CandleType)
yield return (Security, DailyCandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_rsi = null;
_previousDailyLow = null;
_previousDailyHigh = null;
_lastBuyDate = null;
_lastSellDate = null;
_stopPrice = null;
_takePrice = null;
_stopDistance = 0m;
_isBreakEvenActivated = false;
_pipSize = 0m;
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pipSize = CalculatePipSize();
_rsi = new RelativeStrengthIndex
{
Length = RsiPeriod
};
var candleSubscription = SubscribeCandles(CandleType);
candleSubscription
.Bind(_rsi, ProcessMainCandle)
.Start();
var dailySubscription = SubscribeCandles(DailyCandleType);
dailySubscription
.Bind(ProcessDailyCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, candleSubscription);
DrawIndicator(area, _rsi);
DrawOwnTrades(area);
}
}
private void ProcessDailyCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
// Store the most recent completed daily range for entry validation.
_previousDailyHigh = candle.HighPrice;
_previousDailyLow = candle.LowPrice;
}
private void ProcessMainCandle(ICandleMessage candle, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_rsi.IsFormed)
return;
if (_rsi == null || !_rsi.IsFormed)
return;
// Manage any open position before looking for new entries.
if (HandleOpenPosition(candle))
return;
var signal = GetSignal(rsiValue);
if (signal == 0)
return;
if (signal > 0)
TryEnterLong(candle, rsiValue);
else
TryEnterShort(candle, rsiValue);
}
private bool HandleOpenPosition(ICandleMessage candle)
{
if (Position > 0)
{
InitializeRiskLevelsIfNeeded(isLong: true, candle.ClosePrice);
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket();
LogInfo($"Exit long via stop at {_stopPrice:0.#####}.");
ResetRiskLevels();
return true;
}
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
SellMarket();
LogInfo($"Exit long via take-profit at {_takePrice:0.#####}.");
ResetRiskLevels();
return true;
}
if (!_isBreakEvenActivated && _stopDistance > 0m)
{
var entryPrice = _entryPrice;
if (candle.ClosePrice - entryPrice >= _stopDistance)
{
_stopPrice = entryPrice;
_isBreakEvenActivated = true;
LogInfo($"Moved long stop to break-even at {entryPrice:0.#####}.");
}
}
}
else if (Position < 0)
{
InitializeRiskLevelsIfNeeded(isLong: false, candle.ClosePrice);
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket();
LogInfo($"Exit short via stop at {_stopPrice:0.#####}.");
ResetRiskLevels();
return true;
}
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
BuyMarket();
LogInfo($"Exit short via take-profit at {_takePrice:0.#####}.");
ResetRiskLevels();
return true;
}
if (!_isBreakEvenActivated && _stopDistance > 0m)
{
var entryPrice = _entryPrice;
if (entryPrice - candle.ClosePrice >= _stopDistance)
{
_stopPrice = entryPrice;
_isBreakEvenActivated = true;
LogInfo($"Moved short stop to break-even at {entryPrice:0.#####}.");
}
}
}
else if (_stopPrice.HasValue || _takePrice.HasValue)
{
// No open position: clear any residual risk levels.
ResetRiskLevels();
}
return false;
}
private void InitializeRiskLevelsIfNeeded(bool isLong, decimal referencePrice)
{
if (_stopPrice.HasValue && _takePrice.HasValue)
return;
var entryPrice = _entryPrice;
if (entryPrice == 0m)
entryPrice = referencePrice;
var stopDistance = GetStopDistance();
if (stopDistance <= 0m)
return;
_stopDistance = stopDistance;
if (isLong)
{
_stopPrice = entryPrice - stopDistance;
_takePrice = entryPrice + stopDistance * TakeProfitMultiplier;
}
else
{
_stopPrice = entryPrice + stopDistance;
_takePrice = entryPrice - stopDistance * TakeProfitMultiplier;
}
_isBreakEvenActivated = false;
}
private void TryEnterLong(ICandleMessage candle, decimal rsiValue)
{
if (_previousDailyLow == null)
return;
if (Position > 0)
return;
var today = candle.OpenTime.Date;
if (_lastBuyDate.HasValue && _lastBuyDate.Value >= today)
return;
var stopDistance = GetStopDistance();
if (stopDistance <= 0m)
return;
var entryPrice = candle.ClosePrice;
var stopPrice = entryPrice - stopDistance;
var buffer = DailyBufferPips * _pipSize;
var adjustedLow = _previousDailyLow.Value - buffer;
if (adjustedLow < stopPrice)
return;
var volume = CalculatePositionSize(entryPrice, stopPrice);
if (volume <= 0m)
return;
ResetRiskLevels();
var tradeVolume = volume + Math.Abs(Position);
BuyMarket();
_entryPrice = entryPrice;
_lastBuyDate = today;
_stopPrice = stopPrice;
_takePrice = entryPrice + stopDistance * TakeProfitMultiplier;
_stopDistance = stopDistance;
_isBreakEvenActivated = false;
LogInfo($"Buy signal: RSI {rsiValue:F2} > {RsiNeutralLevel:F2}, entry {entryPrice:0.#####}, stop {_stopPrice:0.#####}, take {_takePrice:0.#####}. Volume {tradeVolume:0.#####}.");
}
private void TryEnterShort(ICandleMessage candle, decimal rsiValue)
{
if (_previousDailyHigh == null)
return;
if (Position < 0)
return;
var today = candle.OpenTime.Date;
if (_lastSellDate.HasValue && _lastSellDate.Value >= today)
return;
var stopDistance = GetStopDistance();
if (stopDistance <= 0m)
return;
var entryPrice = candle.ClosePrice;
var stopPrice = entryPrice + stopDistance;
var buffer = DailyBufferPips * _pipSize;
var adjustedHigh = _previousDailyHigh.Value + buffer;
if (adjustedHigh > stopPrice)
return;
var volume = CalculatePositionSize(entryPrice, stopPrice);
if (volume <= 0m)
return;
ResetRiskLevels();
var tradeVolume = volume + Math.Abs(Position);
SellMarket();
_entryPrice = entryPrice;
_lastSellDate = today;
_stopPrice = stopPrice;
_takePrice = entryPrice - stopDistance * TakeProfitMultiplier;
_stopDistance = stopDistance;
_isBreakEvenActivated = false;
LogInfo($"Sell signal: RSI {rsiValue:F2} < {RsiNeutralLevel:F2}, entry {entryPrice:0.#####}, stop {_stopPrice:0.#####}, take {_takePrice:0.#####}. Volume {tradeVolume:0.#####}.");
}
private int GetSignal(decimal rsiValue)
{
if (rsiValue == 0m)
return 0;
return rsiValue > RsiNeutralLevel ? 1 : -1;
}
private decimal CalculatePositionSize(decimal entryPrice, decimal stopPrice)
{
var stopDistance = Math.Abs(entryPrice - stopPrice);
if (stopDistance <= 0m)
return Volume;
var equity = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (equity <= 0m)
return Volume;
var riskAmount = equity * (RiskPercent / 100m);
if (riskAmount <= 0m)
return Volume;
var volume = riskAmount / stopDistance;
return volume > 0m ? volume : Volume;
}
private decimal GetStopDistance()
{
if (_pipSize <= 0m)
_pipSize = CalculatePipSize();
return StopLossPips * _pipSize;
}
private decimal CalculatePipSize()
{
var step = Security?.PriceStep ?? 1m;
var digits = GetDecimalDigits(step);
return digits is 3 or 5
? step * 10m
: step;
}
private static int GetDecimalDigits(decimal value)
{
var digits = 0;
var normalized = value;
while (normalized != Math.Truncate(normalized) && digits < 10)
{
normalized *= 10m;
digits++;
}
return digits;
}
private void ResetRiskLevels()
{
_stopPrice = null;
_takePrice = null;
_stopDistance = 0m;
_isBreakEvenActivated = false;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
from datatype_extensions import *
from indicator_extensions import *
class rsi_eraser_strategy(Strategy):
def __init__(self):
super(rsi_eraser_strategy, self).__init__()
self._rsi_period = self.Param("RsiPeriod", 14).SetGreaterThanZero().SetDisplay("RSI Period", "RSI lookback", "Indicators")
self._rsi_neutral = self.Param("RsiNeutralLevel", 50.0).SetDisplay("RSI Neutral", "Neutral level", "Indicators")
self._sl_pips = self.Param("StopLossPips", 500.0).SetDisplay("Stop Loss (pips)", "SL distance", "Risk")
self._tp_multiplier = self.Param("TakeProfitMultiplier", 3.0).SetGreaterThanZero().SetDisplay("TP Multiplier", "TP as multiple of SL", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))).SetDisplay("Candle Type", "Primary timeframe", "General")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(rsi_eraser_strategy, self).OnReseted()
self._stop_price = None
self._take_price = None
self._entry_price = 0
self._stop_distance = 0
self._break_even = False
def OnStarted2(self, time):
super(rsi_eraser_strategy, self).OnStarted2(time)
self._stop_price = None
self._take_price = None
self._entry_price = 0
self._stop_distance = 0
self._break_even = False
self._pip_size = 1.0
if self.Security is not None and self.Security.PriceStep is not None and self.Security.PriceStep > 0:
self._pip_size = float(self.Security.PriceStep)
rsi = RelativeStrengthIndex()
rsi.Length = self._rsi_period.Value
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(rsi, self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawIndicator(area, rsi)
self.DrawOwnTrades(area)
def OnProcess(self, candle, rsi_val):
if candle.State != CandleStates.Finished:
return
try:
rsi = float(rsi_val.Value) if hasattr(rsi_val, 'Value') else float(rsi_val)
except:
rsi = float(rsi_val)
close = float(candle.ClosePrice)
neutral = float(self._rsi_neutral.Value)
# Manage existing position
if self.Position > 0:
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket()
self._reset()
return
if self._take_price is not None and float(candle.HighPrice) >= self._take_price:
self.SellMarket()
self._reset()
return
if not self._break_even and self._stop_distance > 0:
if close - self._entry_price >= self._stop_distance:
self._stop_price = self._entry_price
self._break_even = True
elif self.Position < 0:
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket()
self._reset()
return
if self._take_price is not None and float(candle.LowPrice) <= self._take_price:
self.BuyMarket()
self._reset()
return
if not self._break_even and self._stop_distance > 0:
if self._entry_price - close >= self._stop_distance:
self._stop_price = self._entry_price
self._break_even = True
else:
if self._stop_price is not None or self._take_price is not None:
self._reset()
# Entry signals
if self.Position == 0:
sd = float(self._sl_pips.Value) * self._pip_size
if sd <= 0:
return
if rsi > neutral:
self.BuyMarket()
self._entry_price = close
self._stop_price = close - sd
self._take_price = close + sd * float(self._tp_multiplier.Value)
self._stop_distance = sd
self._break_even = False
elif rsi < neutral:
self.SellMarket()
self._entry_price = close
self._stop_price = close + sd
self._take_price = close - sd * float(self._tp_multiplier.Value)
self._stop_distance = sd
self._break_even = False
def _reset(self):
self._stop_price = None
self._take_price = None
self._entry_price = 0
self._stop_distance = 0
self._break_even = False
def CreateClone(self):
return rsi_eraser_strategy()