Ver no GitHub

Estratégia Morse Code

Visão geral

A Estratégia Morse Code replica o expert original do MetaTrader 5 que trata cada candle concluído como um "traço" ou um "ponto". Um candle de alta (preço de fechamento maior ou igual à abertura) é codificado como 1, enquanto um candle de baixa (preço de fechamento menor ou igual à abertura) é codificado como 0. A estratégia escaneia a última sequência de candles concluídos e a compara com uma máscara binária selecionada pelo usuário. Quando os últimos candles coincidem exatamente com a sequência configurada, a estratégia abre uma posição na direção escolhida e imediatamente anexa tanto uma ordem de take-profit quanto uma de stop-loss expressadas em pips.

A implementação depende exclusivamente de APIs de alto nível do StockSharp: as assinaturas de candles fornecem dados, o binding cuida da entrega de eventos, e o módulo de proteção integrado gerencia as saídas. Nenhuma coleção personalizada ou acesso direto a valores de indicadores é necessário, mantendo a lógica concisa e robusta.

Lógica do padrão

  • Os candles são avaliados apenas após estarem completamente fechados (CandleStates.Finished).
  • Cada candle se torna um dígito binário:
    • 1 – o candle é de alta ou neutro (Close >= Open).
    • 0 – o candle é de baixa ou neutro (Close <= Open). Candles doji correspondem a ambos os dígitos, exatamente como no expert original.
  • A máscara é selecionada da enumeração MorsePatternMasks. Contém cada sequência binária de comprimento 1 a 5 que apareceu na versão MT5 (por exemplo 000, 1011, 11111).
  • A estratégia mantém uma janela deslizante dos candles mais recentes. Quando a janela mais nova corresponde à máscara, o sinal de entrada é disparado.

Este comportamento espelha a implementação MT5 que chamava CopyRates e comparava cada barra com a string do padrão caractere por caractere.

Fluxo de trading

  1. Assinar o tipo de candle configurado e aguardar até que barras suficientes sejam acumuladas para cobrir o comprimento da máscara.
  2. Para cada candle concluído:
    • Atualizar as máscaras internas que classificam o candle como de alta, de baixa ou neutro.
    • Ignorar verificações adicionais até que pelo menos tantos candles tenham sido processados quanto a máscara exige.
    • Se os últimos candles coincidem exatamente com a máscara selecionada, verificar a direção desejada.
    • Enviar uma ordem de mercado na direção do sinal (BuyMarket ou SellMarket). Quando existe uma posição oposta, a estratégia primeiro a fecha aumentando o volume da ordem, reproduzindo o comportamento do consultor especialista original.
  3. StartProtection anexa imediatamente um stop-loss e um take-profit expressados em unidades de preço. As ordens protetoras são gerenciadas pelo motor do StockSharp usando saídas de mercado para evitar llenados perdidos.

Parâmetros

Nome Padrão Descrição
CandleType Candles de 5 minutos (TimeSpan.FromMinutes(5).TimeFrame()) Tipo de dados usado para construir a sequência Morse.
Pattern _0 ("0") Máscara binária para comparar com os candles mais recentes. Os valores vêm diretamente de MorsePatternMasks.
Direction Sides.Buy Se abrir uma posição comprada (Buy) ou vendida (Sell) quando o padrão aparece.
TakeProfitPips 50 Distância da entrada ao take-profit em pips. A estratégia se adapta automaticamente a cotações forex de 3 e 5 decimais multiplicando o passo de preço por dez.
StopLossPips 50 Distância da entrada ao stop-loss em pips, usando o mesmo cálculo de pips acima.
Volume (propriedade da estratégia) definido pelo usuário Tamanho da ordem em lotes/contratos, equivalente a InpLot no expert MT5.

Todos os parâmetros suportam a janela de parâmetros do StockSharp, podem ser otimizados e podem ser alterados antes de iniciar a estratégia.

Gestão de risco

  • StartProtection anexa ambos os alvos usando offsets baseados em preço derivados das configurações de pips. As saídas são executadas com ordens de mercado para que o comportamento corresponda à classe de trade MT5 que definia valores de stop-loss e take-profit na entrada da posição.
  • Como a estratégia não faz pirâmide, uma nova operação é ignorada enquanto existe uma posição na mesma direção. Quando o padrão aparece enquanto mantém a direção oposta, o volume é automaticamente aumentado para inverter a posição.
  • O log padrão do StockSharp reporta cada entrada no diário da estratégia.

