Ver en GitHub

Estrategia Nevalyashka Martingale

Descripción general

La estrategia Nevalyashka Martingale es una portación directa del asesor experto de MetaTrader 5 "Nevalyashka3_1". Ejecuta un martingale de símbolo único que alterna entre comprar y vender después de operaciones con pérdida. La estrategia siempre comienza vendiendo y mide el capital de la cuenta para decidir si el ciclo de operaciones anterior terminó en ganancia o pérdida. Una ganancia reinicia el volumen al tamaño de lote base y mantiene la dirección sin cambios, mientras que una pérdida multiplica el tamaño del lote y cambia la dirección en un intento de recuperar la caída.

Cómo funciona

  • Operación inicial – se abre una posición corta en el primer candle completado usando el tamaño de lote base.
  • Seguimiento de capital – la estrategia almacena el valor de capital máximo observado. Cuando no hay posición abierta compara el capital actual con el pico almacenado.
    • Si el capital marcó un nuevo máximo, la siguiente operación usa el tamaño de lote base y repite la última dirección.
    • Si el capital no marcó un nuevo máximo, la siguiente operación aumenta el lote por el multiplicador y cambia de dirección.
  • Stop loss / take profit – cada orden usa distancias fijas definidas en "puntos" (pasos del instrumento). El take profit refleja el experto original: el stop está StopLossPoints alejado de la entrada y el objetivo está TakeProfitPoints alejado.
  • Trailing – una vez que el precio se mueve en MoveProfitPoints, el stop se ajusta. Cada movimiento requiere un buffer adicional de MoveStepPoints para que el stop solo avance cuando el mercado continúa empujando. Cuando el stop se lleva más allá del precio de entrada, el volumen planificado se divide por el multiplicador, reduciendo la siguiente operación hacia el lote base.
  • Salida de posición – la posición se cierra inmediatamente cuando el máximo/mínimo del candle alcanza los niveles de stop o take. Después del cierre, la estrategia evalúa el capital y prepara la siguiente señal.

Parámetros

  • BaseVolume – tamaño de lote para la operación inicial y cualquier ciclo rentable (predeterminado 0.1).
  • VolumeMultiplier – factor aplicado después de una pérdida para aumentar el siguiente lote (predeterminado 1.1).
  • TakeProfitPoints – distancia de take-profit medida en puntos de precio (predeterminado 94).
  • MoveProfitPoints – excursión favorable mínima antes de que se active el stop trailing (predeterminado 25).
  • MoveStepPoints – buffer extra requerido entre ajustes de trailing sucesivos (predeterminado 11).
  • StopLossPoints – distancia inicial del stop-loss medida en puntos de precio (predeterminado 70).
  • CandleType – marco temporal usado para la gestión de operaciones. El predeterminado son velas de 5 minutos.

Detalles de gestión de posición

  • La estrategia mantiene _plannedVolume para reflejar la variable "Lot" original. Solo cambia después de cerrar una operación o cuando el stop supera el break-even.
  • AdjustVolume respeta las reglas de exchange alineando el tamaño del lote a VolumeStep y haciendo cumplir MinVolume/MaxVolume.
  • GetPointValue replica la lógica de "punto ajustado" de MT5: para instrumentos cotizados con 3 o 5 decimales, el tamaño del punto se multiplica por 10 para trabajar con pips enteros.
  • HandleLongPosition y HandleShortPosition usan máximos y mínimos de velas para emular la modificación de stops y comportamiento de salida de MT5 sin depender del historial de indicadores.

