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>
/// Money Rain strategy converted from the original MQL5 expert advisor.
/// </summary>
public class MoneyRainStrategy : Strategy
{
private enum ExitReasons
{
None,
StopLoss,
TakeProfit
}
private readonly StrategyParam<int> _deMarkerPeriod;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<int> _lossLimit;
private readonly StrategyParam<bool> _fastOptimize;
private readonly StrategyParam<DataType> _candleType;
private DeMarker _deMarker;
private decimal _adjustedPoint;
private decimal _takeProfitOffset;
private decimal _stopLossOffset;
private decimal _lastSpreadPoints;
private decimal _entryPrice;
private decimal _stopPrice;
private decimal _takePrice;
private decimal _activeVolume;
private int _consecutiveLosses;
private int _consecutiveProfits;
private decimal _lossesVolume;
private bool _exitOrderActive;
private ExitReasons _pendingExitReason;
private Sides? _currentSide;
/// <summary>
/// DeMarker indicator period.
/// </summary>
public int DeMarkerPeriod
{
get => _deMarkerPeriod.Value;
set => _deMarkerPeriod.Value = value;
}
/// <summary>
/// Take-profit distance measured in DeMarker-style points.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Stop-loss distance measured in DeMarker-style points.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Base trading volume.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Maximum allowed consecutive losses.
/// </summary>
public int LossLimit
{
get => _lossLimit.Value;
set => _lossLimit.Value = value;
}
/// <summary>
/// Enables lightweight optimisation mode that disables money management.
/// </summary>
public bool FastOptimize
{
get => _fastOptimize.Value;
set => _fastOptimize.Value = value;
}
/// <summary>
/// Candles used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes strategy parameters with defaults close to the MQL version.
/// </summary>
public MoneyRainStrategy()
{
_deMarkerPeriod = Param(nameof(DeMarkerPeriod), 31)
.SetGreaterThanZero()
.SetDisplay("DeMarker Period", "DeMarker indicator averaging period", "Indicators")
.SetOptimize(5, 60, 5);
_takeProfitPoints = Param(nameof(TakeProfitPoints), 5m)
.SetGreaterThanZero()
.SetDisplay("Take Profit (points)", "Take-profit distance expressed in points", "Risk")
.SetOptimize(2m, 15m, 1m);
_stopLossPoints = Param(nameof(StopLossPoints), 20m)
.SetGreaterThanZero()
.SetDisplay("Stop Loss (points)", "Stop-loss distance expressed in points", "Risk")
.SetOptimize(10m, 60m, 5m);
_baseVolume = Param(nameof(BaseVolume), 0.01m)
.SetGreaterThanZero()
.SetDisplay("Base Volume", "Lot size used when no recovery is required", "Trading")
.SetOptimize(0.01m, 1m, 0.01m);
_lossLimit = Param(nameof(LossLimit), 1000000)
.SetGreaterThanZero()
.SetDisplay("Loss Limit", "Maximum consecutive losses before trading is paused", "Risk");
_fastOptimize = Param(nameof(FastOptimize), false)
.SetDisplay("Fast Optimisation", "Disable adaptive position sizing during rough optimisation", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candles used for indicator calculations", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_deMarker = null;
_adjustedPoint = 0m;
_takeProfitOffset = 0m;
_stopLossOffset = 0m;
_lastSpreadPoints = 0m;
_entryPrice = 0m;
_stopPrice = 0m;
_takePrice = 0m;
_activeVolume = 0m;
_consecutiveLosses = 0;
_consecutiveProfits = 0;
_lossesVolume = 0m;
_exitOrderActive = false;
_pendingExitReason = ExitReasons.None;
_currentSide = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
UpdateOffsets();
_deMarker = new DeMarker
{
Length = DeMarkerPeriod
};
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_deMarker, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _deMarker);
DrawOwnTrades(area);
}
}
private void ProcessLevel1(Level1ChangeMessage level1)
{
if (_adjustedPoint <= 0m)
return;
if (level1.Changes.TryGetValue(Level1Fields.BestBidPrice, out var bidObj) &&
level1.Changes.TryGetValue(Level1Fields.BestAskPrice, out var askObj) &&
bidObj is decimal bid &&
askObj is decimal ask &&
ask > bid &&
bid > 0m)
{
_lastSpreadPoints = (ask - bid) / _adjustedPoint;
}
}
private void ProcessCandle(ICandleMessage candle, decimal deMarkerValue)
{
if (candle.State != CandleStates.Finished)
return;
ManageOpenPosition(candle);
if (_deMarker == null || !_deMarker.IsFormed)
return;
if (Position != 0 || _exitOrderActive)
return;
if (LossLimit > 0 && _consecutiveLosses >= LossLimit)
{
this.LogInfo($"Trading paused after reaching loss limit of {LossLimit} consecutive losses.");
return;
}
if (_adjustedPoint <= 0m)
UpdateOffsets();
var volume = GetTradeVolume();
if (volume <= 0m)
return;
if (deMarkerValue > 0.5m)
{
EnterPosition(Sides.Buy, volume, candle.ClosePrice, deMarkerValue);
}
else
{
EnterPosition(Sides.Sell, volume, candle.ClosePrice, deMarkerValue);
}
}
private void ManageOpenPosition(ICandleMessage candle)
{
if (_currentSide == null || Position == 0 || _exitOrderActive)
return;
var hasStop = _stopLossOffset > 0m;
var hasTake = _takeProfitOffset > 0m;
var hitStop = false;
var hitTake = false;
switch (_currentSide)
{
case Sides.Buy:
hitStop = hasStop && candle.LowPrice <= _stopPrice;
hitTake = hasTake && candle.HighPrice >= _takePrice;
break;
case Sides.Sell:
hitStop = hasStop && candle.HighPrice >= _stopPrice;
hitTake = hasTake && candle.LowPrice <= _takePrice;
break;
}
if (!hitStop && !hitTake)
return;
_exitOrderActive = true;
_pendingExitReason = hitStop ? ExitReasons.StopLoss : ExitReasons.TakeProfit;
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
var exitPrice = hitStop ? _stopPrice : _takePrice;
this.LogInfo(hitStop
? $"Stop-loss triggered near {exitPrice} (range {candle.LowPrice} - {candle.HighPrice})."
: $"Take-profit triggered near {exitPrice} (range {candle.LowPrice} - {candle.HighPrice}).");
}
private void EnterPosition(Sides side, decimal volume, decimal referencePrice, decimal deMarkerValue)
{
CancelActiveOrders();
_currentSide = side;
_exitOrderActive = false;
_pendingExitReason = ExitReasons.None;
_entryPrice = referencePrice;
_activeVolume = volume;
if (side == Sides.Buy)
{
_stopPrice = referencePrice - _stopLossOffset;
_takePrice = referencePrice + _takeProfitOffset;
BuyMarket();
this.LogInfo($"Entered long at {referencePrice} (DeMarker={deMarkerValue:F4}) with volume {volume}.");
}
else
{
_stopPrice = referencePrice + _stopLossOffset;
_takePrice = referencePrice - _takeProfitOffset;
SellMarket();
this.LogInfo($"Entered short at {referencePrice} (DeMarker={deMarkerValue:F4}) with volume {volume}.");
}
}
private decimal GetTradeVolume()
{
var volume = BaseVolume;
if (volume <= 0m)
return 0m;
if (FastOptimize)
return volume;
if (_lossesVolume <= 0.5m || _consecutiveProfits > 0)
return volume;
var spread = Math.Max(0m, _lastSpreadPoints);
var denominator = TakeProfitPoints - spread;
if (denominator <= 0m)
return volume;
var multiplier = _lossesVolume * (StopLossPoints + spread) / denominator;
if (multiplier <= 0m)
return volume;
return volume * multiplier;
}
private void UpdateTradeStats(bool isProfit)
{
if (isProfit)
{
_consecutiveLosses = 0;
if (_consecutiveProfits > 1)
_lossesVolume = 0m;
_consecutiveProfits++;
this.LogInfo($"Take-profit confirmed. Profit streak = {_consecutiveProfits}.");
}
else
{
_consecutiveLosses++;
_consecutiveProfits = 0;
if (BaseVolume > 0m)
_lossesVolume += _activeVolume / BaseVolume;
this.LogInfo($"Stop-loss confirmed. Loss streak = {_consecutiveLosses}, accumulated loss volume = {_lossesVolume:F2}.");
}
}
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (_exitOrderActive)
{
if (Position != 0)
return;
UpdateTradeStats(_pendingExitReason == ExitReasons.TakeProfit);
_exitOrderActive = false;
_pendingExitReason = ExitReasons.None;
_currentSide = null;
_entryPrice = 0m;
_stopPrice = 0m;
_takePrice = 0m;
_activeVolume = 0m;
return;
}
if (_currentSide != null && Position != 0)
{
_entryPrice = trade.Trade.Price;
_activeVolume = trade.Trade.Volume;
}
}
private void UpdateOffsets()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
priceStep = 0.0001m;
var decimals = Security?.Decimals ?? 0;
var digitsAdjust = (decimals == 3 || decimals == 5) ? 10m : 1m;
_adjustedPoint = priceStep * digitsAdjust;
_takeProfitOffset = TakeProfitPoints * _adjustedPoint;
_stopLossOffset = StopLossPoints * _adjustedPoint;
}
}
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, Unit, UnitTypes
from StockSharp.Algo.Indicators import DeMarker
from StockSharp.Algo.Strategies import Strategy
EXIT_NONE = 0
EXIT_STOP_LOSS = 1
EXIT_TAKE_PROFIT = 2
SIDE_BUY = 1
SIDE_SELL = -1
class money_rain_strategy(Strategy):
def __init__(self):
super(money_rain_strategy, self).__init__()
self._demarker_period = self.Param("DeMarkerPeriod", 31)
self._take_profit_points = self.Param("TakeProfitPoints", 5.0)
self._stop_loss_points = self.Param("StopLossPoints", 20.0)
self._base_volume = self.Param("BaseVolume", 0.01)
self._loss_limit = self.Param("LossLimit", 1000000)
self._fast_optimize = self.Param("FastOptimize", False)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._adjusted_point = 0.0
self._take_profit_offset = 0.0
self._stop_loss_offset = 0.0
self._last_spread_points = 0.0
self._entry_price = 0.0
self._stop_price = 0.0
self._take_price = 0.0
self._active_volume = 0.0
self._consecutive_losses = 0
self._consecutive_profits = 0
self._losses_volume = 0.0
self._exit_order_active = False
self._pending_exit_reason = EXIT_NONE
self._current_side = None
@property
def DeMarkerPeriod(self):
return self._demarker_period.Value
@DeMarkerPeriod.setter
def DeMarkerPeriod(self, value):
self._demarker_period.Value = value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@TakeProfitPoints.setter
def TakeProfitPoints(self, value):
self._take_profit_points.Value = value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@StopLossPoints.setter
def StopLossPoints(self, value):
self._stop_loss_points.Value = value
@property
def BaseVolume(self):
return self._base_volume.Value
@BaseVolume.setter
def BaseVolume(self, value):
self._base_volume.Value = value
@property
def LossLimit(self):
return self._loss_limit.Value
@LossLimit.setter
def LossLimit(self, value):
self._loss_limit.Value = value
@property
def FastOptimize(self):
return self._fast_optimize.Value
@FastOptimize.setter
def FastOptimize(self, value):
self._fast_optimize.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(money_rain_strategy, self).OnStarted2(time)
self._update_offsets()
self._demarker = DeMarker()
self._demarker.Length = self.DeMarkerPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._demarker, self.ProcessCandle).Start()
self.StartProtection(
Unit(2000.0, UnitTypes.Absolute),
Unit(1000.0, UnitTypes.Absolute))
def ProcessCandle(self, candle, demarker_value):
if candle.State != CandleStates.Finished:
return
self._manage_open_position(candle)
if not self._demarker.IsFormed:
return
if self.Position != 0 or self._exit_order_active:
return
if self.LossLimit > 0 and self._consecutive_losses >= self.LossLimit:
return
if self._adjusted_point <= 0.0:
self._update_offsets()
volume = self._get_trade_volume()
if volume <= 0.0:
return
dm = float(demarker_value)
close = float(candle.ClosePrice)
if dm > 0.5:
self._enter_position(SIDE_BUY, volume, close)
else:
self._enter_position(SIDE_SELL, volume, close)
def _manage_open_position(self, candle):
if self._current_side is None or self.Position == 0 or self._exit_order_active:
return
has_stop = self._stop_loss_offset > 0.0
has_take = self._take_profit_offset > 0.0
hit_stop = False
hit_take = False
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self._current_side == SIDE_BUY:
hit_stop = has_stop and low <= self._stop_price
hit_take = has_take and high >= self._take_price
elif self._current_side == SIDE_SELL:
hit_stop = has_stop and high >= self._stop_price
hit_take = has_take and low <= self._take_price
if not hit_stop and not hit_take:
return
self._exit_order_active = True
self._pending_exit_reason = EXIT_STOP_LOSS if hit_stop else EXIT_TAKE_PROFIT
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
is_profit = (self._pending_exit_reason == EXIT_TAKE_PROFIT)
self._update_trade_stats(is_profit)
self._exit_order_active = False
self._pending_exit_reason = EXIT_NONE
self._current_side = None
self._entry_price = 0.0
self._stop_price = 0.0
self._take_price = 0.0
self._active_volume = 0.0
def _enter_position(self, side, volume, reference_price):
self._current_side = side
self._exit_order_active = False
self._pending_exit_reason = EXIT_NONE
self._entry_price = reference_price
self._active_volume = volume
if side == SIDE_BUY:
self._stop_price = reference_price - self._stop_loss_offset
self._take_price = reference_price + self._take_profit_offset
self.BuyMarket()
else:
self._stop_price = reference_price + self._stop_loss_offset
self._take_price = reference_price - self._take_profit_offset
self.SellMarket()
def _get_trade_volume(self):
volume = float(self.BaseVolume)
if volume <= 0.0:
return 0.0
if self.FastOptimize:
return volume
if self._losses_volume <= 0.5 or self._consecutive_profits > 0:
return volume
spread = max(0.0, self._last_spread_points)
denominator = float(self.TakeProfitPoints) - spread
if denominator <= 0.0:
return volume
multiplier = self._losses_volume * (float(self.StopLossPoints) + spread) / denominator
if multiplier <= 0.0:
return volume
return volume * multiplier
def _update_trade_stats(self, is_profit):
if is_profit:
self._consecutive_losses = 0
if self._consecutive_profits > 1:
self._losses_volume = 0.0
self._consecutive_profits += 1
else:
self._consecutive_losses += 1
self._consecutive_profits = 0
bv = float(self.BaseVolume)
if bv > 0.0:
self._losses_volume += self._active_volume / bv
def _update_offsets(self):
price_step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0001
if price_step <= 0.0:
price_step = 0.0001
decimals = self.Security.Decimals if self.Security is not None and self.Security.Decimals is not None else 0
digits_adjust = 10.0 if (decimals == 3 or decimals == 5) else 1.0
self._adjusted_point = price_step * digits_adjust
self._take_profit_offset = float(self.TakeProfitPoints) * self._adjusted_point
self._stop_loss_offset = float(self.StopLossPoints) * self._adjusted_point
def OnReseted(self):
super(money_rain_strategy, self).OnReseted()
self._adjusted_point = 0.0
self._take_profit_offset = 0.0
self._stop_loss_offset = 0.0
self._last_spread_points = 0.0
self._entry_price = 0.0
self._stop_price = 0.0
self._take_price = 0.0
self._active_volume = 0.0
self._consecutive_losses = 0
self._consecutive_profits = 0
self._losses_volume = 0.0
self._exit_order_active = False
self._pending_exit_reason = EXIT_NONE
self._current_side = None
def CreateClone(self):
return money_rain_strategy()