Ver no GitHub

Estratégia MT45

Visão geral

A estratégia MT45 é uma conversão direta do consultor especializado original do MetaTrader. Ela alterna entre posições compradas e vendidas de mercado em cada barra completada, enquanto protege cada operação com as mesmas distâncias fixas de take-profit e stop-loss usadas na implementação MQL. O dimensionamento de posição segue uma regra de recuperação estilo martingale, de modo que a próxima operação aumenta seu volume somente após um resultado perdedor.

Lógica de trading

  1. A estratégia se inscreve em uma única série de velas definida pelo parâmetro Candle Type e aguarda velas completadas para evitar ruído intrabar.
  2. Quando nenhuma posição está aberta e a ordem de entrada anterior foi completamente processada, o algoritmo envia uma ordem a mercado na direção programada para este turno (compra, depois venda, depois compra, ...).
  3. A direção alterna somente após a ordem correspondente ser executada, garantindo que a alternância coincida com o comportamento do especialista MQL onde cada operação completada muda o lado para o próximo sinal.
  4. As ordens protetoras de stop-loss e take-profit são gerenciadas automaticamente através de StartProtection, de modo que a estratégia sai do mercado quando qualquer uma das distâncias é alcançada.

Dimensionamento de posição

  • Base Volume define o tamanho de lote inicial. É restaurado após cada operação lucrativa ou de equilíbrio.
  • Após uma operação perdedora, o volume da próxima entrada é multiplicado pelo Martingale Multiplier. Se o valor escalado excedesse Max Volume, a estratégia volta ao volume base para evitar crescimento descontrolado.
  • O lucro ou perda realizado é medido comparando o preço de saída com o preço de entrada armazenado, o que reproduz a função Lot() do consultor especializado original.

Gestão de risco

  • Stop Points e Take Points são expressos em passos de preço, espelhando o multiplicador _Point que era usado no MetaTrader. A estratégia converte esses valores em distâncias de preço absolutas via PriceStep do instrumento antes de habilitar StartProtection.
  • As ordens protetoras são anexadas automaticamente a cada posição e são colocadas simetricamente tanto para operações compradas quanto vendidas.

Parâmetros

Nome Descrição Padrão
Stop Points Distância ao stop protetor em passos de preço do instrumento. 600
Take Points Distância ao alvo de take-profit em passos de preço do instrumento. 700
Base Volume Volume base usado para novas posições após ganhos. 0.01
Martingale Multiplier Multiplicador de volume aplicado após perdas. 2
Max Volume Volume máximo permitido para o escalonamento martingale. 10
Candle Type Série de velas usada para detectar a conclusão de barras (padrão: 1 minuto). 1 minuto

Notas de uso

  • Escolha o período de velas que corresponda ao período do gráfico do especialista original. A lógica opera estritamente em velas completadas.
  • A estratégia não enfileira outra entrada enquanto houver uma ordem pendente ou uma posição ativa; sempre aguarda a operação existente ser fechada por stop-loss ou take-profit.
  • Não há uma versão Python separada para esta estratégia no momento, seguindo as diretrizes do projeto.
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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Alternating long/short strategy converted from the original MT45 MQL expert.
/// </summary>
public class MT45Strategy : Strategy
{
	private readonly StrategyParam<decimal> _stopPoints;
	private readonly StrategyParam<decimal> _takePoints;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _multiplier;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<DataType> _candleType;

	private Sides _nextSide;
	private Sides? _pendingSide;
	private bool _entryPending;
	private decimal _entryPrice;
	private decimal _lastTradeVolume;
	private decimal _nextVolume;
	private decimal _prevPosition;
	private decimal _pointValue;

