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()