GitHub で見る

Money Rain Strategy

Overview

  • Conversion of the original MoneyRain (barabashkakvn's edition) expert advisor from MQL5 to StockSharp high-level API.
  • Uses the DeMarker oscillator to choose direction: values above 0.5 trigger long entries, while values at or below 0.5 trigger short entries.
  • Trades only one position at a time and relies on fixed stop-loss / take-profit offsets expressed in points.

Market Data & Indicators

  • Subscribes to the configurable CandleType (default: 30-minute time frame).
  • Computes a single DeMarker indicator with adjustable DeMarkerPeriod (default: 31).
  • Subscribes to Level 1 quotes to approximate the current spread, which is required by the adaptive position-sizing logic.

Trading Logic

  1. Process only finished candles to stay aligned with the original "new bar" logic (iTime(0) check in MQL).
  2. While a position exists, monitor the candle high/low against pre-computed stop-loss and take-profit levels. If one of them is touched, close the position with a market order and mark the result as either a loss or a profit.
  3. When there is no open position and the loss-limit safeguard is not hit, calculate the trade volume.
  4. Enter long on DeMarker > 0.5; otherwise enter short. The strategy cancels any resting orders before sending the market order.

Money Management

  • Reproduces the getLots() logic from the MQL version by tracking:
    • _lossesVolume: cumulative volume of recent losing trades scaled by the base lot size.
    • _consecutiveLosses and _consecutiveProfits: streak counters used to decide when to reset the loss accumulator.
  • When the first profitable trade after a losing streak appears (_consecutiveProfits == 0), the next order size is increased according to the original formula: [ \text = \text \times \frac{_lossesVolume \times (\text + \text)}{\text - \text} ]
  • The spread is estimated from best bid/ask quotes (in points) and ignored when Level 1 data is not yet available.
  • Setting FastOptimize = true disables the adaptive sizing and always uses the base lot.

Risk Controls

  • StopLossPoints and TakeProfitPoints are converted to absolute prices using the security price step with an additional 10x multiplier for 3- or 5-digit symbols (mirrors the digits_adjust logic from MQL).
  • LossLimit blocks further trades once the number of consecutive losses exceeds the user-defined threshold (default: effectively disabled at 1,000,000).

Parameters

Parameter Description Default
DeMarkerPeriod Averaging period of the DeMarker indicator. 31
TakeProfitPoints Take-profit offset in DeMarker-style points. 5
StopLossPoints Stop-loss offset in DeMarker-style points. 20
BaseVolume Default order volume (lot size). 0.01
LossLimit Maximum consecutive losses allowed before pausing. 1,000,000
FastOptimize When true, disables adaptive position sizing. false
CandleType Candle data type used for calculations. 30-minute candles

Implementation Notes

  • Stops and targets are emulated by checking candle extremes. Intrabar fill order cannot be recovered, so simultaneous touches favour the stop-loss branch (conservative assumption).
  • OnOwnTradeReceived is used to detect when a protective exit order completed, allowing the strategy to update streak counters and loss-volume accumulator.
  • The code keeps indentation with tabs and uses English comments, following repository guidelines.

Files

  • CS/MoneyRainStrategy.cs – strategy implementation.
  • README.md / README_ru.md / README_zh.md – multilingual documentation.

Differences from the MQL Version

  • Broker-side protective orders are replaced with market exits based on candle ranges.
  • Spread is approximated from Level 1 quotes rather than directly from symbol metadata.
  • Mailing functionality and explicit IsTradeAllowed checks are omitted because the StockSharp environment manages connectivity separately.
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;
}
}