Ver en GitHub

Estrategia Morse Code

Descripción general

La Estrategia Morse Code replica el experto original de MetaTrader 5 que trata cada vela completada como un "guión" o un "punto". Una vela alcista (precio de cierre mayor o igual a la apertura) se codifica como 1, mientras que una vela bajista (precio de cierre menor o igual a la apertura) se codifica como 0. La estrategia escanea la última secuencia de velas completadas y la compara con una máscara binaria seleccionada por el usuario. Cuando las últimas velas coinciden exactamente con la secuencia configurada, la estrategia abre una posición en la dirección elegida e inmediatamente adjunta tanto una orden de take-profit como una de stop-loss expresadas en pips.

La implementación se basa exclusivamente en APIs de alto nivel de StockSharp: las suscripciones de velas proporcionan datos, el binding maneja la entrega de eventos, y el módulo de protección integrado gestiona las salidas. No se requieren colecciones personalizadas ni acceso directo a valores de indicadores, manteniendo la lógica concisa y robusta.

Lógica del patrón

  • Las velas se evalúan solo después de que están completamente cerradas (CandleStates.Finished).
  • Cada vela se convierte en un dígito binario:
    • 1 – la vela es alcista o neutral (Close >= Open).
    • 0 – la vela es bajista o neutral (Close <= Open). Las velas doji coinciden con ambos dígitos, exactamente como en el experto original.
  • La máscara se selecciona de la enumeración MorsePatternMasks. Contiene cada secuencia binaria de longitud 1 a 5 que apareció en la versión MT5 (por ejemplo 000, 1011, 11111).
  • La estrategia mantiene una ventana deslizante de las velas más recientes. Cuando la ventana más nueva coincide con la máscara, se dispara la señal de entrada.

Este comportamiento refleja la implementación MT5 que llamaba a CopyRates y comparaba cada barra con la cadena del patrón carácter por carácter.

Flujo de trading

  1. Suscribirse al tipo de vela configurado y esperar hasta que se acumulen suficientes barras para cubrir la longitud de la máscara.
  2. Para cada vela completada:
    • Actualizar las máscaras internas que clasifican la vela como alcista, bajista o neutral.
    • Ignorar las comprobaciones adicionales hasta que se hayan procesado al menos tantas velas como requiere la máscara.
    • Si las últimas velas coinciden exactamente con la máscara seleccionada, verificar la dirección deseada.
    • Enviar una orden de mercado en la dirección de la señal (BuyMarket o SellMarket). Cuando existe una posición opuesta, la estrategia primero la cierra aumentando el volumen de la orden, reproduciendo el comportamiento del asesor experto original.
  3. StartProtection adjunta inmediatamente un stop-loss y un take-profit expresados en unidades de precio. Las órdenes protectoras son manejadas por el motor StockSharp usando salidas de mercado para evitar llenados perdidos.

Parámetros

Nombre Valor predeterminado Descripción
CandleType Velas de 5 minutos (TimeSpan.FromMinutes(5).TimeFrame()) Tipo de datos usado para construir la secuencia Morse.
Pattern _0 ("0") Máscara binaria para comparar con las velas más recientes. Los valores provienen directamente de MorsePatternMasks.
Direction Sides.Buy Si abrir una posición larga (Buy) o corta (Sell) cuando aparece el patrón.
TakeProfitPips 50 Distancia desde la entrada al take-profit en pips. La estrategia se adapta automáticamente a las cotizaciones forex de 3 y 5 decimales multiplicando el paso de precio por diez.
StopLossPips 50 Distancia desde la entrada al stop-loss en pips, usando el mismo cálculo de pips anterior.
Volume (propiedad de estrategia) definido por usuario Tamaño de la orden en lotes/contratos, equivalente a InpLot en el experto MT5.

Todos los parámetros admiten la ventana de parámetros de StockSharp, pueden optimizarse y pueden cambiarse antes de iniciar la estrategia.

Gestión de riesgos

  • StartProtection adjunta ambos objetivos usando desplazamientos basados en precio derivados de la configuración de pips. Las salidas se ejecutan con órdenes de mercado para que el comportamiento coincida con la clase de trading MT5 que establecía valores de stop-loss y take-profit al abrir la posición.
  • Dado que la estrategia no hace piramidación, una nueva operación se ignora mientras existe una posición en la misma dirección. Cuando el patrón aparece mientras se mantiene la dirección opuesta, el volumen se aumenta automáticamente para invertir la posición.
  • El registro estándar de StockSharp reporta cada entrada al diario de la estrategia.

Notas de uso

  • Las máscaras binarias son intencionalmente cortas (hasta cinco velas) para mantener la lógica fiel a la idea original. Considere combinar múltiples máscaras de patrón a través de la orquestación de cartera si se necesita un vocabulario más rico.
  • La conversión de pips depende del paso de precio del instrumento. Para símbolos exóticos con incrementos no estándar puede ajustar TakeProfitPips y StopLossPips manualmente.
  • La estrategia no filtra por hora del día o volatilidad. Puede envolverla dentro de una estrategia padre que maneje sesiones o indicadores adicionales si es necesario.
  • Al probar, asegúrese de que la propiedad Volume coincida con el tamaño de lote esperado. El probador de StockSharp reutilizará las mismas protecciones y flujo de órdenes que el modo en vivo.

Referencia de patrones

Ejemplos de valores de enumeración:

  • _0"0" (vela bajista individual)
  • _5"11" (dos velas alcistas consecutivas)
  • _20"0110" (secuencia bajista-alcista formando un zig-zag)
  • _33"00011" (tres velas bajistas seguidas de dos alcistas)
  • _61"11111" (cinco velas alcistas consecutivas)

Cualquiera de las 62 máscaras puede seleccionarse desde el panel de parámetros para reproducir exactamente la firma de código Morse requerida por el plan 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}.");
	}
}