Notas de uso

  • La estrategia asume que opera con un único instrumento. Agréguela a la estrategia y configure Security/Portfolio antes de iniciar.
  • Dado que es un martingale, el riesgo crece rápidamente tras una serie de pérdidas. Ajuste BaseVolume y VolumeMultiplier cuidadosamente y pruebe con requisitos de margen realistas.
  • Las distancias de stop y take-profit se definen en puntos del instrumento. Asegúrese de que los metadatos del instrumento (PriceStep, VolumeStep, MinVolume) estén completos para que los desplazamientos y cálculos de lote coincidan con su broker.
  • La lógica de trailing actúa sobre velas terminadas. Los golpes de stop intrabar pueden ocurrir antes en operativa real dependiendo del camino del precio.
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>
/// Equity-based martingale strategy that alternates trade direction after losses.
/// Opens a short position on startup, resets volume after profitable cycles,
/// and increases exposure following drawdowns while managing fixed stops and targets.
/// </summary>
public class NevalyashkaMartingaleStrategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _moveProfitPoints;
	private readonly StrategyParam<decimal> _moveStepPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _plannedVolume;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _equityPeak;
	private bool _nextDirectionIsSell = true;
	private bool _initialOrderPlaced;

	/// <summary>
	/// Initializes <see cref="NevalyashkaMartingaleStrategy"/>.
	/// </summary>
	public NevalyashkaMartingaleStrategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 0.1m)
			.SetDisplay("Base Volume", "Initial trade volume", "Risk")
			.SetGreaterThanZero()
			;

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.1m)
			.SetDisplay("Volume Multiplier", "Multiplier applied after losses", "Risk")
			.SetGreaterThanZero()
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
			.SetDisplay("Take Profit Points", "Profit target in points", "Orders")
			.SetGreaterThanZero()
			;

		_moveProfitPoints = Param(nameof(MoveProfitPoints), 100m)
			.SetDisplay("Move Profit Points", "Profit buffer before trailing activates", "Orders")
			.SetGreaterThanZero()
			;

		_moveStepPoints = Param(nameof(MoveStepPoints), 50m)
			.SetDisplay("Move Step Points", "Extra buffer for trailing stop updates", "Orders")
			.SetGreaterThanZero()
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 400m)
			.SetDisplay("Stop Loss Points", "Initial protective distance", "Orders")
			.SetGreaterThanZero()
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for trade management", "General");
	}

	/// <summary>
	/// Base order volume.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Multiplier applied when recovering from a loss.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	/// <summary>
	/// Take profit distance in points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Minimum profit in points before the stop is tightened.
	/// </summary>
	public decimal MoveProfitPoints
	{
		get => _moveProfitPoints.Value;
		set => _moveProfitPoints.Value = value;
	}

	/// <summary>
	/// Additional margin in points required between stop adjustments.
	/// </summary>
	public decimal MoveStepPoints
	{
		get => _moveStepPoints.Value;
		set => _moveStepPoints.Value = value;
	}

	/// <summary>
	/// Initial stop loss distance in points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Candle type used to drive the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_plannedVolume = 0m;
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
		_equityPeak = 0m;
		_nextDirectionIsSell = true;
		_initialOrderPlaced = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_equityPeak = Portfolio?.CurrentValue ?? 0m;
		_plannedVolume = AdjustVolume(BaseVolume);

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		var point = GetPointValue();

		HandleOpenPosition(candle, point);

		if (Position != 0)
			return;

		var equity = Portfolio?.CurrentValue ?? 0m;

		if (!_initialOrderPlaced)
		{
			if (_plannedVolume == 0m)
			{
				_plannedVolume = AdjustVolume(BaseVolume);
				if (_plannedVolume == 0m)
					return;
			}

			if (OpenPosition(true, candle.ClosePrice, point))
			{
				_initialOrderPlaced = true;
				_nextDirectionIsSell = true;
			}

			return;
		}

		if (equity > _equityPeak)
		{
			_equityPeak = equity;
			_plannedVolume = AdjustVolume(BaseVolume);

			if (_plannedVolume == 0m)
				return;

			if (_nextDirectionIsSell)
			{
				if (OpenPosition(true, candle.ClosePrice, point))
					return;
			}
			else
			{
				if (OpenPosition(false, candle.ClosePrice, point))
					return;
			}
		}
		else
		{
			var increased = VolumeMultiplier > 0m ? AdjustVolume(_plannedVolume * VolumeMultiplier) : 0m;

			if (increased == 0m)
				return;

			_plannedVolume = increased;

			if (_nextDirectionIsSell)
			{
				if (OpenPosition(false, candle.ClosePrice, point))
					_nextDirectionIsSell = false;
			}
			else
			{
				if (OpenPosition(true, candle.ClosePrice, point))
					_nextDirectionIsSell = true;
			}
		}
	}

	private void HandleOpenPosition(ICandleMessage candle, decimal point)
	{
		if (Position > 0)
		{
			HandleLongPosition(candle, point);
		}
		else if (Position < 0)
		{
			HandleShortPosition(candle, point);
		}
	}

	private void HandleLongPosition(ICandleMessage candle, decimal point)
	{
		if (_stopPrice is not decimal currentStop || _takePrice is not decimal currentTake)
			return;

		var price = candle.ClosePrice;
		var moveThreshold = MoveProfitPoints * point;

		if (price - _entryPrice > moveThreshold)
		{
			var candidate = price - (StopLossPoints + MoveStepPoints) * point;

			if (candidate > currentStop)
			{
				var newStop = price - StopLossPoints * point;
				_stopPrice = newStop;

				if (_plannedVolume > AdjustVolume(BaseVolume) && newStop > _entryPrice)
					ReduceVolume();
			}
		}

		if (candle.LowPrice <= _stopPrice)
		{
			SellMarket();
			ResetProtection();
			return;
		}

		if (candle.HighPrice >= currentTake)
		{
			SellMarket();
			ResetProtection();
		}
	}

	private void HandleShortPosition(ICandleMessage candle, decimal point)
	{
		if (_stopPrice is not decimal currentStop || _takePrice is not decimal currentTake)
			return;

		var price = candle.ClosePrice;
		var moveThreshold = MoveProfitPoints * point;

		if (_entryPrice - price > moveThreshold)
		{
			var candidate = price + (StopLossPoints + MoveStepPoints) * point;

			if (candidate < currentStop)
			{
				var newStop = price + StopLossPoints * point;
				_stopPrice = newStop;

				if (_plannedVolume > AdjustVolume(BaseVolume) && newStop < _entryPrice)
					ReduceVolume();
			}
		}

		if (candle.HighPrice >= _stopPrice)
		{
			BuyMarket();
			ResetProtection();
			return;
		}

		if (candle.LowPrice <= currentTake)
		{
			BuyMarket();
			ResetProtection();
		}
	}

	private bool OpenPosition(bool isSell, decimal price, decimal point)
	{
		if (_plannedVolume <= 0m)
			return false;

		if (point <= 0m)
			return false;

		var stopOffset = StopLossPoints * point;
		var takeOffset = TakeProfitPoints * point;

		if (stopOffset <= 0m || takeOffset <= 0m)
			return false;

		if (isSell)
		{
			SellMarket();
			_stopPrice = price + stopOffset;
			_takePrice = price - takeOffset;
		}
		else
		{
			BuyMarket();
			_stopPrice = price - stopOffset;
			_takePrice = price + takeOffset;
		}

		_entryPrice = price;
		return true;
	}

	private void ReduceVolume()
	{
		if (VolumeMultiplier <= 0m)
			return;

		var baseVolume = AdjustVolume(BaseVolume);

		if (baseVolume == 0m)
			return;

		var reduced = AdjustVolume(_plannedVolume / VolumeMultiplier);

		if (reduced < baseVolume)
			reduced = baseVolume;

		_plannedVolume = reduced;
	}

	private void ResetProtection()
	{
		_stopPrice = null;
		_takePrice = null;
		_entryPrice = 0m;
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (Security is null)
			return volume;

		if (volume <= 0m)
			return 0m;

		var step = Security.VolumeStep ?? 0m;

		if (step > 0m)
			volume = Math.Floor(volume / step) * step;

		var min = Security.MinVolume ?? 0m;
		if (min > 0m && volume < min)
			return 0m;

		var max = Security.MaxVolume ?? 0m;
		if (max > 0m && volume > max)
			volume = max;

		return volume;
	}

	private decimal GetPointValue()
	{
		var step = Security?.PriceStep ?? 0m;

		if (step <= 0m)
			step = 1m;

		if (step <= 0m)
			return 1m;

		var digits = 0;
		var value = step;

		while (value < 1m && digits < 10)
		{
			value *= 10m;
			digits++;
		}

		if (digits == 3 || digits == 5)
			step *= 10m;

		return step;
	}
}