Ver no GitHub

Estratégia New Martin

Visão Geral

A Estratégia New Martin replica o assessor especialista do MetaTrader "New Martin" executando uma cobertura martingale simétrica em ambos os lados do mercado. A estratégia mantém uma posição comprada e vendida inicial abertas o tempo todo e reequilibra a cobertura quando as médias móveis suavizadas rápida e lenta (SMMA) se cruzam. Quando um lado da cobertura está perdendo, o algoritmo multiplica a exposição nesse lado e simultaneamente realiza ganhos na perna mais lucrativa. As saídas de take profit reciclam a cobertura reabrindo o lado ausente e opcionalmente purgando tanto os melhores quanto os piores desempenhos para manter a grade compacta.

A implementação tem como alvo a API de alto nível do StockSharp e espera um portfólio com suporte a hedge para que as pernas comprada e vendida possam coexistir. As ordens são enviadas como ordens de mercado por simplicidade, refletindo a lógica MQL original onde os preenchimentos são assumidos como imediatos.

Indicadores e Sinais

  • SMMA rápida (comprimento padrão 5): rastreia a direção de preço de curto prazo.
  • SMMA lenta (comprimento padrão 20): representa a tendência dominante.
  • Detecção de cruzamento: um cruzamento das duas barras completadas anteriores aciona a adição martingale na perna de pior desempenho. O sinal é limitado a uma vez por vela armazenando o horário de abertura da vela do último cruzamento.

Gestão de Posições

  • Hedge inicial: assim que os indicadores se formam, a estratégia abre uma posição comprada e uma vendida com o volume inicial configurado. Ambas as negociações usam uma distância de take profit simétrica em pips.
  • Reciclagem de take profit: quando o preço toca o nível de take profit de qualquer perna, a estratégia fecha essa posição, registra o evento e opcionalmente fecha tanto as posições mais lucrativas quanto as mais perdedoras para realizar ganhos e perdas em pares. Os lados ausentes são imediatamente reabertos com o volume base para que o hedge permaneça equilibrado.
  • Médio martingale: em cada cruzamento de SMMA, o algoritmo identifica a posição com o menor lucro não realizado. Ele aumenta a exposição naquele lado multiplicando o volume da negociação pelo multiplicador martingale (padrão 1.6) após ajuste ao passo de volume do instrumento. A posição aberta mais lucrativa é fechada logo após a negociação de médio para liberar o lucro bloqueado.

Gestão de Risco

  • Proteção contra queda de capital: o maior patrimônio de portfólio observado é rastreado. Se a queda desse pico exceder a porcentagem configurada, todas as posições abertas são liquidadas e a inicialização do hedge é adiada até a próxima vela.
  • Volume base dinâmico: quando o capital cresce pelo menos pelo multiplicador martingale em relação ao saldo registrado anteriormente, o volume base do hedge é aumentado pelo mesmo multiplicador (respeitando também os limites de volume do exchange). Isso replica o comportamento do EA original onde os lucros são reinvestidos para escalar a grade.
  • Normalização de volume: cada volume solicitado é arredondado para baixo ao passo de volume do exchange e limitado entre o volume mínimo e máximo do instrumento para evitar rejeições de ordens.

Parâmetros

  • Take Profit (pips): distância do preço de entrada para colocar o alvo de take profit para cada perna. Padrão 50 pips.
  • Initial Volume: volume base por lado do hedge. Padrão 0.1 contratos.
  • Slow MA / Fast MA: comprimentos dos indicadores SMMA lento e rápido (padrões 20 e 5). O período lento deve permanecer maior que o período rápido.
  • Equity DD %: queda máxima permitida do pico de capital antes de todas as posições serem fechadas. Padrão 12%.
  • Multiplier: fator martingale usado para médio abaixo e para escalar o volume base após crescimento de capital importante. Padrão 1.6.
  • Candle Type: período das velas usadas para cálculos. Padrão velas de 15 minutos, mas pode ser alterado para corresponder ao período do gráfico do EA original.

Notas

  • A estratégia requer contas habilitadas para hedge porque mantém posições compradas e vendidas abertas simultaneamente.
  • Ordens de mercado são usadas para entradas e saídas, assim como o especialista MQL que dependia de preenchimentos instantâneos. Adapte a lógica de ordens se controle de slippage for necessário.
  • Certifique-se de que os metadados do instrumento (passo de preço, passo de volume, volume mínimo/máximo) estejam corretamente configurados para que a normalização de volume funcione conforme esperado.
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>
/// Hedged martingale strategy that scales positions on moving average crossovers.
/// </summary>
public class NewMartinStrategy : Strategy
{
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<decimal> _lossPercent;
	private readonly StrategyParam<decimal> _multiplier;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<PositionEntry> _longPositions = new();
	private readonly List<PositionEntry> _shortPositions = new();

	private SmoothedMovingAverage _slowMa;
	private SmoothedMovingAverage _fastMa;

	private decimal? _slowPrev1;
	private decimal? _slowPrev2;
	private decimal? _fastPrev1;
	private decimal? _fastPrev2;

