Ver no GitHub

Estratégias cruzadas de preços médios móveis

Visão geral

Este pacote contém duas portas de estratégia C# dos MetaTrader 5 exemplos localizados em MQL/50198:

  • MovingAveragePriceCrossStrategy – um sistema minimalista de média móvel versus cruzamento de preços que negocia uma única posição por vez.
  • MovingAverageMartingaleStrategy – uma versão aprimorada que aplica o dimensionamento de posição no estilo martingale após perdas, preservando a mesma lógica de cruzamento de preço/média.

Ambas as implementações dependem do StockSharp API de alto nível, usam assinaturas de velas para avaliação de sinal e expõem parâmetros compatíveis com MetaTrader para distâncias de stop-loss e take-profit.

Arquivos

Arquivo Descrição
CS/MovingAveragePriceCrossStrategy.cs Cruzamento de preço base/MA usando volume fixo e ordens de proteção estáticas.
CS/MovingAverageMartingaleStrategy.cs Variante Martingale que aumenta o volume e as distâncias de proteção após perder negociações.

Lógica de negociação

MovingAveragePriceCrossEstratégia

  1. Assina velas do período configurado e calcula uma média móvel simples (SMA).
  2. Avalia sinais apenas em velas finalizadas para imitar o comportamento do especialista MT5.
  3. Detecta cruzamentos entre SMA e o preço de fechamento da vela usando as duas últimas velas concluídas:
    • Venda quando a média móvel subir acima do fechamento da vela (preço ultrapassado abaixo da média).
    • Compre quando a média móvel cair abaixo do fechamento da vela (preço cruzado acima da média).
  4. Coloca uma única ordem de mercado por sinal se nenhuma posição estiver aberta no momento.
  5. Aplica proteção automática via StartProtection com distâncias de MetaTrader pontos convertidas em compensações de preço absoluto.

MovingAverageMartingaleEstratégia

  1. Compartilha a mesma assinatura de vela e geração de sinal SMA que a estratégia base.
  2. Rastreia o PnL realizado após cada posição fechada e armazena o último resultado da negociação.
  3. Quando um novo sinal de cruzamento aparece e nenhuma posição está aberta:
    • Se a última negociação foi prejudicadora, multiplica o próximo volume de negociação por VolumeMultiplier (limitado a MaxVolume) e aumenta as distâncias de stop-loss e take-profit em TargetMultiplier.
    • Se a última negociação foi lucrativa, redefine o volume de negociação e as distâncias de proteção para seus valores iniciais.
  4. Aplica StartProtection com as compensações ajustadas dinamicamente imediatamente antes de enviar a ordem de mercado.
  5. Continua a negociar apenas uma posição por vez, correspondendo à lógica original do Expert Advisor.

Gestão de risco

  • Os níveis de proteção são expressos em MetaTrader pontos e automaticamente traduzidos em compensações de preço absoluto usando o tamanho do pip detectado (PriceStep ajustado para símbolos FX decimais de 3/5).
  • A estratégia martingale mantém os multiplicadores de stop-loss e take-profit limitados para evitar distâncias descontroladas.
  • O volume de posição está alinhado com VolumeStep, MinVolume e MaxVolume opcional do instrumento para evitar pedidos inválidos.

Parâmetros

Entradas compartilhadas

Parâmetro Estratégia Padrão Descrição
CandleType ambos 1 minute Tipo de dados Candle usado para cálculo de sinal.
MaPeriod ambos 50 Comprimento da média móvel simples.

MovingAveragePriceCrossEstratégia

Parâmetro Padrão Descrição
OrderVolume 1 Volume do pedido alinhado à etapa do instrumento.
TakeProfitPoints 150 Distância de lucro em MetaTrader pontos (0 desativações).
StopLossPoints 150 Distância de stop-loss em MetaTrader pontos (0 desabilita).

MovingAverageMartingaleEstratégia

Parâmetro Padrão Descrição
StartingVolume 1 Volume base restaurado após negociações lucrativas.
MaxVolume 5 Volume máximo após aplicação de multiplicadores.
TakeProfitPoints 100 Distância inicial de lucro em MetaTrader pontos.
StopLossPoints 300 Distância inicial do stop-loss em MetaTrader pontos.
VolumeMultiplier 2 Fator aplicado ao próximo volume de pedido após uma perda.
TargetMultiplier 2 Fator aplicado às distâncias de stop-loss e take-profit após uma perda.

