Ver en GitHub

Estrategia Money Rain

Descripción general

  • Conversión del asesor experto original MoneyRain (edición de barabashkakvn) de MQL5 a la API de alto nivel de StockSharp.
  • Usa el oscilador DeMarker para elegir la dirección: valores por encima de 0.5 activan entradas largas, mientras que valores de 0.5 o por debajo activan entradas cortas.
  • Opera solo una posición a la vez y se basa en offsets fijos de stop-loss/take-profit expresados en puntos.

Datos de mercado e indicadores

  • Se suscribe al CandleType configurable (por defecto: marco temporal de 30 minutos).
  • Calcula un único indicador DeMarker con DeMarkerPeriod ajustable (por defecto: 31).
  • Se suscribe a cotizaciones de Nivel 1 para aproximar el spread actual, que es requerido por la lógica de dimensionamiento adaptativo de posiciones.

Lógica de trading

  1. Procesa solo velas finalizadas para mantenerse alineado con la lógica original de "nueva barra" (verificación iTime(0) en MQL).
  2. Mientras existe una posición, monitorea el máximo/mínimo de la vela contra los niveles de stop-loss y take-profit precalculados. Si uno de ellos es tocado, cierra la posición con una orden de mercado y marca el resultado como pérdida o ganancia.
  3. Cuando no hay posición abierta y el límite de pérdidas no ha sido alcanzado, calcula el volumen de la operación.
  4. Entra largo en DeMarker > 0.5; de lo contrario entra corto. La estrategia cancela cualquier orden pendiente antes de enviar la orden de mercado.

Gestión de capital

  • Reproduce la lógica getLots() de la versión MQL rastreando:
    • _lossesVolume: volumen acumulado de operaciones perdedoras recientes escalado por el tamaño de lote base.
    • _consecutiveLosses y _consecutiveProfits: contadores de rachas usados para decidir cuándo reiniciar el acumulador de pérdidas.
  • Cuando aparece la primera operación rentable tras una racha perdedora (_consecutiveProfits == 0), el siguiente tamaño de orden se incrementa según la fórmula original: [ \text = \text \times \frac{_lossesVolume \times (\text + \text)}{\text - \text} ]
  • El spread se estima a partir de las mejores cotizaciones de compra/venta (en puntos) y se ignora cuando los datos de Nivel 1 aún no están disponibles.
  • Establecer FastOptimize = true deshabilita el dimensionamiento adaptativo y siempre usa el lote base.

Controles de riesgo

  • StopLossPoints y TakeProfitPoints se convierten a precios absolutos usando el paso de precio del instrumento con un multiplicador adicional de 10x para símbolos de 3 o 5 dígitos (refleja la lógica digits_adjust de MQL).
  • LossLimit bloquea más operaciones una vez que el número de pérdidas consecutivas supera el umbral definido por el usuario (por defecto: prácticamente deshabilitado en 1.000.000).

Parámetros

Parámetro Descripción Predeterminado
DeMarkerPeriod Período de promediado del indicador DeMarker. 31
TakeProfitPoints Offset de take-profit en puntos estilo DeMarker. 5
StopLossPoints Offset de stop-loss en puntos estilo DeMarker. 20
BaseVolume Volumen de orden predeterminado (tamaño de lote). 0.01
LossLimit Máximas pérdidas consecutivas permitidas antes de pausar. 1.000.000
FastOptimize Cuando es true, deshabilita el dimensionamiento adaptativo de posiciones. false
CandleType Tipo de datos de velas usado para cálculos. Velas de 30 minutos

Notas de implementación

  • Los stops y objetivos se emulan comprobando los extremos de las velas. El orden de ejecución intrabarra no puede recuperarse, por lo que los toques simultáneos favorecen la rama del stop-loss (suposición conservadora).
  • OnOwnTradeReceived se usa para detectar cuándo se completó una orden de salida protectora, permitiendo a la estrategia actualizar los contadores de racha y el acumulador de volumen de pérdidas.
  • El código mantiene la indentación con tabulaciones y usa comentarios en inglés, siguiendo las pautas del repositorio.

Archivos

  • CS/MoneyRainStrategy.cs – implementación de la estrategia.
  • README.md / README_ru.md / README_zh.md – documentación multilingüe.

Diferencias respecto a la versión MQL

  • Las órdenes protectoras del lado del bróker se reemplazan con salidas de mercado basadas en rangos de velas.
  • El spread se aproxima a partir de cotizaciones de Nivel 1 en lugar de directamente desde los metadatos del símbolo.
  • La funcionalidad de correo y las verificaciones explícitas de IsTradeAllowed se omiten porque el entorno StockSharp gestiona la conectividad por separado.
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;
}
}