Notas de uso

  • As máscaras binárias são intencionalmente curtas (até cinco candles) para manter a lógica fiel à ideia original. Considere combinar múltiplas máscaras de padrão através da orquestração de portfólio se um vocabulário mais rico for necessário.
  • A conversão de pips depende do passo de preço do instrumento. Para símbolos exóticos com incrementos não padrão, você pode ajustar TakeProfitPips e StopLossPips manualmente.
  • A estratégia não filtra por hora do dia ou volatilidade. Você pode envolvê-la dentro de uma estratégia pai que lida com sessões ou indicadores adicionais se necessário.
  • Ao testar, certifique-se de que a propriedade Volume corresponda ao tamanho de lote esperado. O testador do StockSharp reutilizará as mesmas proteções e fluxo de ordens que o modo ao vivo.

Referência de padrões

Exemplos de valores de enumeração:

  • _0"0" (candle de baixa individual)
  • _5"11" (dois candles de alta consecutivos)
  • _20"0110" (sequência baixa-alta formando um zig-zag)
  • _33"00011" (três candles de baixa seguidos de dois de alta)
  • _61"11111" (cinco candles de alta consecutivos)

Qualquer uma das 62 máscaras pode ser selecionada no painel de parâmetros para reproduzir exatamente a assinatura de código Morse exigida pelo plano de trading.

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>
/// Strategy that trades when a selected Morse code candle pattern appears.
/// </summary>
public class MorseCodeStrategy : Strategy
{
	/// <summary>
	/// Available Morse code style patterns where '1' is bullish and '0' is bearish.
	/// </summary>
	public enum MorsePatternMasks
	{
		_0 = 0,
		_1 = 1,
		_2 = 2,
		_3 = 3,
		_4 = 4,
		_5 = 5,
		_6 = 6,
		_7 = 7,
		_8 = 8,
		_9 = 9,
		_10 = 10,
		_11 = 11,
		_12 = 12,
		_13 = 13,
		_14 = 14,
		_15 = 15,
		_16 = 16,
		_17 = 17,
		_18 = 18,
		_19 = 19,
		_20 = 20,
		_21 = 21,
		_22 = 22,
		_23 = 23,
		_24 = 24,
		_25 = 25,
		_26 = 26,
		_27 = 27,
		_28 = 28,
		_29 = 29,
		_30 = 30,
		_31 = 31,
		_32 = 32,
		_33 = 33,
		_34 = 34,
		_35 = 35,
		_36 = 36,
		_37 = 37,
		_38 = 38,
		_39 = 39,
		_40 = 40,
		_41 = 41,
		_42 = 42,
		_43 = 43,
		_44 = 44,
		_45 = 45,
		_46 = 46,
		_47 = 47,
		_48 = 48,
		_49 = 49,
		_50 = 50,
		_51 = 51,
		_52 = 52,
		_53 = 53,
		_54 = 54,
		_55 = 55,
		_56 = 56,
		_57 = 57,
		_58 = 58,
		_59 = 59,
		_60 = 60,
		_61 = 61
	}

	private static readonly string[] PatternValues = new[]
	{
		"0",
		"1",
		"00",
		"01",
		"10",
		"11",
		"000",
		"001",
		"010",
		"011",
		"100",
		"101",
		"110",
		"111",
		"0000",
		"0001",
		"0010",
		"0011",
		"0100",
		"0101",
		"0110",
		"0111",
		"1000",
		"1001",
		"1010",
		"1011",
		"1100",
		"1101",
		"1110",
		"1111",
		"00000",
		"00000",
		"00010",
		"00011",
		"00100",
		"00101",
		"00111",
		"00111",
		"01000",
		"01001",
		"01010",
		"01011",
		"01100",
		"01101",
		"01110",
		"01111",
		"10000",
		"10001",
		"10010",
		"10011",
		"10100",
		"10101",
		"10110",
		"10111",
		"11000",
		"11001",
		"11010",
		"11011",
		"11100",
		"11101",
		"11110",
		"11111"
	};

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<MorsePatternMasks> _patternMask;
	private readonly StrategyParam<Sides> _direction;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;

	private string _patternText = string.Empty;
	private int _patternLength;
	private int _maskLimit;
	private int _bullMask;
	private int _bearMask;
	private int _processedBars;
	private decimal _pipSize;
	private decimal _takeProfitDistance;
	private decimal _stopLossDistance;