	private decimal _currentVolume;
	private decimal _pipSize;
	private decimal _startBalance;
	private decimal _peakBalance;
	private DateTimeOffset? _lastCrossTime;
	private bool _positionsInitialized;

	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Initial hedge volume per side.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Period of the slow smoothed moving average.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Period of the fast smoothed moving average.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Maximum equity drawdown percentage before all positions are liquidated.
	/// </summary>
	public decimal LossPercent
	{
		get => _lossPercent.Value;
		set => _lossPercent.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the martingale additions and base volume growth.
	/// </summary>
	public decimal Multiplier
	{
		get => _multiplier.Value;
		set => _multiplier.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of <see cref="NewMartinStrategy"/>.
	/// </summary>
	public NewMartinStrategy()
	{
		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
		.SetGreaterThanZero()
		.SetDisplay("Take Profit", "Target distance in pips", "Risk")
		
		.SetOptimize(10m, 200m, 10m);

		_initialVolume = Param(nameof(InitialVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Initial Volume", "Volume per hedge side", "Trading")
		
		.SetOptimize(0.01m, 1m, 0.01m);

		_slowPeriod = Param(nameof(SlowPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("Slow MA", "Slow smoothed MA period", "Indicators")
		
		.SetOptimize(10, 80, 5);

		_fastPeriod = Param(nameof(FastPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("Fast MA", "Fast smoothed MA period", "Indicators")
		
		.SetOptimize(2, 20, 1);

		_lossPercent = Param(nameof(LossPercent), 12m)
		.SetGreaterThanZero()
		.SetDisplay("Equity DD %", "Maximum drawdown before reset", "Risk")
		
		.SetOptimize(5m, 30m, 1m);

		_multiplier = Param(nameof(Multiplier), 1.6m)
		.SetGreaterThanZero()
		.SetDisplay("Multiplier", "Martingale growth factor", "Trading")
		
		.SetOptimize(1.1m, 3m, 0.1m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Time frame for calculations", "General");
	}

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

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

		_longPositions.Clear();
		_shortPositions.Clear();
		_slowPrev1 = null;
		_slowPrev2 = null;
		_fastPrev1 = null;
		_fastPrev2 = null;
		_currentVolume = 0m;
		_pipSize = 0m;
		_startBalance = 0m;
		_peakBalance = 0m;
		_lastCrossTime = null;
		_positionsInitialized = false;
	}

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

		if (SlowPeriod <= FastPeriod)
			throw new InvalidOperationException("Slow period must be greater than fast period.");

		_currentVolume = AdjustVolume(InitialVolume);

		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
		{
			_pipSize = 1m;
		}
		else
		{
			_pipSize = step;
			var decimals = Security?.Decimals ?? 0;
			if (decimals == 3 || decimals == 5)
				_pipSize = step * 10m;
		}

		_slowMa = new SmoothedMovingAverage { Length = SlowPeriod };
		_fastMa = new SmoothedMovingAverage { Length = FastPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_slowMa, _fastMa, ProcessCandle)
		.Start();

		_startBalance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		_peakBalance = _startBalance;
	}

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

		if (!_slowMa.IsFormed || !_fastMa.IsFormed)
		{
			UpdateAverageHistory(slow, fast);
			return;
		}

		UpdateAccountMetrics();

		if (ShouldCloseAllPositions())
		{
			CloseAllPositions();
			_positionsInitialized = false;
		}

		if (!_positionsInitialized)
		{
			InitializeHedge(candle.ClosePrice);
		}

		var tpTriggered = CheckTakeProfits(candle);

		if (tpTriggered)
		{
			CloseExtremePositions(candle.ClosePrice);
		}

		if (_longPositions.Count == 0)
			OpenPosition(Sides.Buy, _currentVolume, candle.ClosePrice);

		if (_shortPositions.Count == 0)
			OpenPosition(Sides.Sell, _currentVolume, candle.ClosePrice);

		HandleCrossing(candle, slow, fast);

		UpdateAverageHistory(slow, fast);
	}

	private void InitializeHedge(decimal price)
	{
		if (_currentVolume <= 0m)
			return;

		// Start with symmetric hedge on both sides.
		OpenPosition(Sides.Buy, _currentVolume, price);
		OpenPosition(Sides.Sell, _currentVolume, price);
		_positionsInitialized = _longPositions.Count > 0 && _shortPositions.Count > 0;
	}

	private void UpdateAccountMetrics()
	{
		var equity = Portfolio?.CurrentValue ?? 0m;

		if (equity > _peakBalance)
			_peakBalance = equity;

		if (_startBalance > 0m && equity >= _startBalance * Multiplier)
		{
			_startBalance = equity;

			var newVolume = AdjustVolume(_currentVolume * Multiplier);
			if (newVolume > 0m)
				_currentVolume = newVolume;
		}
	}

	private bool ShouldCloseAllPositions()
	{
		if (_peakBalance <= 0m)
			return false;

		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
			return false;

		var drawdown = (_peakBalance - equity) / _peakBalance * 100m;
		return drawdown >= LossPercent;
	}

	private bool CheckTakeProfits(ICandleMessage candle)
	{
		var triggered = false;
		var offset = TakeProfitPips * _pipSize;
		if (offset <= 0m)
			return false;

		foreach (var entry in _longPositions.ToArray())
		{
			if (entry is null)
				continue;

			if (candle.HighPrice >= entry.TakeProfit)
			{
				CloseEntry(entry);
				triggered = true;
			}
		}

		foreach (var entry in _shortPositions.ToArray())
		{
			if (entry is null)
				continue;

			if (candle.LowPrice <= entry.TakeProfit)
			{
				CloseEntry(entry);
				triggered = true;
			}
		}

		return triggered;
	}

	private void CloseExtremePositions(decimal price)
	{
		var (lossEntry, lossValue, profitEntry, profitValue) = GetExtremePositions(price);

		if (lossEntry is not null && lossValue < 0m)
			CloseEntry(lossEntry);

		if (profitEntry is not null && profitEntry != lossEntry)
			CloseEntry(profitEntry);
	}

	private void HandleCrossing(ICandleMessage candle, decimal slow, decimal fast)
	{
		if (!_slowPrev2.HasValue || !_slowPrev1.HasValue || !_fastPrev2.HasValue || !_fastPrev1.HasValue)
			return;

		var crossDetected = (_slowPrev2.Value > _fastPrev2.Value && _slowPrev1.Value < _fastPrev1.Value)
		|| (_slowPrev2.Value < _fastPrev2.Value && _slowPrev1.Value > _fastPrev1.Value);

		if (!crossDetected)
			return;

		if (_lastCrossTime == candle.OpenTime)
			return;

		_lastCrossTime = candle.OpenTime;

		var (lossEntry, _, profitEntry, _) = GetExtremePositions(candle.ClosePrice);
		if (lossEntry is null)
			return;

		var volume = AdjustVolume(lossEntry.Volume * Multiplier);
		if (volume <= 0m)
			return;

		// Average down on the weakest side.
		OpenPosition(lossEntry.Side, volume, candle.ClosePrice);

		if (profitEntry is not null && profitEntry != lossEntry)
		{
			// Lock in profit on the strongest position after the new hedge.
			CloseEntry(profitEntry);
		}
	}

	private void UpdateAverageHistory(decimal slow, decimal fast)
	{
		_slowPrev2 = _slowPrev1;
		_slowPrev1 = slow;
		_fastPrev2 = _fastPrev1;
		_fastPrev1 = fast;
	}

	private void OpenPosition(Sides side, decimal requestedVolume, decimal price)
	{
		var volume = AdjustVolume(requestedVolume);
		if (volume <= 0m)
			return;

		var offset = TakeProfitPips * _pipSize;
		if (offset <= 0m)
			return;

		var takeProfit = side == Sides.Buy ? price + offset : price - offset;
		var entry = new PositionEntry(side, volume, price, takeProfit);

		if (side == Sides.Buy)
		{
			_longPositions.Add(entry);
			BuyMarket(volume);
		}
		else
		{
			_shortPositions.Add(entry);
			SellMarket(volume);
		}
	}

	private void CloseAllPositions()
	{
		foreach (var entry in _longPositions.ToArray())
			CloseEntry(entry);

		foreach (var entry in _shortPositions.ToArray())
			CloseEntry(entry);
	}

	private void CloseEntry(PositionEntry entry)
	{
		if (entry.Side == Sides.Buy)
		{
			SellMarket(entry.Volume);

			for (var i = _longPositions.Count - 1; i >= 0; i--)
			{
				if (_longPositions[i] == entry)
				{
					_longPositions.RemoveAt(i);
					break;
				}
			}
		}
		else
		{
			BuyMarket(entry.Volume);

			for (var i = _shortPositions.Count - 1; i >= 0; i--)
			{
				if (_shortPositions[i] == entry)
				{
					_shortPositions.RemoveAt(i);
					break;
				}
			}
		}
	}

	private (PositionEntry lossEntry, decimal lossValue, PositionEntry profitEntry, decimal profitValue) GetExtremePositions(decimal price)
	{
		PositionEntry lossEntry = null;
		PositionEntry profitEntry = null;
		var lossValue = 0m;
		var profitValue = 0m;

		foreach (var entry in _longPositions)
		{
			var pnl = (price - entry.EntryPrice) * entry.Volume;
			if (lossEntry is null || pnl < lossValue)
			{
				lossEntry = entry;
				lossValue = pnl;
			}

			if (profitEntry is null || pnl > profitValue)
			{
				profitEntry = entry;
				profitValue = pnl;
			}
		}

		foreach (var entry in _shortPositions)
		{
			var pnl = (entry.EntryPrice - price) * entry.Volume;
			if (lossEntry is null || pnl < lossValue)
			{
				lossEntry = entry;
				lossValue = pnl;
			}

			if (profitEntry is null || pnl > profitValue)
			{
				profitEntry = entry;
				profitValue = pnl;
			}
		}

		return (lossEntry, lossValue, profitEntry, profitValue);
	}

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

		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 sealed record PositionEntry(Sides Side, decimal Volume, decimal EntryPrice, decimal TakeProfit);
}