Two MA RSI 策略
概述
Two MA RSI 策略来自 MetaTrader 专家顾问“2MA_RSI”的移植版本。策略结合了一条快 EMA 和一条慢 EMA 的金叉/死叉,并用 RSI 过滤信号。下单量采用类似马丁格尔的资金管理:每次亏损后会扩大下一笔订单的数量。StockSharp 版本仅在每根 K 线收盘后运行,并按照点数重新计算原策略中的止盈与止损。
数据与指标
- 只订阅一个由
CandleType指定的蜡烛序列(默认 5 分钟)。 - 每根完成的 K 线都会更新三个指标:
FastLength长度的 EMA(使用收盘价)。SlowLength长度的 EMA。RsiLength长度的 RSI。
- 策略内部保存上一根 K 线的 EMA 值,用于检测金叉/死叉,无需访问指标缓冲区。
入场逻辑
- 必须在上一根 K 线收盘后评估信号,避免盘中重复触发。
- 当前必须没有持仓(
Position == 0)。 - 做多条件:
- 快 EMA 从下往上穿越慢 EMA(当前快 EMA > 慢 EMA,上一根快 EMA < 慢 EMA)。
- RSI 低于
RsiOversold,显示市场超卖。
- 做空条件:
- 快 EMA 从上往下穿越慢 EMA(当前快 EMA < 慢 EMA,上一根快 EMA > 慢 EMA)。
- RSI 高于
RsiOverbought,显示市场超买。
- 满足条件时发送市价单,数量由马丁格尔模块决定。
出场逻辑
- 入场后立即根据点数计算止损和止盈,点数会乘以标的的
PriceStep转成价格:- 多头:
- 止损 =
入场价 - StopLossPoints * PriceStep。 - 止盈 =
入场价 + TakeProfitPoints * PriceStep。
- 止损 =
- 空头:
- 止损 =
入场价 + StopLossPoints * PriceStep。 - 止盈 =
入场价 - TakeProfitPoints * PriceStep。
- 止损 =
- 多头:
- 只有触发这些保护价位才会平仓。策略在下一根 K 线检查最高价和最低价是否触碰目标,并调用
ClosePosition()发出市价离场。 - 如果同一根 K 线同时覆盖止盈和止损区间,会优先判定止损,保持与原有 EA 相同的保守行为。
仓位管理与马丁格尔
- 每次入场前计算基础下单量:
floor(balance / BalanceDivider) * VolumeStep。余额优先使用投资组合的CurrentValue,若不可用则使用BeginValue,并确保不低于一个成交量步长。 - 每次亏损后马丁格尔阶段加一,但不超过
MaxDoublings,下一次下单量乘以2^stage。 - 任意盈利或达到最大加倍次数都会将阶段归零,恢复基础下单量。
- 当
MaxDoublings小于或等于零时,策略不会放大仓位,始终采用基础下单量。
其他行为
- 策略内部保存所需的 EMA 值,不使用额外的数据结构。
- 只有在指标已形成且允许交易 (
IsFormedAndOnlineAndAllowTrading) 时才会发送订单。 - 图表会绘制价格 K 线、自己的成交记录以及三个指标曲线,便于可视化分析。
参数
| 参数 | 说明 | 默认值 |
|---|---|---|
FastLength |
快速 EMA 的周期。 | 5 |
SlowLength |
慢速 EMA 的周期。 | 20 |
RsiLength |
RSI 的计算周期。 | 14 |
RsiOverbought |
允许做空的 RSI 超买阈值。 | 70 |
RsiOversold |
允许做多的 RSI 超卖阈值。 | 30 |
StopLossPoints |
以价格步长表示的止损距离。 | 500 |
TakeProfitPoints |
以价格步长表示的止盈距离。 | 1500 |
BalanceDivider |
将账户价值除以该系数得到基础下单量。 | 1000 |
MaxDoublings |
连续亏损后允许的最大加倍次数。 | 1 |
CandleType |
使用的蜡烛类型。 | 5 分钟 |
使用提示
- 请确保证券的
PriceStep和VolumeStep已设置,否则点数和下单量无法正确换算。 - 由于采用市价平仓,实际成交仍可能出现滑点,但止损/止盈的逻辑与原 EA 保持一致。
- 本次仅提供 C# 实现,未创建 Python 版本或对应目录。
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>
/// Moving average crossover strategy with RSI confirmation and martingale sizing.
/// </summary>
public class TwoMaRsiStrategy : Strategy
{
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _rsiLength;
private readonly StrategyParam<decimal> _rsiOverbought;
private readonly StrategyParam<decimal> _rsiOversold;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _balanceDivider;
private readonly StrategyParam<int> _maxDoublings;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _fastEma;
private ExponentialMovingAverage _slowEma;
private RelativeStrengthIndex _rsi;
private decimal? _previousFast;
private decimal? _previousSlow;
private decimal _entryPrice;
private decimal _stopPrice;
private decimal _takeProfitPrice;
private int _martingaleStage;
private bool _isClosing;
/// <summary>
/// Initializes a new instance of the <see cref="TwoMaRsiStrategy"/> class.
/// </summary>
public TwoMaRsiStrategy()
{
_fastLength = Param(nameof(FastLength), 5)
.SetDisplay("Fast EMA Length", "Length of the fast exponential moving average", "Indicators")
.SetOptimize(2, 20, 1);
_slowLength = Param(nameof(SlowLength), 20)
.SetDisplay("Slow EMA Length", "Length of the slow exponential moving average", "Indicators")
.SetOptimize(10, 60, 5);
_rsiLength = Param(nameof(RsiLength), 14)
.SetDisplay("RSI Length", "Number of bars for the RSI calculation", "Indicators")
.SetOptimize(5, 30, 1);
_rsiOverbought = Param(nameof(RsiOverbought), 50m)
.SetDisplay("RSI Overbought", "Upper RSI threshold for short entries", "Signals");
_rsiOversold = Param(nameof(RsiOversold), 50m)
.SetDisplay("RSI Oversold", "Lower RSI threshold for long entries", "Signals");
_stopLossPoints = Param(nameof(StopLossPoints), 500m)
.SetDisplay("Stop Loss (points)", "Stop loss distance in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 1500m)
.SetDisplay("Take Profit (points)", "Take profit distance in price steps", "Risk");
_balanceDivider = Param(nameof(BalanceDivider), 1000m)
.SetDisplay("Balance Divider", "Divides portfolio value to estimate base order volume", "Money Management");
_maxDoublings = Param(nameof(MaxDoublings), 1)
.SetDisplay("Max Doublings", "Maximum number of martingale doublings", "Money Management");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Primary candle series for the strategy", "General");
}
/// <summary>
/// Fast EMA length.
/// </summary>
public int FastLength
{
get => _fastLength.Value;
set => _fastLength.Value = value;
}
/// <summary>
/// Slow EMA length.
/// </summary>
public int SlowLength
{
get => _slowLength.Value;
set => _slowLength.Value = value;
}
/// <summary>
/// RSI period.
/// </summary>
public int RsiLength
{
get => _rsiLength.Value;
set => _rsiLength.Value = value;
}
/// <summary>
/// Overbought threshold for RSI.
/// </summary>
public decimal RsiOverbought
{
get => _rsiOverbought.Value;
set => _rsiOverbought.Value = value;
}
/// <summary>
/// Oversold threshold for RSI.
/// </summary>
public decimal RsiOversold
{
get => _rsiOversold.Value;
set => _rsiOversold.Value = value;
}
/// <summary>
/// Stop loss distance expressed in price steps.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance expressed in price steps.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Divider applied to the portfolio value to calculate the base order volume.
/// </summary>
public decimal BalanceDivider
{
get => _balanceDivider.Value;
set => _balanceDivider.Value = value;
}
/// <summary>
/// Maximum number of martingale doublings.
/// </summary>
public int MaxDoublings
{
get => _maxDoublings.Value;
set => _maxDoublings.Value = value;
}
/// <summary>
/// Candle data type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fastEma = null;
_slowEma = null;
_rsi = null;
_previousFast = null;
_previousSlow = null;
_entryPrice = default;
_stopPrice = default;
_takeProfitPrice = default;
_martingaleStage = 0;
_isClosing = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastEma = new ExponentialMovingAverage
{
Length = FastLength
};
_slowEma = new ExponentialMovingAverage
{
Length = SlowLength
};
_rsi = new RelativeStrengthIndex
{
Length = RsiLength
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
if (Position == 0 && _isClosing)
{
_isClosing = false;
_entryPrice = default;
_stopPrice = default;
_takeProfitPrice = default;
}
var fastResult = _fastEma.Process(candle);
var slowResult = _slowEma.Process(candle);
var rsiResult = _rsi.Process(candle);
if (fastResult.IsEmpty || slowResult.IsEmpty || rsiResult.IsEmpty)
{
return;
}
if (!_fastEma.IsFormed || !_slowEma.IsFormed || !_rsi.IsFormed)
{
_previousFast = fastResult.GetValue<decimal>();
_previousSlow = slowResult.GetValue<decimal>();
return;
}
var fast = fastResult.GetValue<decimal>();
var slow = slowResult.GetValue<decimal>();
var rsi = rsiResult.GetValue<decimal>();
var point = GetPoint();
if (Position > 0)
{
var stopHit = candle.LowPrice <= _stopPrice;
var takeHit = candle.HighPrice >= _takeProfitPrice;
if (!_isClosing && stopHit)
{
_isClosing = true;
ClosePosition();
RegisterLoss();
}
else if (!_isClosing && takeHit)
{
_isClosing = true;
ClosePosition();
RegisterWin();
}
}
else if (Position < 0)
{
var stopHit = candle.HighPrice >= _stopPrice;
var takeHit = candle.LowPrice <= _takeProfitPrice;
if (!_isClosing && stopHit)
{
_isClosing = true;
ClosePosition();
RegisterLoss();
}
else if (!_isClosing && takeHit)
{
_isClosing = true;
ClosePosition();
RegisterWin();
}
}
else if (!_isClosing)
{
if (_previousFast is null || _previousSlow is null)
{
_previousFast = fast;
_previousSlow = slow;
return;
}
var prevFast = _previousFast.Value;
var prevSlow = _previousSlow.Value;
var crossUp = prevFast < prevSlow && fast > slow && rsi < RsiOversold;
var crossDown = prevFast > prevSlow && fast < slow && rsi > RsiOverbought;
if (crossUp)
{
var volume = CalculateOrderVolume();
if (volume > 0m)
{
BuyMarket(volume);
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice - StopLossPoints * point;
_takeProfitPrice = _entryPrice + TakeProfitPoints * point;
}
}
else if (crossDown)
{
var volume = CalculateOrderVolume();
if (volume > 0m)
{
SellMarket(volume);
_entryPrice = candle.ClosePrice;
_stopPrice = _entryPrice + StopLossPoints * point;
_takeProfitPrice = _entryPrice - TakeProfitPoints * point;
}
}
}
_previousFast = fast;
_previousSlow = slow;
}
private decimal GetPoint()
{
var step = Security?.PriceStep ?? 1m;
return step > 0m ? step : 1m;
}
private decimal CalculateOrderVolume()
{
var step = Security?.VolumeStep ?? 1m;
if (step <= 0m)
step = 1m;
var baseVolume = step;
var divider = BalanceDivider;
var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
if (divider > 0m && balance > 0m)
{
var count = Math.Floor((double)(balance / divider));
baseVolume = (decimal)count * step;
if (baseVolume < step)
baseVolume = step;
}
var multiplier = CalculateMartingaleMultiplier();
var volume = baseVolume * multiplier;
if (volume < step)
volume = step;
var ratio = volume / step;
volume = Math.Ceiling(ratio) * step;
return volume;
}
private decimal CalculateMartingaleMultiplier()
{
if (MaxDoublings <= 0 || _martingaleStage <= 0)
return 1m;
var stage = Math.Min(_martingaleStage, MaxDoublings);
return (decimal)Math.Pow(2d, stage);
}
private void RegisterWin()
{
_martingaleStage = 0;
}
private void ClosePosition()
{
if (Position > 0)
SellMarket(Position);
else if (Position < 0)
BuyMarket(-Position);
}
private void RegisterLoss()
{
if (MaxDoublings <= 0)
{
_martingaleStage = 0;
return;
}
if (_martingaleStage < MaxDoublings)
{
_martingaleStage++;
}
else
{
_martingaleStage = 0;
}
}
}
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 ExponentialMovingAverage, RelativeStrengthIndex, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class two_ma_rsi_strategy(Strategy):
def __init__(self):
super(two_ma_rsi_strategy, self).__init__()
self._fast_length = self.Param("FastLength", 5)
self._slow_length = self.Param("SlowLength", 20)
self._rsi_length = self.Param("RsiLength", 14)
self._rsi_overbought = self.Param("RsiOverbought", 50.0)
self._rsi_oversold = self.Param("RsiOversold", 50.0)
self._stop_loss_points = self.Param("StopLossPoints", 500.0)
self._take_profit_points = self.Param("TakeProfitPoints", 1500.0)
self._balance_divider = self.Param("BalanceDivider", 1000.0)
self._max_doublings = self.Param("MaxDoublings", 1)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._fast_ema = None
self._slow_ema = None
self._rsi = None
self._previous_fast = None
self._previous_slow = None
self._entry_price = 0.0
self._stop_price = 0.0
self._take_profit_price = 0.0
self._martingale_stage = 0
self._is_closing = False
@property
def FastLength(self):
return self._fast_length.Value
@property
def SlowLength(self):
return self._slow_length.Value
@property
def RsiLength(self):
return self._rsi_length.Value
@property
def RsiOverbought(self):
return self._rsi_overbought.Value
@property
def RsiOversold(self):
return self._rsi_oversold.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def BalanceDivider(self):
return self._balance_divider.Value
@property
def MaxDoublings(self):
return self._max_doublings.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(two_ma_rsi_strategy, self).OnStarted2(time)
self._fast_ema = ExponentialMovingAverage()
self._fast_ema.Length = self.FastLength
self._slow_ema = ExponentialMovingAverage()
self._slow_ema.Length = self.SlowLength
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.RsiLength
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
pos = float(self.Position)
if pos == 0 and self._is_closing:
self._is_closing = False
self._entry_price = 0.0
self._stop_price = 0.0
self._take_profit_price = 0.0
civ1 = CandleIndicatorValue(self._fast_ema, candle)
civ1.IsFinal = True
fast_result = self._fast_ema.Process(civ1)
civ2 = CandleIndicatorValue(self._slow_ema, candle)
civ2.IsFinal = True
slow_result = self._slow_ema.Process(civ2)
civ3 = CandleIndicatorValue(self._rsi, candle)
civ3.IsFinal = True
rsi_result = self._rsi.Process(civ3)
if fast_result.IsEmpty or slow_result.IsEmpty or rsi_result.IsEmpty:
return
if not self._fast_ema.IsFormed or not self._slow_ema.IsFormed or not self._rsi.IsFormed:
try:
self._previous_fast = float(fast_result.Value)
self._previous_slow = float(slow_result.Value)
except:
pass
return
try:
fast = float(fast_result.Value)
slow = float(slow_result.Value)
rsi = float(rsi_result.Value)
except:
return
point = self._get_point()
pos = float(self.Position)
if pos > 0:
stop_hit = float(candle.LowPrice) <= self._stop_price
take_hit = float(candle.HighPrice) >= self._take_profit_price
if not self._is_closing and stop_hit:
self._is_closing = True
self._close_position()
self._register_loss()
elif not self._is_closing and take_hit:
self._is_closing = True
self._close_position()
self._register_win()
elif pos < 0:
stop_hit = float(candle.HighPrice) >= self._stop_price
take_hit = float(candle.LowPrice) <= self._take_profit_price
if not self._is_closing and stop_hit:
self._is_closing = True
self._close_position()
self._register_loss()
elif not self._is_closing and take_hit:
self._is_closing = True
self._close_position()
self._register_win()
elif not self._is_closing:
if self._previous_fast is None or self._previous_slow is None:
self._previous_fast = fast
self._previous_slow = slow
return
prev_fast = self._previous_fast
prev_slow = self._previous_slow
cross_up = prev_fast < prev_slow and fast > slow and rsi < float(self.RsiOversold)
cross_down = prev_fast > prev_slow and fast < slow and rsi > float(self.RsiOverbought)
if cross_up:
volume = self._calculate_order_volume()
if volume > 0:
self.BuyMarket(volume)
self._entry_price = float(candle.ClosePrice)
self._stop_price = self._entry_price - float(self.StopLossPoints) * point
self._take_profit_price = self._entry_price + float(self.TakeProfitPoints) * point
elif cross_down:
volume = self._calculate_order_volume()
if volume > 0:
self.SellMarket(volume)
self._entry_price = float(candle.ClosePrice)
self._stop_price = self._entry_price + float(self.StopLossPoints) * point
self._take_profit_price = self._entry_price - float(self.TakeProfitPoints) * point
self._previous_fast = fast
self._previous_slow = slow
def _get_point(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
return step if step > 0 else 1.0
def _calculate_order_volume(self):
sec = self.Security
step = float(sec.VolumeStep) if sec is not None and sec.VolumeStep is not None else 1.0
if step <= 0:
step = 1.0
base_volume = step
divider = float(self.BalanceDivider)
portfolio = self.Portfolio
balance = 0.0
if portfolio is not None:
if portfolio.CurrentValue is not None:
balance = float(portfolio.CurrentValue)
elif portfolio.BeginValue is not None:
balance = float(portfolio.BeginValue)
if divider > 0 and balance > 0:
import math
count = math.floor(balance / divider)
base_volume = count * step
if base_volume < step:
base_volume = step
multiplier = self._calculate_martingale_multiplier()
volume = base_volume * multiplier
if volume < step:
volume = step
import math
ratio = volume / step
volume = math.ceil(ratio) * step
return volume
def _calculate_martingale_multiplier(self):
if self.MaxDoublings <= 0 or self._martingale_stage <= 0:
return 1.0
stage = min(self._martingale_stage, self.MaxDoublings)
return 2.0 ** stage
def _register_win(self):
self._martingale_stage = 0
def _register_loss(self):
if self.MaxDoublings <= 0:
self._martingale_stage = 0
return
if self._martingale_stage < self.MaxDoublings:
self._martingale_stage += 1
else:
self._martingale_stage = 0
def _close_position(self):
pos = float(self.Position)
if pos > 0:
self.SellMarket(pos)
elif pos < 0:
self.BuyMarket(abs(pos))
def OnReseted(self):
super(two_ma_rsi_strategy, self).OnReseted()
self._fast_ema = None
self._slow_ema = None
self._rsi = None
self._previous_fast = None
self._previous_slow = None
self._entry_price = 0.0
self._stop_price = 0.0
self._take_profit_price = 0.0
self._martingale_stage = 0
self._is_closing = False
def CreateClone(self):
return two_ma_rsi_strategy()