RSI EA 趋势交叉策略
RSI EA 策略复现了 MetaTrader 5 中的 “RSI EA” 智能交易系统。策略在所选 K 线序列上监控相对强弱指数(RSI),当动量穿越可调的超买或超卖阈值时采取操作。转换版本保留了原始系统的止损、止盈、移动止损以及自动资金管理思想,并采用 StockSharp 的高层策略 API 实现。
策略逻辑
指标
- RSI:周期可调,基于选定的蜡烛类型计算。
入场条件
- 做多:RSI 从下方向上穿越
RsiBuyLevel(上一根 RSI 低于阈值,本根高于阈值),且允许做多。 - 做空:RSI 从上方向下穿越
RsiSellLevel(上一根 RSI 高于阈值,本根低于阈值),且允许做空。
策略仅保持单一净头寸,在已有头寸时不会再开立对冲方向的仓位。
离场条件
- 信号离场:当
CloseBySignal开启时,RSI 反向穿越立即平掉当前持仓。 - 保护性止损:
StopLoss大于零时,策略监控入场均价与当前价格的距离,当亏损达到设定值时平仓。 - 止盈:
TakeProfit大于零时,在达到目标距离后平仓。 - 移动止损:
TrailingStop大于零时,止损会跟随价格移动。做多时,当价格至少向有利方向移动TrailingStop距离后,止损上移至收盘价 - TrailingStop;做空时采用对称规则。
仓位大小
UseAutoVolume = true时,根据账户权益与风险计算下单量:Volume = Equity * RiskPercent / (100 * stopDistance),其中stopDistance优先使用StopLoss,若未设置则使用TrailingStop。若缺少任何保护距离,则退回使用手工仓位。UseAutoVolume = false时,所有订单均使用固定的ManualVolume数量。
参数
CandleType:用于计算指标的蜡烛类型(默认 1 分钟)。RsiPeriod:RSI 计算窗口长度(默认 14)。RsiBuyLevel:触发做多与平空的超卖阈值(默认 30)。RsiSellLevel:触发做空与平多的超买阈值(默认 70)。EnableLong:是否允许做多(默认 true)。EnableShort:是否允许做空(默认 true)。CloseBySignal:是否在 RSI 反向穿越时平仓(默认 true)。StopLoss:以价格单位表示的止损距离(默认 0,关闭)。TakeProfit:以价格单位表示的止盈距离(默认 0,关闭)。TrailingStop:以价格单位表示的移动止损距离(默认 0,关闭)。UseAutoVolume:是否启用基于风险的仓位控制(默认 true)。RiskPercent:自动仓位时使用的权益风险百分比(默认 10)。ManualVolume:关闭自动仓位时的固定下单量(默认 0.1)。
实现细节
- 使用
SubscribeCandles(...).Bind(...)高层接口,让 RSI 指标直接将数值传入策略,无需手工处理指标缓冲区。 - 当持仓归零时会清除所有缓存的止损与止盈水平,避免旧值残留。
- 移动止损逻辑遵循原始 MQL 代码:只有当价格相对当前止损前进超过两倍的跟踪距离时才会上调或下调止损,以避免过早收紧。
- StockSharp 运行于净头寸模式,因此无法像原始 EA 那样同时持有多空仓位。策略会等待当前仓位平掉后再开反向单。
- 自动仓位计算需要
StopLoss或TrailingStop中至少一个有效;若无法确定风险距离,则使用手工仓位。
默认配置
- 时间框架:1 分钟蜡烛。
- RSI:周期 14,阈值 30/70。
- 资金管理:启用自动仓位,风险 10% 权益,备用手工数量 0.1。
- 风险控制:默认未启用止损、止盈或移动止损(实盘前请自行配置)。
使用建议
- 根据交易品种与周期设置合适的
CandleType,策略可在 StockSharp 支持的任何时间框架运行。 - 在启用自动仓位前,请先设定合理的
StopLoss或TrailingStop,保证风险计算有意义。 - 代码中已调用
StartProtection(),建议保持启用,以减少连接中断或孤立头寸的风险。 - 在不同市场上应用时,应持续跟踪成交表现,并根据波动性调整 RSI 阈值。
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;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Relative Strength Index crossover strategy translated from the MetaTrader RSI EA.
/// </summary>
public class RsiCrossoverEaStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _rsiBuyLevel;
private readonly StrategyParam<decimal> _rsiSellLevel;
private readonly StrategyParam<bool> _enableLong;
private readonly StrategyParam<bool> _enableShort;
private readonly StrategyParam<bool> _closeBySignal;
private readonly StrategyParam<decimal> _stopLoss;
private readonly StrategyParam<decimal> _takeProfit;
private readonly StrategyParam<decimal> _trailingStop;
private readonly StrategyParam<bool> _useAutoVolume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<decimal> _manualVolume;
private RelativeStrengthIndex _rsi;
private decimal? _previousRsi;
private decimal? _longStop;
private decimal? _shortStop;
private decimal? _longTakeProfit;
private decimal? _shortTakeProfit;
/// <summary>
/// Candle type used to calculate RSI.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Period of the RSI indicator.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// Oversold level that triggers long entries.
/// </summary>
public decimal RsiBuyLevel
{
get => _rsiBuyLevel.Value;
set => _rsiBuyLevel.Value = value;
}
/// <summary>
/// Overbought level that triggers short entries.
/// </summary>
public decimal RsiSellLevel
{
get => _rsiSellLevel.Value;
set => _rsiSellLevel.Value = value;
}
/// <summary>
/// Enable or disable long trades.
/// </summary>
public bool EnableLong
{
get => _enableLong.Value;
set => _enableLong.Value = value;
}
/// <summary>
/// Enable or disable short trades.
/// </summary>
public bool EnableShort
{
get => _enableShort.Value;
set => _enableShort.Value = value;
}
/// <summary>
/// Close positions when the RSI crosses the opposite level.
/// </summary>
public bool CloseBySignal
{
get => _closeBySignal.Value;
set => _closeBySignal.Value = value;
}
/// <summary>
/// Stop-loss distance in price units.
/// </summary>
public decimal StopLoss
{
get => _stopLoss.Value;
set => _stopLoss.Value = value;
}
/// <summary>
/// Take-profit distance in price units.
/// </summary>
public decimal TakeProfit
{
get => _takeProfit.Value;
set => _takeProfit.Value = value;
}
/// <summary>
/// Trailing stop distance in price units.
/// </summary>
public decimal TrailingStop
{
get => _trailingStop.Value;
set => _trailingStop.Value = value;
}
/// <summary>
/// Automatically size orders by account risk.
/// </summary>
public bool UseAutoVolume
{
get => _useAutoVolume.Value;
set => _useAutoVolume.Value = value;
}
/// <summary>
/// Risk percentage applied when auto volume is enabled.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Fixed order volume used when auto sizing is disabled.
/// </summary>
public decimal ManualVolume
{
get => _manualVolume.Value;
set => _manualVolume.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="RsiCrossoverEaStrategy"/> class.
/// </summary>
public RsiCrossoverEaStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Type of candles used for RSI", "General");
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "Number of bars used for RSI", "RSI")
.SetOptimize(8, 28, 2);
_rsiBuyLevel = Param(nameof(RsiBuyLevel), 30m)
.SetRange(0m, 100m)
.SetDisplay("RSI Buy Level", "Cross above this level opens longs", "RSI")
.SetOptimize(20m, 40m, 5m);
_rsiSellLevel = Param(nameof(RsiSellLevel), 70m)
.SetRange(0m, 100m)
.SetDisplay("RSI Sell Level", "Cross below this level opens shorts", "RSI")
.SetOptimize(60m, 80m, 5m);
_enableLong = Param(nameof(EnableLong), true)
.SetDisplay("Enable Long", "Allow bullish trades", "Trading");
_enableShort = Param(nameof(EnableShort), true)
.SetDisplay("Enable Short", "Allow bearish trades", "Trading");
_closeBySignal = Param(nameof(CloseBySignal), true)
.SetDisplay("Close By Signal", "Exit when RSI flips", "Trading");
_stopLoss = Param(nameof(StopLoss), 0m)
.SetRange(0m, 1000m)
.SetDisplay("Stop Loss", "Distance from entry for stop loss", "Risk")
.SetOptimize(0m, 200m, 20m);
_takeProfit = Param(nameof(TakeProfit), 0m)
.SetRange(0m, 1000m)
.SetDisplay("Take Profit", "Distance from entry for take profit", "Risk")
.SetOptimize(0m, 200m, 20m);
_trailingStop = Param(nameof(TrailingStop), 0m)
.SetRange(0m, 1000m)
.SetDisplay("Trailing Stop", "Trailing distance after price moves", "Risk")
.SetOptimize(0m, 200m, 20m);
_useAutoVolume = Param(nameof(UseAutoVolume), true)
.SetDisplay("Auto Volume", "Size positions by risk percent", "Money Management");
_riskPercent = Param(nameof(RiskPercent), 10m)
.SetRange(0m, 100m)
.SetDisplay("Risk Percent", "Percentage of equity risked per trade", "Money Management");
_manualVolume = Param(nameof(ManualVolume), 0.1m)
.SetRange(0.01m, 100m)
.SetDisplay("Manual Volume", "Fixed volume when auto sizing is off", "Money Management");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_previousRsi = null;
_longStop = null;
_shortStop = null;
_longTakeProfit = null;
_shortTakeProfit = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_rsi = new RelativeStrengthIndex
{
Length = RsiPeriod
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_rsi, Process)
.Start();
StartProtection(null, null);
}
private void Process(ICandleMessage candle, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return; // Wait for completed candles only.
if (!_rsi.IsFormed)
{
_previousRsi = rsiValue;
return; // Indicator still gathering enough data.
}
var previous = _previousRsi;
_previousRsi = rsiValue;
if (ManageOpenPosition(candle))
return; // Exit orders were submitted, wait for fills before new decisions.
var crossAboveBuy = previous.HasValue && previous.Value < RsiBuyLevel && rsiValue > RsiBuyLevel;
var crossBelowSell = previous.HasValue && previous.Value > RsiSellLevel && rsiValue < RsiSellLevel;
if (CloseBySignal)
{
if (Position > 0 && crossBelowSell)
{
SellMarket();
ResetProtection();
return; // Close long trades when RSI drops below the sell level.
}
if (Position < 0 && crossAboveBuy)
{
BuyMarket();
ResetProtection();
return; // Close short trades when RSI rises above the buy level.
}
}
if (Position != 0)
return; // Do not add hedged positions in the netted environment.
if (EnableShort && crossBelowSell)
{
var volume = CalculateVolume();
if (volume > 0m)
SellMarket();
return;
}
if (EnableLong && crossAboveBuy)
{
var volume = CalculateVolume();
if (volume > 0m)
BuyMarket();
}
}
/// <inheritdoc />
protected override void OnPositionReceived(Position position)
{
base.OnPositionReceived(position);
if (Position == 0)
ResetProtection();
}
private bool ManageOpenPosition(ICandleMessage candle)
{
if (Position > 0)
{
var entryPrice = candle.ClosePrice;
if (_longStop is null && StopLoss > 0m)
_longStop = entryPrice - StopLoss; // Initial protective stop below entry.
if (_longTakeProfit is null && TakeProfit > 0m)
_longTakeProfit = entryPrice + TakeProfit; // Profit target above entry.
if (TrailingStop > 0m && candle.ClosePrice > entryPrice)
{
var candidate = candle.ClosePrice - TrailingStop;
if (!_longStop.HasValue || candle.ClosePrice - 2m * TrailingStop > _longStop.Value)
_longStop = candidate; // Trail only when price advances enough.
}
if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
{
SellMarket();
ResetProtection();
return true;
}
if (_longTakeProfit.HasValue && candle.HighPrice >= _longTakeProfit.Value)
{
SellMarket();
ResetProtection();
return true;
}
}
else if (Position < 0)
{
var entryPrice = candle.ClosePrice;
if (_shortStop is null && StopLoss > 0m)
_shortStop = entryPrice + StopLoss; // Protective stop above entry.
if (_shortTakeProfit is null && TakeProfit > 0m)
_shortTakeProfit = entryPrice - TakeProfit; // Profit target below entry.
if (TrailingStop > 0m && candle.ClosePrice < entryPrice)
{
var candidate = candle.ClosePrice + TrailingStop;
if (!_shortStop.HasValue || candle.ClosePrice + 2m * TrailingStop < _shortStop.Value)
_shortStop = candidate; // Trail short stops only after favorable move.
}
if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
{
BuyMarket();
ResetProtection();
return true;
}
if (_shortTakeProfit.HasValue && candle.LowPrice <= _shortTakeProfit.Value)
{
BuyMarket();
ResetProtection();
return true;
}
}
else
{
ResetProtection(); // Ensure cached levels are cleared when flat.
}
return false;
}
private decimal CalculateVolume()
{
if (!UseAutoVolume)
return ManualVolume; // Use fixed size when auto sizing is disabled.
var equity = Portfolio?.CurrentValue ?? 0m;
if (equity <= 0m)
return ManualVolume; // Fallback if equity information is unavailable.
var stopDistance = StopLoss > 0m ? StopLoss : TrailingStop;
if (stopDistance <= 0m)
return ManualVolume; // Cannot compute risk-based size without a stop.
var riskAmount = equity * RiskPercent / 100m;
if (riskAmount <= 0m)
return ManualVolume;
var volume = riskAmount / stopDistance;
return volume > 0m ? volume : ManualVolume;
}
private void ResetProtection()
{
_longStop = null;
_shortStop = null;
_longTakeProfit = null;
_shortTakeProfit = null;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
class rsi_crossover_ea_strategy(Strategy):
def __init__(self):
super(rsi_crossover_ea_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._rsi_period = self.Param("RsiPeriod", 14)
self._rsi_buy_level = self.Param("RsiBuyLevel", 30.0)
self._rsi_sell_level = self.Param("RsiSellLevel", 70.0)
self._enable_long = self.Param("EnableLong", True)
self._enable_short = self.Param("EnableShort", True)
self._close_by_signal = self.Param("CloseBySignal", True)
self._stop_loss = self.Param("StopLoss", 0.0)
self._take_profit = self.Param("TakeProfit", 0.0)
self._trailing_stop = self.Param("TrailingStop", 0.0)
self._previous_rsi = None
self._long_stop = None
self._short_stop = None
self._long_take_profit = None
self._short_take_profit = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def RsiPeriod(self):
return self._rsi_period.Value
@RsiPeriod.setter
def RsiPeriod(self, value):
self._rsi_period.Value = value
@property
def RsiBuyLevel(self):
return self._rsi_buy_level.Value
@RsiBuyLevel.setter
def RsiBuyLevel(self, value):
self._rsi_buy_level.Value = value
@property
def RsiSellLevel(self):
return self._rsi_sell_level.Value
@RsiSellLevel.setter
def RsiSellLevel(self, value):
self._rsi_sell_level.Value = value
@property
def EnableLong(self):
return self._enable_long.Value
@EnableLong.setter
def EnableLong(self, value):
self._enable_long.Value = value
@property
def EnableShort(self):
return self._enable_short.Value
@EnableShort.setter
def EnableShort(self, value):
self._enable_short.Value = value
@property
def CloseBySignal(self):
return self._close_by_signal.Value
@CloseBySignal.setter
def CloseBySignal(self, value):
self._close_by_signal.Value = value
@property
def StopLoss(self):
return self._stop_loss.Value
@StopLoss.setter
def StopLoss(self, value):
self._stop_loss.Value = value
@property
def TakeProfit(self):
return self._take_profit.Value
@TakeProfit.setter
def TakeProfit(self, value):
self._take_profit.Value = value
@property
def TrailingStop(self):
return self._trailing_stop.Value
@TrailingStop.setter
def TrailingStop(self, value):
self._trailing_stop.Value = value
def OnStarted2(self, time):
super(rsi_crossover_ea_strategy, self).OnStarted2(time)
self._previous_rsi = None
self._long_stop = None
self._short_stop = None
self._long_take_profit = None
self._short_take_profit = None
rsi = RelativeStrengthIndex()
rsi.Length = self.RsiPeriod
self._rsi = rsi
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(rsi, self.ProcessCandle).Start()
def ProcessCandle(self, candle, rsi_value):
if candle.State != CandleStates.Finished:
return
rsi_val = float(rsi_value)
if not self._rsi.IsFormed:
self._previous_rsi = rsi_val
return
previous = self._previous_rsi
self._previous_rsi = rsi_val
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self._manage_open_position(candle):
return
buy_level = float(self.RsiBuyLevel)
sell_level = float(self.RsiSellLevel)
cross_above_buy = previous is not None and previous < buy_level and rsi_val > buy_level
cross_below_sell = previous is not None and previous > sell_level and rsi_val < sell_level
if self.CloseBySignal:
if self.Position > 0 and cross_below_sell:
self.SellMarket()
self._reset_protection()
return
if self.Position < 0 and cross_above_buy:
self.BuyMarket()
self._reset_protection()
return
if self.Position != 0:
return
if self.EnableShort and cross_below_sell:
self.SellMarket()
return
if self.EnableLong and cross_above_buy:
self.BuyMarket()
def _manage_open_position(self, candle):
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
sl = float(self.StopLoss)
tp = float(self.TakeProfit)
trail = float(self.TrailingStop)
if self.Position > 0:
entry_price = close
if self._long_stop is None and sl > 0.0:
self._long_stop = entry_price - sl
if self._long_take_profit is None and tp > 0.0:
self._long_take_profit = entry_price + tp
if trail > 0.0 and close > entry_price:
candidate = close - trail
if self._long_stop is None or close - 2.0 * trail > self._long_stop:
self._long_stop = candidate
if self._long_stop is not None and low <= self._long_stop:
self.SellMarket()
self._reset_protection()
return True
if self._long_take_profit is not None and high >= self._long_take_profit:
self.SellMarket()
self._reset_protection()
return True
elif self.Position < 0:
entry_price = close
if self._short_stop is None and sl > 0.0:
self._short_stop = entry_price + sl
if self._short_take_profit is None and tp > 0.0:
self._short_take_profit = entry_price - tp
if trail > 0.0 and close < entry_price:
candidate = close + trail
if self._short_stop is None or close + 2.0 * trail < self._short_stop:
self._short_stop = candidate
if self._short_stop is not None and high >= self._short_stop:
self.BuyMarket()
self._reset_protection()
return True
if self._short_take_profit is not None and low <= self._short_take_profit:
self.BuyMarket()
self._reset_protection()
return True
else:
self._reset_protection()
return False
def _reset_protection(self):
self._long_stop = None
self._short_stop = None
self._long_take_profit = None
self._short_take_profit = None
def OnReseted(self):
super(rsi_crossover_ea_strategy, self).OnReseted()
self._previous_rsi = None
self._long_stop = None
self._short_stop = None
self._long_take_profit = None
self._short_take_profit = None
def CreateClone(self):
return rsi_crossover_ea_strategy()