	/// <summary>
	/// Stop-loss distance expressed in price steps.
	/// </summary>
	public decimal StopPoints
	{
		get => _stopPoints.Value;
		set => _stopPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in price steps.
	/// </summary>
	public decimal TakePoints
	{
		get => _takePoints.Value;
		set => _takePoints.Value = value;
	}

	/// <summary>
	/// Base trading volume used after profitable trades.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

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

	/// <summary>
	/// Maximum allowed trading volume.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Candle type used to detect new bars.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public MT45Strategy()
	{
		_stopPoints = Param(nameof(StopPoints), 600m)
			.SetDisplay("Stop Points", "Distance to stop loss measured in price steps", "Risk")
			
			.SetOptimize(100m, 1500m, 50m);

		_takePoints = Param(nameof(TakePoints), 700m)
			.SetDisplay("Take Points", "Distance to take profit measured in price steps", "Risk")
			
			.SetOptimize(100m, 2000m, 50m);

		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetDisplay("Base Volume", "Initial trade volume used by the strategy", "Trading");

		_multiplier = Param(nameof(MartingaleMultiplier), 2m)
			.SetDisplay("Martingale Multiplier", "Volume multiplier applied after a losing trade", "Trading")
			
			.SetOptimize(1m, 5m, 0.5m);

		_maxVolume = Param(nameof(MaxVolume), 10m)
			.SetDisplay("Max Volume", "Upper limit for martingale scaling", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle series used to trigger new trades", "General");

		_nextSide = Sides.Buy;
		_nextVolume = BaseVolume;
		_lastTradeVolume = BaseVolume;
		Volume = BaseVolume;
	}

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

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

		_nextSide = Sides.Buy;
		_pendingSide = null;
		_entryPending = false;
		_entryPrice = 0m;
		_lastTradeVolume = BaseVolume;
		_nextVolume = BaseVolume;
		_prevPosition = 0m;
		_pointValue = 0m;
		Volume = BaseVolume;
	}

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

		_pointValue = Security?.PriceStep ?? 1m;
		Volume = BaseVolume;

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}

		StartProtection(
			takeProfit: CreatePriceUnit(TakePoints),
			stopLoss: CreatePriceUnit(StopPoints));
	}

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

		if (_entryPending || Position != 0)
			return;

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

		var side = _nextSide;
		_pendingSide = side;

		// Alternate between long and short trades every finished bar.
		if (side == Sides.Buy)
		{
			BuyMarket();
		}
		else
		{
			SellMarket();
		}

		_entryPending = true;
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Order == null || trade.Trade == null)
			return;

		var newPosition = Position;
		var previousPosition = _prevPosition;
		_prevPosition = newPosition;

		if (previousPosition == 0m && newPosition != 0m)
		{
			// Store entry price and volume for later profit calculation.
			_entryPrice = trade.Trade.Price;
			_lastTradeVolume = Math.Abs(newPosition);
			_entryPending = false;

			if (_pendingSide.HasValue)
			{
				_nextSide = Opposite(_pendingSide.Value);
				_pendingSide = null;
			}
		}
		else if (previousPosition != 0m && newPosition == 0m)
		{
			// Position closed: evaluate the result and adjust the next volume.
			var direction = previousPosition > 0m ? Sides.Buy : Sides.Sell;
			UpdateNextVolume(direction, trade.Trade.Price, Math.Abs(previousPosition));
			_entryPrice = 0m;
			_entryPending = false;
		}
	}

	private void UpdateNextVolume(Sides direction, decimal exitPrice, decimal volume)
	{
		if (volume <= 0m)
			return;

		var profit = direction == Sides.Buy
			? (exitPrice - _entryPrice) * volume
			: (_entryPrice - exitPrice) * volume;

		if (profit < 0m)
		{
			var scaled = _lastTradeVolume * MartingaleMultiplier;
			_nextVolume = scaled > MaxVolume ? BaseVolume : scaled;
		}
		else
		{
			_nextVolume = BaseVolume;
		}

		Volume = _nextVolume;
	}

	private Unit CreatePriceUnit(decimal points)
	{
		if (points <= 0m || _pointValue <= 0m)
			return default;

		return new Unit(points * _pointValue, UnitTypes.Absolute);
	}

	private static Sides Opposite(Sides side)
	{
		return side == Sides.Buy ? Sides.Sell : Sides.Buy;
	}
}