Notas de uso

  • MetaTrader “pontos” correspondem a um PriceStep para a maioria dos instrumentos; as estratégias se multiplicam automaticamente por 10 para símbolos FX de 3 ou 5 decimais para corresponder ao comportamento do MT5.
  • Ambas as estratégias requerem apenas um título e irão ignorar os sinais enquanto uma posição estiver aberta, reproduzindo a guarda PositionsTotal() dos especialistas originais.
  • Ative a otimização nos parâmetros expostos dentro do designer StockSharp para replicar o ajuste de entrada MT5.
namespace StockSharp.Samples.Strategies;

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;

using StockSharp.Algo;

/// <summary>
/// Moving average crossover strategy with martingale money management converted from the MT5 "MovingAverageMartinGale" expert advisor.
/// Scales trade volume and protective distances after losses while resetting to the base configuration after profitable trades.
/// </summary>
public class MovingAverageMartingaleStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<decimal> _startingVolume;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _targetMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private SMA _sma;
	private decimal? _previousClose;
	private decimal? _previousMa;
	private decimal? _currentClose;
	private decimal? _currentMa;
	private decimal _pipSize;

	private decimal _currentVolume;
	private decimal _currentTakeProfitPoints;
	private decimal _currentStopLossPoints;
	private decimal _lastRealizedPnL;
	private decimal _previousPosition;
	private decimal _lastTradeResult;

	/// <summary>
	/// Initializes a new instance of the <see cref="MovingAverageMartingaleStrategy"/> class.
	/// </summary>
	public MovingAverageMartingaleStrategy()
	{
		_maPeriod = Param(nameof(MaPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("MA period", "Length of the simple moving average used for entries.", "Indicator")
			;

		_startingVolume = Param(nameof(StartingVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Starting volume", "Base order volume used after profitable trades.", "Money management")
			;

		_maxVolume = Param(nameof(MaxVolume), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Maximum volume", "Upper limit for martingale scaling.", "Money management")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 100)
			.SetNotNegative()
			.SetDisplay("Take profit (points)", "Initial profit target distance expressed in MetaTrader points.", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 300)
			.SetNotNegative()
			.SetDisplay("Stop loss (points)", "Initial stop-loss distance expressed in MetaTrader points.", "Risk")
			;

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Volume multiplier", "Factor applied to the next trade volume after a loss.", "Money management")
			;

		_targetMultiplier = Param(nameof(TargetMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Target multiplier", "Factor applied to stop-loss and take-profit distances after a loss.", "Money management")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle type", "Primary timeframe processed by the strategy.", "General");
	}

	/// <summary>
	/// Moving average period used for generating signals.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Base position volume restored after profitable trades.
	/// </summary>
	public decimal StartingVolume
	{
		get => _startingVolume.Value;
		set => _startingVolume.Value = value;
	}

	/// <summary>
	/// Maximum position volume allowed by the martingale logic.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Initial take-profit distance expressed in MetaTrader points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

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

	/// <summary>
	/// Multiplier applied to the trade volume after a losing trade.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	/// <summary>
	/// Multiplier applied to stop-loss and take-profit distances after a losing trade.
	/// </summary>
	public decimal TargetMultiplier
	{
		get => _targetMultiplier.Value;
		set => _targetMultiplier.Value = value;
	}

	/// <summary>
	/// Candle type used to read market data.
	/// </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();

		_sma = null;
		_previousClose = null;
		_previousMa = null;
		_currentClose = null;
		_currentMa = null;
		_pipSize = 0m;

		_currentVolume = 0m;
		_currentTakeProfitPoints = 0m;
		_currentStopLossPoints = 0m;
		_lastRealizedPnL = 0m;
		_previousPosition = 0m;
		_lastTradeResult = 0m;
	}

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

		_pipSize = CalculatePipSize();

		_currentVolume = NormalizeVolume(StartingVolume);
		_currentTakeProfitPoints = TakeProfitPoints;
		_currentStopLossPoints = StopLossPoints;
		_lastRealizedPnL = PnL;
		_previousPosition = Position;
		_lastTradeResult = 0m;

		_sma = new SMA { Length = MaPeriod };

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

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (_previousPosition != 0m && Position == 0m)
		{
			var tradePnL = PnL - _lastRealizedPnL;
			_lastRealizedPnL = PnL;
			_lastTradeResult = tradePnL;
		}

		_previousPosition = Position;
	}

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (_currentClose is null)
		{
			_currentClose = candle.ClosePrice;
			_currentMa = maValue;
			return;
		}

		if (_previousClose is null)
		{
			_previousClose = _currentClose;
			_previousMa = _currentMa;
			_currentClose = candle.ClosePrice;
			_currentMa = maValue;
			return;
		}

		if (_sma?.IsFormed != true)
		{
			_previousClose = _currentClose;
			_previousMa = _currentMa;
			_currentClose = candle.ClosePrice;
			_currentMa = maValue;
			return;
		}

		var previousClose = _previousClose.Value;
		var previousMa = _previousMa!.Value;
		var currentClose = _currentClose.Value;
		var currentMa = _currentMa!.Value;

		var crossedBelowPrice = previousMa < previousClose && currentMa > currentClose;
		var crossedAbovePrice = previousMa > previousClose && currentMa < currentClose;

		if (Position == 0m && (crossedBelowPrice || crossedAbovePrice))
		{
			ApplyMartingaleAdjustments();
		}

		if (Position == 0m)
		{
			var volume = NormalizeVolume(_currentVolume);

			if (volume <= 0m)
			{
				ShiftBuffers(candle, maValue);
				return;
			}

			if (crossedBelowPrice)
			{
				ApplyProtection();
				SellMarket(volume);
			}
			else if (crossedAbovePrice)
			{
				ApplyProtection();
				BuyMarket(volume);
			}
		}

		ShiftBuffers(candle, maValue);
	}

	private void ApplyMartingaleAdjustments()
	{
		if (_lastTradeResult < 0m)
		{
			var nextVolume = Math.Min(_currentVolume * VolumeMultiplier, MaxVolume);
			_currentVolume = NormalizeVolume(nextVolume);

			_currentTakeProfitPoints = Math.Min(_currentTakeProfitPoints * TargetMultiplier, 100000m);
			_currentStopLossPoints = Math.Min(_currentStopLossPoints * TargetMultiplier, 100000m);
		}
		else if (_lastTradeResult > 0m)
		{
			_currentVolume = NormalizeVolume(StartingVolume);
			_currentTakeProfitPoints = TakeProfitPoints;
			_currentStopLossPoints = StopLossPoints;
		}

		_lastTradeResult = 0m;
	}

	private void ApplyProtection()
	{
		var stopDistance = _currentStopLossPoints > 0m ? _currentStopLossPoints * _pipSize : 0m;
		var takeDistance = _currentTakeProfitPoints > 0m ? _currentTakeProfitPoints * _pipSize : 0m;

		StartProtection(
			stopLoss: stopDistance > 0m ? new Unit(stopDistance, UnitTypes.Absolute) : null,
			takeProfit: takeDistance > 0m ? new Unit(takeDistance, UnitTypes.Absolute) : null);

		Volume = NormalizeVolume(_currentVolume);
	}

	private void ShiftBuffers(ICandleMessage candle, decimal maValue)
	{
		_previousClose = _currentClose;
		_previousMa = _currentMa;
		_currentClose = candle.ClosePrice;
		_currentMa = maValue;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var minVolume = Security?.MinVolume ?? step;
		if (volume < minVolume)
			volume = minVolume;

		var multiplier = volume / step;
		var rounded = Math.Round(multiplier, MidpointRounding.AwayFromZero) * step;

		if (rounded < minVolume)
			rounded = minVolume;

		var maxVolume = Security?.MaxVolume;
		if (maxVolume is decimal max && rounded > max)
			rounded = max;

		rounded = Math.Min(rounded, MaxVolume);

		return Math.Max(rounded, step);
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		var decimals = Security?.Decimals ?? 0;
		if (decimals == 3 || decimals == 5)
			step *= 10m;

		return step;
	}
}