4218 RSI MA 策略
概览
该策略是 MQL/9925 中 MetaTrader 专家的 C# 移植版本。它通过 (High + Low + 2 * Close) / 4 的加权价格计算 EMA 斜率,并将其与传统 RSI 相乘,从而重建 RSI_MA 动量指标。仅使用收盘完成的 K 线参与计算,以保持与原始 EA 一致的信号节奏。
默认配置针对 EURUSD 日线 (D1),任意时刻只允许存在一笔仓位。如果需要,也可以通过调整蜡烛类型与阈值,将策略应用到其他品种与周期。
交易逻辑
- 指标计算
- 使用可配置周期的 RSI 处理收盘价。
- 使用相同周期的 EMA 处理加权价格。
- 指标值为
RSI * (EMA(当前) - EMA(前一根)) / 点值,并限制在[1, 99]区间。
- 做多条件
- 前一根指标值低于超卖极值(默认 5)。
- 最新指标值高于多头激活阈值(默认 20)。
- 当前没有多头仓位;若存在空头仓位会先行平仓再入场做多。
- 做空条件
- 前一根指标值高于超买极值(默认 95)。
- 最新指标值低于空头激活阈值(默认 80)。
- 当前没有空头仓位;若存在多头仓位会先行平仓再入场做空。
- 指标出场
- 多头:指标从超买极值回落到激活阈值以下(95 → 80)。
- 空头:指标从超卖极值回升到激活阈值以上(5 → 20)。
- 防护机制
- 止损、止盈与追踪止损以点数表示,并按照标的的
PriceStep(缺省为 0.0001)换算成价格。 - 追踪止损仅在浮盈超过设置距离后才会向有利方向推进,永不放宽。
- 止损、止盈与追踪止损以点数表示,并按照标的的
参数说明
| 参数 | 说明 |
|---|---|
RsiPeriod |
RSI 与 EMA 的周期。 |
OversoldActivationLevel |
超卖极值出现后触发多头的激活阈值。 |
OversoldExtremeLevel |
允许做多前必须达到的超卖极值。 |
OverboughtActivationLevel |
超买极值出现后触发空头的激活阈值。 |
OverboughtExtremeLevel |
允许做空前必须达到的超买极值。 |
StopLossPips |
止损距离(点),可通过 UseStopLoss 启用/禁用。 |
TakeProfitPips |
止盈距离(点),可通过 UseTakeProfit 启用/禁用。 |
TrailingStopPips |
追踪止损距离(点),可通过 UseTrailingStop 启用/禁用。 |
UseStopLoss |
是否启用止损管理。 |
UseTakeProfit |
是否启用止盈管理。 |
UseTrailingStop |
是否启用追踪止损。 |
UseMoneyManagement |
是否使用按风险百分比计算仓位。 |
RiskPercent |
启用资金管理时的单笔风险百分比。 |
TradeVolume |
未启用资金管理时的固定下单量。 |
CandleType |
策略处理的蜡烛类型(默认日线)。 |
使用建议
- 若需完全复制原 EA 的行为,请使用 EURUSD 日线数据;更换品种或周期时请同步调整
CandleType与阈值。 - 策略始终保持单一仓位,开仓前会自动平掉反向单。
- 当无法获取组合价值或计算出的仓位小于等于 0 时,会退回到固定
TradeVolume。 - 请确保标的的
PriceStep与实际点值一致(大多数外汇品种为 0.0001),否则需要重新评估止损/止盈距离。
风险控制
- 每根完成的 K 线都会根据最高价/最低价判断是否触发止损或止盈。
- 追踪止损只在盈利状态下收紧,永不反向放宽。
- 即使关闭防护参数,指标条件仍会执行平仓,保持与 MQL 版本相同的退场逻辑。
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.Candles;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// RSI and EMA based strategy converted from the original MQL implementation.
/// Combines a custom RSI*EMA momentum oscillator with basic risk management.
/// </summary>
public class RsiMaStrategy : Strategy
{
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<decimal> _oversoldActivationLevel;
private readonly StrategyParam<decimal> _oversoldExtremeLevel;
private readonly StrategyParam<decimal> _overboughtActivationLevel;
private readonly StrategyParam<decimal> _overboughtExtremeLevel;
private readonly StrategyParam<decimal> _stopLossPips;
private readonly StrategyParam<decimal> _takeProfitPips;
private readonly StrategyParam<decimal> _trailingStopPips;
private readonly StrategyParam<bool> _useStopLoss;
private readonly StrategyParam<bool> _useTakeProfit;
private readonly StrategyParam<bool> _useTrailingStop;
private readonly StrategyParam<bool> _useMoneyManagement;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<DataType> _candleType;
private RelativeStrengthIndex _rsi;
private ExponentialMovingAverage _ema;
private decimal? _previousIndicatorValue;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
private decimal _entryPrice;
public RsiMaStrategy()
{
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetDisplay("RSI Period", string.Empty, "Oscillator")
;
_oversoldActivationLevel = Param(nameof(OversoldActivationLevel), 40m)
.SetDisplay("Oversold Activation", string.Empty, "Oscillator")
;
_oversoldExtremeLevel = Param(nameof(OversoldExtremeLevel), 30m)
.SetDisplay("Oversold Extreme", string.Empty, "Oscillator");
_overboughtActivationLevel = Param(nameof(OverboughtActivationLevel), 60m)
.SetDisplay("Overbought Activation", string.Empty, "Oscillator")
;
_overboughtExtremeLevel = Param(nameof(OverboughtExtremeLevel), 70m)
.SetDisplay("Overbought Extreme", string.Empty, "Oscillator");
_stopLossPips = Param(nameof(StopLossPips), 399m)
.SetDisplay("Stop Loss (pips)", string.Empty, "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 999m)
.SetDisplay("Take Profit (pips)", string.Empty, "Risk");
_trailingStopPips = Param(nameof(TrailingStopPips), 299m)
.SetDisplay("Trailing Stop (pips)", string.Empty, "Risk");
_useStopLoss = Param(nameof(UseStopLoss), true)
.SetDisplay("Use Stop Loss", string.Empty, "Risk");
_useTakeProfit = Param(nameof(UseTakeProfit), true)
.SetDisplay("Use Take Profit", string.Empty, "Risk");
_useTrailingStop = Param(nameof(UseTrailingStop), true)
.SetDisplay("Use Trailing Stop", string.Empty, "Risk");
_useMoneyManagement = Param(nameof(UseMoneyManagement), false)
.SetDisplay("Use Risk Percent Position Sizing", string.Empty, "Position");
_riskPercent = Param(nameof(RiskPercent), 10m)
.SetDisplay("Risk Percent", string.Empty, "Position");
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetDisplay("Fixed Volume", string.Empty, "Position");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle TimeFrame", string.Empty, "General");
}
/// <summary>
/// RSI calculation period.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// Activation threshold after an oversold extreme.
/// </summary>
public decimal OversoldActivationLevel
{
get => _oversoldActivationLevel.Value;
set => _oversoldActivationLevel.Value = value;
}
/// <summary>
/// Oversold extreme required before a long setup becomes valid.
/// </summary>
public decimal OversoldExtremeLevel
{
get => _oversoldExtremeLevel.Value;
set => _oversoldExtremeLevel.Value = value;
}
/// <summary>
/// Activation threshold after an overbought extreme.
/// </summary>
public decimal OverboughtActivationLevel
{
get => _overboughtActivationLevel.Value;
set => _overboughtActivationLevel.Value = value;
}
/// <summary>
/// Overbought extreme required before a short setup becomes valid.
/// </summary>
public decimal OverboughtExtremeLevel
{
get => _overboughtExtremeLevel.Value;
set => _overboughtExtremeLevel.Value = value;
}
/// <summary>
/// Stop-loss distance measured in pips.
/// </summary>
public decimal StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take-profit distance measured in pips.
/// </summary>
public decimal TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing-stop distance measured in pips.
/// </summary>
public decimal TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Enable or disable stop-loss management.
/// </summary>
public bool UseStopLoss
{
get => _useStopLoss.Value;
set => _useStopLoss.Value = value;
}
/// <summary>
/// Enable or disable take-profit management.
/// </summary>
public bool UseTakeProfit
{
get => _useTakeProfit.Value;
set => _useTakeProfit.Value = value;
}
/// <summary>
/// Enable or disable trailing stop adjustments.
/// </summary>
public bool UseTrailingStop
{
get => _useTrailingStop.Value;
set => _useTrailingStop.Value = value;
}
/// <summary>
/// Enable or disable percent based position sizing.
/// </summary>
public bool UseMoneyManagement
{
get => _useMoneyManagement.Value;
set => _useMoneyManagement.Value = value;
}
/// <summary>
/// Portfolio risk percentage when money management is enabled.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Fixed volume used when money management is disabled.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Candle type used for signal generation.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, CandleType);
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_rsi = null;
_ema = null;
_previousIndicatorValue = null;
_stopLossPrice = null;
_takeProfitPrice = null;
_entryPrice = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_rsi = new RelativeStrengthIndex
{
Length = RsiPeriod
};
_ema = new ExponentialMovingAverage
{
Length = RsiPeriod
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_rsi, ProcessCandle)
.Start();
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle, decimal rsiValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_rsi.IsFormed)
return;
var indicatorValue = rsiValue;
if (_previousIndicatorValue is decimal previousValue)
{
ManageOpenPosition(candle, previousValue, indicatorValue);
EvaluateEntries(candle, previousValue, indicatorValue);
}
_previousIndicatorValue = indicatorValue;
}
private void ManageOpenPosition(ICandleMessage candle, decimal previousValue, decimal currentValue)
{
if (Position > 0)
{
var exitSignal = previousValue > OverboughtExtremeLevel && currentValue < OverboughtActivationLevel;
if (exitSignal)
{
SellMarket();
ResetRiskLevels();
return;
}
UpdateTrailingStopForLong(candle);
if (ShouldCloseLong(candle))
{
SellMarket();
ResetRiskLevels();
}
}
else if (Position < 0)
{
var exitSignal = previousValue < OversoldExtremeLevel && currentValue > OversoldActivationLevel;
if (exitSignal)
{
BuyMarket();
ResetRiskLevels();
return;
}
UpdateTrailingStopForShort(candle);
if (ShouldCloseShort(candle))
{
BuyMarket();
ResetRiskLevels();
}
}
}
private void EvaluateEntries(ICandleMessage candle, decimal previousValue, decimal currentValue)
{
var price = candle.ClosePrice;
if (price <= 0m)
return;
if (previousValue < OversoldExtremeLevel && currentValue > OversoldActivationLevel && Position <= 0)
{
_entryPrice = price;
InitializeRiskLevelsForLong(price);
BuyMarket();
}
else if (previousValue > OverboughtExtremeLevel && currentValue < OverboughtActivationLevel && Position >= 0)
{
_entryPrice = price;
InitializeRiskLevelsForShort(price);
SellMarket();
}
}
private void InitializeRiskLevelsForLong(decimal price)
{
var pipDistance = GetPipSize();
if (UseStopLoss && StopLossPips > 0m)
_stopLossPrice = price - pipDistance * StopLossPips;
else
_stopLossPrice = null;
if (UseTakeProfit && TakeProfitPips > 0m)
_takeProfitPrice = price + pipDistance * TakeProfitPips;
else
_takeProfitPrice = null;
}
private void InitializeRiskLevelsForShort(decimal price)
{
var pipDistance = GetPipSize();
if (UseStopLoss && StopLossPips > 0m)
_stopLossPrice = price + pipDistance * StopLossPips;
else
_stopLossPrice = null;
if (UseTakeProfit && TakeProfitPips > 0m)
_takeProfitPrice = price - pipDistance * TakeProfitPips;
else
_takeProfitPrice = null;
}
private void UpdateTrailingStopForLong(ICandleMessage candle)
{
if (!UseTrailingStop || TrailingStopPips <= 0m)
return;
var pipDistance = GetPipSize() * TrailingStopPips;
if (pipDistance <= 0m)
return;
var profit = candle.ClosePrice - _entryPrice;
if (profit <= pipDistance)
return;
var newStop = candle.ClosePrice - pipDistance;
if (_stopLossPrice is null || newStop > _stopLossPrice)
_stopLossPrice = newStop;
}
private void UpdateTrailingStopForShort(ICandleMessage candle)
{
if (!UseTrailingStop || TrailingStopPips <= 0m)
return;
var pipDistance = GetPipSize() * TrailingStopPips;
if (pipDistance <= 0m)
return;
var profit = _entryPrice - candle.ClosePrice;
if (profit <= pipDistance)
return;
var newStop = candle.ClosePrice + pipDistance;
if (_stopLossPrice is null || newStop < _stopLossPrice)
_stopLossPrice = newStop;
}
private bool ShouldCloseLong(ICandleMessage candle)
{
var stopHit = UseStopLoss && _stopLossPrice is decimal stop && candle.LowPrice <= stop;
var takeProfitHit = UseTakeProfit && _takeProfitPrice is decimal takeProfit && candle.HighPrice >= takeProfit;
return stopHit || takeProfitHit;
}
private bool ShouldCloseShort(ICandleMessage candle)
{
var stopHit = UseStopLoss && _stopLossPrice is decimal stop && candle.HighPrice >= stop;
var takeProfitHit = UseTakeProfit && _takeProfitPrice is decimal takeProfit && candle.LowPrice <= takeProfit;
return stopHit || takeProfitHit;
}
private void ResetRiskLevels()
{
_stopLossPrice = null;
_takeProfitPrice = null;
_entryPrice = 0m;
}
private decimal GetPipSize()
{
var priceStep = Security?.PriceStep;
if (priceStep is null || priceStep == 0m)
return 0.0001m;
return priceStep.Value;
}
private decimal GetOrderVolume(decimal price)
{
var volume = TradeVolume;
if (!UseMoneyManagement || Portfolio is null || price <= 0m)
return volume;
var portfolioValue = Portfolio.CurrentValue ?? 0m;
if (portfolioValue <= 0m)
return volume;
var riskAmount = portfolioValue * RiskPercent / 100m;
if (riskAmount <= 0m)
return volume;
var estimatedVolume = riskAmount / price;
var volumeStep = Security?.VolumeStep ?? 0m;
if (volumeStep > 0m)
{
estimatedVolume = Math.Floor(estimatedVolume / volumeStep) * volumeStep;
}
if (estimatedVolume <= 0m)
estimatedVolume = volume;
return estimatedVolume;
}
}
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
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import RelativeStrengthIndex
class rsi_ma_strategy(Strategy):
def __init__(self):
super(rsi_ma_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2))) \
.SetDisplay("Candle TimeFrame", "", "General")
self._rsi_period = self.Param("RsiPeriod", 14) \
.SetDisplay("RSI Period", "", "Oscillator")
self._oversold_activation = self.Param("OversoldActivationLevel", 40.0) \
.SetDisplay("Oversold Activation", "", "Oscillator")
self._oversold_extreme = self.Param("OversoldExtremeLevel", 30.0) \
.SetDisplay("Oversold Extreme", "", "Oscillator")
self._overbought_activation = self.Param("OverboughtActivationLevel", 60.0) \
.SetDisplay("Overbought Activation", "", "Oscillator")
self._overbought_extreme = self.Param("OverboughtExtremeLevel", 70.0) \
.SetDisplay("Overbought Extreme", "", "Oscillator")
self._stop_loss_pips = self.Param("StopLossPips", 399.0) \
.SetDisplay("Stop Loss (pips)", "", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 999.0) \
.SetDisplay("Take Profit (pips)", "", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 299.0) \
.SetDisplay("Trailing Stop (pips)", "", "Risk")
self._use_stop_loss = self.Param("UseStopLoss", True) \
.SetDisplay("Use Stop Loss", "", "Risk")
self._use_take_profit = self.Param("UseTakeProfit", True) \
.SetDisplay("Use Take Profit", "", "Risk")
self._use_trailing_stop = self.Param("UseTrailingStop", True) \
.SetDisplay("Use Trailing Stop", "", "Risk")
self._prev_rsi = None
self._entry_price = 0.0
self._stop_loss_price = None
self._take_profit_price = None
@property
def CandleType(self):
return self._candle_type.Value
@property
def RsiPeriod(self):
return self._rsi_period.Value
@property
def OversoldActivationLevel(self):
return float(self._oversold_activation.Value)
@property
def OversoldExtremeLevel(self):
return float(self._oversold_extreme.Value)
@property
def OverboughtActivationLevel(self):
return float(self._overbought_activation.Value)
@property
def OverboughtExtremeLevel(self):
return float(self._overbought_extreme.Value)
@property
def StopLossPips(self):
return float(self._stop_loss_pips.Value)
@property
def TakeProfitPips(self):
return float(self._take_profit_pips.Value)
@property
def TrailingStopPips(self):
return float(self._trailing_stop_pips.Value)
@property
def UseStopLoss(self):
return self._use_stop_loss.Value
@property
def UseTakeProfit(self):
return self._use_take_profit.Value
@property
def UseTrailingStop(self):
return self._use_trailing_stop.Value
def OnStarted2(self, time):
super(rsi_ma_strategy, self).OnStarted2(time)
self._prev_rsi = None
self._entry_price = 0.0
self._stop_loss_price = None
self._take_profit_price = None
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.RsiPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._rsi, self.ProcessCandle).Start()
def _get_pip_size(self):
sec = self.Security
if sec is not None:
ps = sec.PriceStep
if ps is not None and float(ps) > 0:
return float(ps)
return 0.0001
def ProcessCandle(self, candle, rsi_val):
if candle.State != CandleStates.Finished:
return
rv = float(rsi_val)
if self._prev_rsi is None:
self._prev_rsi = rv
return
close = float(candle.ClosePrice)
low = float(candle.LowPrice)
high = float(candle.HighPrice)
prev = self._prev_rsi
# manage open position
if self.Position > 0:
exit_signal = prev > self.OverboughtExtremeLevel and rv < self.OverboughtActivationLevel
if exit_signal:
self.SellMarket()
self._reset_risk()
self._prev_rsi = rv
return
self._update_trailing_long(close)
if self._should_close_long(low, high):
self.SellMarket()
self._reset_risk()
self._prev_rsi = rv
return
elif self.Position < 0:
exit_signal = prev < self.OversoldExtremeLevel and rv > self.OversoldActivationLevel
if exit_signal:
self.BuyMarket()
self._reset_risk()
self._prev_rsi = rv
return
self._update_trailing_short(close)
if self._should_close_short(low, high):
self.BuyMarket()
self._reset_risk()
self._prev_rsi = rv
return
# evaluate entries
if not self.IsFormedAndOnlineAndAllowTrading():
self._prev_rsi = rv
return
if close > 0:
if prev < self.OversoldExtremeLevel and rv > self.OversoldActivationLevel and self.Position <= 0:
self._entry_price = close
self._init_risk_long(close)
self.BuyMarket()
elif prev > self.OverboughtExtremeLevel and rv < self.OverboughtActivationLevel and self.Position >= 0:
self._entry_price = close
self._init_risk_short(close)
self.SellMarket()
self._prev_rsi = rv
def _init_risk_long(self, price):
pip = self._get_pip_size()
if self.UseStopLoss and self.StopLossPips > 0:
self._stop_loss_price = price - pip * self.StopLossPips
else:
self._stop_loss_price = None
if self.UseTakeProfit and self.TakeProfitPips > 0:
self._take_profit_price = price + pip * self.TakeProfitPips
else:
self._take_profit_price = None
def _init_risk_short(self, price):
pip = self._get_pip_size()
if self.UseStopLoss and self.StopLossPips > 0:
self._stop_loss_price = price + pip * self.StopLossPips
else:
self._stop_loss_price = None
if self.UseTakeProfit and self.TakeProfitPips > 0:
self._take_profit_price = price - pip * self.TakeProfitPips
else:
self._take_profit_price = None
def _update_trailing_long(self, close):
if not self.UseTrailingStop or self.TrailingStopPips <= 0:
return
pip = self._get_pip_size()
pip_dist = pip * self.TrailingStopPips
if pip_dist <= 0:
return
profit = close - self._entry_price
if profit <= pip_dist:
return
new_stop = close - pip_dist
if self._stop_loss_price is None or new_stop > self._stop_loss_price:
self._stop_loss_price = new_stop
def _update_trailing_short(self, close):
if not self.UseTrailingStop or self.TrailingStopPips <= 0:
return
pip = self._get_pip_size()
pip_dist = pip * self.TrailingStopPips
if pip_dist <= 0:
return
profit = self._entry_price - close
if profit <= pip_dist:
return
new_stop = close + pip_dist
if self._stop_loss_price is None or new_stop < self._stop_loss_price:
self._stop_loss_price = new_stop
def _should_close_long(self, low, high):
stop_hit = self.UseStopLoss and self._stop_loss_price is not None and low <= self._stop_loss_price
tp_hit = self.UseTakeProfit and self._take_profit_price is not None and high >= self._take_profit_price
return stop_hit or tp_hit
def _should_close_short(self, low, high):
stop_hit = self.UseStopLoss and self._stop_loss_price is not None and high >= self._stop_loss_price
tp_hit = self.UseTakeProfit and self._take_profit_price is not None and low <= self._take_profit_price
return stop_hit or tp_hit
def _reset_risk(self):
self._stop_loss_price = None
self._take_profit_price = None
self._entry_price = 0.0
def OnReseted(self):
super(rsi_ma_strategy, self).OnReseted()
self._prev_rsi = None
self._entry_price = 0.0
self._stop_loss_price = None
self._take_profit_price = None
def CreateClone(self):
return rsi_ma_strategy()