Открыть на GitHub

Стратегия Morse Code

Обзор

Стратегия Morse Code повторяет оригинального советника MetaTrader 5, который рассматривает каждую завершённую свечу как «тире» или «точку». Бычья свеча (цена закрытия больше или равна цене открытия) кодируется как 1, медвежья свеча (цена закрытия меньше или равна цене открытия) — как 0. Стратегия анализирует последовательность последних свечей и сравнивает её с выбранной двоичной маской. Когда последовательность полностью совпадает с маской, открывается позиция в заданном направлении и сразу выставляются заявки на тейк-профит и стоп-лосс в пипсах.

Реализация использует только высокоуровневые API StockSharp: подписка на свечи обеспечивает поток данных, метод Bind передаёт готовые значения, а модуль StartProtection управляет выходами. Не требуются собственные коллекции или прямой доступ к значениям индикаторов, поэтому код остаётся компактным и устойчивым.

Логика паттерна

  • Анализируются только полностью завершённые свечи (CandleStates.Finished).
  • Каждая свеча преобразуется в двоичную цифру:
    • 1 — свеча бычья или нейтральная (Close >= Open).
    • 0 — свеча медвежья или нейтральная (Close <= Open). Дожи подходят под оба значения, как и в исходном советнике.
  • Маска выбирается из перечисления MorsePatternMasks, включающего все комбинации длиной от одной до пяти свечей, присутствовавшие в MT5-версии (например, 000, 1011, 11111).
  • Стратегия поддерживает скользящее окно последних свечей. Когда окно совпадает с выбранной маской, формируется сигнал на вход.

Такое поведение полностью повторяет оригинальный код MT5, где использовался CopyRates и посимвольное сравнение строки маски с каждой свечой.

Торговый процесс

  1. Подписаться на выбранный тип свечей и дождаться накопления количества баров, достаточного для длины маски.
  2. Для каждой завершённой свечи:
    • Обновить внутренние маски, классифицирующие свечу как бычью, медвежью или нейтральную.
    • Пропустить проверку, пока не накоплено столько свечей, сколько требует маска.
    • Если последовательность совпадает с маской, проверить выбранное направление сделки.
    • Отправить рыночную заявку (BuyMarket или SellMarket). Если уже открыта позиция в противоположную сторону, объём автоматически увеличивается для закрытия старой позиции и открытия новой — как делал MT5-советник.
  3. StartProtection немедленно навешивает стоп-лосс и тейк-профит, выраженные в ценовых единицах. Защитные заявки исполняются рыночными ордерами, что снижает риск неполучения исполнения.

Параметры

Имя Значение по умолчанию Описание
CandleType Свечи 5 минут (TimeSpan.FromMinutes(5).TimeFrame()) Тип данных, из которых строится последовательность.
Pattern _0 ("0") Двоичная маска для сравнения с последними свечами. Значения берутся из перечисления MorsePatternMasks.
Direction Sides.Buy Какую позицию открывать при совпадении маски: длинную или короткую.
TakeProfitPips 50 Расстояние до тейк-профита в пипсах. Стратегия автоматически корректирует шаг для 3- и 5-знаковых форекс-котировок, умножая ценовой шаг на десять.
StopLossPips 50 Расстояние до стоп-лосса в пипсах, рассчитывается тем же способом.
Volume (свойство стратегии) задаётся пользователем Объём заявки в лотах/контрактах, аналог параметра InpLot из MT5.

Все параметры доступны в окне настроек StockSharp, поддерживают оптимизацию и могут быть изменены до запуска стратегии.

Управление рисками

  • StartProtection задаёт цели в абсолютных ценовых единицах, рассчитанных из настроек пипсов. Выходы выполняются рыночными ордерами, что имитирует установку стоп-лосса и тейк-профита в MT5 сразу при открытии сделки.
  • Новая сделка в ту же сторону не открывается, пока текущая позиция не закрыта. Если сигнал появляется при противоположной позиции, объём автоматически увеличивается для переворота.
  • Журнал StockSharp фиксирует каждое срабатывание, упрощая анализ результатов.

Рекомендации по использованию

  • Маски короткие (до пяти свечей), что подчёркивает «азбуку Морзе». Для расширения набора сигналов можно комбинировать несколько стратегий с разными масками.
  • Конвертация в пипсы основывается на PriceStep. Для инструментов с нестандартным шагом подберите значения TakeProfitPips и StopLossPips вручную.
  • Стратегия не фильтрует время суток и волатильность. При необходимости подключите внешние фильтры или управляющую стратегию.
  • Перед тестированием убедитесь, что свойство Volume соответствует ожидаемому торговому лоту. Тестер StockSharp применит те же защитные механизмы, что и в реальной торговле.

Справка по маскам

Примеры значений перечисления:

  • _0"0" (одна медвежья свеча)
  • _5"11" (две последовательные бычьи свечи)
  • _20"0110" (зигзаг из медвежьих и бычьих свечей)
  • _33"00011" (три медвежьих свечи и две бычьих)
  • _61"11111" (пять бычьих свечей подряд)

Любая из 62 масок доступна в панели параметров, что позволяет точно воспроизвести нужный «азбуковый» сигнал.

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}.");
	}
}