	/// <summary>
	/// Initializes a new instance of the <see cref="MorseCodeStrategy"/> class.
	/// </summary>
	public MorseCodeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for candle analysis", "General");

		_patternMask = Param(nameof(Pattern), MorsePatternMasks._14)
			.SetDisplay("Pattern", "Morse code pattern where 1= bullish and 0 = bearish", "Pattern");

		_direction = Param(nameof(Direction), Sides.Buy)
			.SetDisplay("Direction", "Side to trade when the pattern appears", "Trading");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Distance from entry to take profit in pips", "Risk Management");

		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Distance from entry to stop loss in pips", "Risk Management");
	}

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

	/// <summary>
	/// Selected Morse code pattern.
	/// </summary>
	public MorsePatternMasks Pattern
	{
		get => _patternMask.Value;
		set => _patternMask.Value = value;
	}

	/// <summary>
	/// Trade direction used when the pattern is detected.
	/// </summary>
	public Sides Direction
	{
		get => _direction.Value;
		set => _direction.Value = value;
	}

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

	/// <summary>
	/// Stop loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

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

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

		_patternText = string.Empty;
		_patternLength = 0;
		_maskLimit = 0;
		_bullMask = 0;
		_bearMask = 0;
		_processedBars = 0;
		_pipSize = 0m;
		_takeProfitDistance = 0m;
		_stopLossDistance = 0m;
	}

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

		_patternText = GetPatternText(Pattern);
		_patternLength = _patternText.Length;
		if (_patternLength == 0)
			throw new InvalidOperationException("Pattern cannot be empty.");

		_maskLimit = (1 << _patternLength) - 1;
		_bullMask = 0;
		_bearMask = 0;
		_processedBars = 0;

		_pipSize = CalculatePipSize();
		_takeProfitDistance = TakeProfitPips * _pipSize;
		_stopLossDistance = StopLossPips * _pipSize;

		// Configure automatic take profit and stop loss handling
		StartProtection(
			takeProfit: new Unit(_takeProfitDistance, UnitTypes.Absolute),
			stopLoss: new Unit(_stopLossDistance, UnitTypes.Absolute),
			useMarketOrders: true);

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

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Only act on completed candles
		if (candle.State != CandleStates.Finished)
			return;

		UpdatePatternMasks(candle);

		// Wait until enough candles were processed to match the pattern
		if (_processedBars < _patternLength)
			return;

		if (!IsPatternMatched())
			return;

		var closePrice = candle.ClosePrice;

		if (Direction == Sides.Buy)
		{
			if (Position > 0m)
				return; // Already in a long position

			EnterLong(closePrice);
		}
		else
		{
			if (Position < 0m)
				return; // Already in a short position

			EnterShort(closePrice);
		}
	}

	private static string GetPatternText(MorsePatternMasks mask)
	{
		var index = (int)mask;
		if (index < 0 || index >= PatternValues.Length)
			throw new ArgumentOutOfRangeException(nameof(mask));

		return PatternValues[index];
	}

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

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

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

		return step;
	}

	private void UpdatePatternMasks(ICandleMessage candle)
	{
		if (_patternLength == 0)
			return;

		var strictBull = candle.ClosePrice > candle.OpenPrice;
		var strictBear = candle.ClosePrice < candle.OpenPrice;

		_bullMask = ((_bullMask << 1) | (strictBull ? 1 : 0)) & _maskLimit;
		_bearMask = ((_bearMask << 1) | (strictBear ? 1 : 0)) & _maskLimit;

		if (_processedBars < _patternLength)
			_processedBars++;
	}

	private bool IsPatternMatched()
	{
		for (var i = 0; i < _patternLength; i++)
		{
			var expected = _patternText[i];
			var isStrictBull = ((_bullMask >> i) & 1) == 1;
			var isStrictBear = ((_bearMask >> i) & 1) == 1;

			if (expected == '1')
			{
				if (isStrictBear)
					return false; // Pattern expects bullish or neutral candle
			}
			else
			{
				if (isStrictBull)
					return false; // Pattern expects bearish or neutral candle
			}
		}

		return true;
	}

	private void EnterLong(decimal price)
	{
		BuyMarket();
		LogInfo($"Entered long position at price {price}.");
	}

	private void EnterShort(decimal price)
	{
		SellMarket();
		LogInfo($"Entered short position at price {price}.");
	}
}