Открыть на GitHub

FuturePatternMemoryStrategy

Обзор

FuturePatternMemoryStrategy — это перенос MetaTrader-советников FutureMA и FutureMACD на платформу StockSharp. Исходные роботы записывали последовательности разностей индикаторов в CSV-файлы, накапливали статистику и по ней решали, какой прорыв (вверх или вниз) вероятнее. В версии на C# вся логика перенесена в память процесса, а все параметры вынесены в публичные настройки. Через параметр Source можно выбрать источник сигналов: спред сглаженных скользящих средних (FutureMA) или гистограмму MACD (FutureMACD).

Каждая закрытая свеча проходит через пять этапов:

  1. Проекция индикатора — рассчитывается выбранный осциллятор (спред SMMА или гистограмма MACD), результат масштабируется коэффициентом NormalizationFactor и дискретизируется до целого числа, чтобы получить компактный «отпечаток» состояния.
  2. Хеширование паттерна — поддерживается скользящее окно из последних AnalysisBars дискретных значений. После закрытия свечи окно преобразуется в строковый ключ, который идентифицирует текущий паттерн.
  3. Анализ экстремумов — для прошлых FractalDepth свечей измеряется расстояние от открытия самой старой свечи до ближайшего максимума и минимума, значения переводятся в пункты. Это ожидаемая потенциальная прибыль, которую копили оригинальные эксперты.
  4. Обновление памяти — по ключу достается (или создается) запись в словаре. Многократные появления паттерна усредняются с коэффициентом забывания ForgettingFactor, что воспроизводит формулу (текущее + новое * z) / (1 + z) из MQL.
  5. Оценка сигнала и исполнение — если вероятность роста выше, накоплено больше MinimumMatches совпадений и ожидаемая цель превышает MinimumTakeProfit, стратегия открывает или наращивает длинную позицию; для коротких позиций логика зеркальная. Уровни стоп-лосса и тейк-профита берутся из накопленных значений, при включенном EnableTrailingStop стоп подтягивается на четверть пути, как делал оригинальный робот.

Особенности конверсии

  • Оба эксперта объединены в одну стратегию. Переключение между логикой FutureMA и FutureMACD осуществляется параметром Source.
  • Файловая система заменена на Dictionary<string, PatternStats>, поэтому хранение статистики происходит полностью в памяти и не зависит от внешних ресурсов.
  • Управление позицией повторяет MQL-версию: стоп устанавливается по полной величине усреднённого фрактального диапазона, тейк-профит — по доле StatisticalTakeRatio. Если активирован трейлинг, стоп переносится на четверть пройденного расстояния.
  • Режим ManualMode соответствует флагу Ruchnik — статистика собирается, но ордера не отправляются.
  • Параметр AllowAddOn воспроизводит dokupka и разрешает добавлять объём при повторении паттерна на новой свече.

Логика торговли

  • Источник данных
    • MaSpread — разность сглаженных скользящих средних (SMMА) с периодами 6 и 24 по медианной цене свечи.
    • MacdHistogram — разность основной и сигнальной линий MACD (12/26/9).
  • Нормализация: коэффициент NormalizationFactor (аналог tocnost) масштабирует разность индикаторов, после чего она делится на 100 * MinPriceStep и округляется до целого.
  • Память паттернов: для каждого ключа хранится количество и средняя величина ожидания для покупок и продаж, обновление выполняется с учётом ForgettingFactor.
  • Условия входа:
    • Лонг — ожидаемая прибыль покупателей ≥ продавцов, число совпадений > MinimumMatches, ожидаемая величина > MinimumTakeProfit.
    • Шорт — ожидаемая прибыль продавцов ≥ покупателей и аналогичные ограничения.
  • Защита позиции: стоп равен полной усреднённой амплитуде, тейк-профит — её части StatisticalTakeRatio. При включённом трейлинге стоп смещается после прохождения четверти расстояния.

Параметры

Параметр Описание Значение по умолчанию
CandleType Основной таймфрейм стратегии. 30 минут
Source Выбор между спредом SMMА и гистограммой MACD. MaSpread
FastMaLength / SlowMaLength Периоды SMMА для режима MaSpread. 6 / 24
MacdFastLength / MacdSlowLength / MacdSignalLength Периоды MACD для режима MacdHistogram. 12 / 26 / 9
AnalysisBars Количество баров в хеше паттерна. 8
FractalDepth Глубина анализа экстремумов. 4
MinimumMatches Минимальное число совпадений до входа. 5
MinimumTakeProfit Минимально допустимое ожидаемое расстояние (в пунктах). 30
NormalizationFactor Коэффициент масштабирования индикатора. 10
ForgettingFactor Вес нового наблюдения в памяти. 1.5
StatisticalTakeRatio Доля, используемая для тейк-профита. 0.5
EnableTrailingStop Включает трейлинг стопа. false
ManualMode Только сбор статистики без сделок. false
AllowAddOn Разрешение на добор позиции. true
Volume Объём заявки. 0.1

Практические рекомендации

  • Подбор NormalizationFactor и AnalysisBars критичен: слишком крупные значения приводят к редким хешам и плохой статистике, слишком мелкие — смешивают разные режимы рынка.
  • Если необходимо сохранять накопленную статистику между запусками, сериализуйте словарь после завершения теста или торговой сессии.
  • Как и в MQL-версии, лучше запускать отдельный экземпляр стратегии для каждого инструмента и таймфрейма, чтобы статистика не смешивалась.
using System;
using System.Collections.Generic;
using System.Text;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Pattern memory strategy that records normalized MA spread sequences,
/// tracks fractal outcomes, and trades when a recognized pattern has favorable statistics.
/// </summary>
public class FuturePatternMemoryStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastMaLength;
	private readonly StrategyParam<int> _slowMaLength;
	private readonly StrategyParam<int> _patternLength;
	private readonly StrategyParam<int> _minMatches;

	private readonly Queue<int> _patternWindow = new();
	private readonly Dictionary<string, (int buyCount, int sellCount)> _patterns = new();
	private decimal _entryPrice;
	private int _barIndex;
	private int _lastEntryBar;

	public FuturePatternMemoryStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for analysis.", "General");

		_fastMaLength = Param(nameof(FastMaLength), 6)
			.SetDisplay("Fast MA", "Fast EMA period.", "Indicators");

		_slowMaLength = Param(nameof(SlowMaLength), 24)
			.SetDisplay("Slow MA", "Slow EMA period.", "Indicators");

		_patternLength = Param(nameof(PatternLength), 5)
			.SetDisplay("Pattern Length", "Number of bars in pattern signature.", "Pattern");

		_minMatches = Param(nameof(MinMatches), 3)
			.SetDisplay("Min Matches", "Minimum pattern occurrences before trading.", "Pattern");
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int FastMaLength
	{
		get => _fastMaLength.Value;
		set => _fastMaLength.Value = value;
	}

	public int SlowMaLength
	{
		get => _slowMaLength.Value;
		set => _slowMaLength.Value = value;
	}

	public int PatternLength
	{
		get => _patternLength.Value;
		set => _patternLength.Value = value;
	}

	public int MinMatches
	{
		get => _minMatches.Value;
		set => _minMatches.Value = value;
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_patternWindow.Clear();
		_patterns.Clear();
		_entryPrice = 0;
		_barIndex = 0;
		_lastEntryBar = 0;
	}

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

		_patternWindow.Clear();
		_patterns.Clear();
		_entryPrice = 0;
		_barIndex = 0;
		_lastEntryBar = 0;

		var fastEma = new ExponentialMovingAverage { Length = FastMaLength };
		var slowEma = new ExponentialMovingAverage { Length = SlowMaLength };

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

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

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

		_barIndex++;
		var close = candle.ClosePrice;

		// Normalize the MA spread into a discrete value
		var spread = fastValue - slowValue;
		var normalized = spread > 0 ? 1 : (spread < 0 ? -1 : 0);

		_patternWindow.Enqueue(normalized);
		while (_patternWindow.Count > PatternLength)
			_patternWindow.Dequeue();

		if (_patternWindow.Count < PatternLength)
			return;

		var key = BuildPatternKey(_patternWindow);

		// Record outcome: if price went up, it's a buy match, otherwise sell
		if (!_patterns.TryGetValue(key, out var stats))
			stats = (0, 0);

		if (close > fastValue)
			stats = (stats.buyCount + 1, stats.sellCount);
		else if (close < fastValue)
			stats = (stats.buyCount, stats.sellCount + 1);

		_patterns[key] = stats;

		// Cooldown: minimum SlowMaLength*4 bars between any trade action
		var cooldownBars = SlowMaLength * 4;
		var cooledDown = _barIndex - _lastEntryBar >= cooldownBars;

		// Position management
		var exitedThisBar = false;
		if (Position > 0)
		{
			if (cooledDown && (spread < 0 || (_entryPrice > 0 && close < _entryPrice * 0.985m)))
			{
				SellMarket();
				_lastEntryBar = _barIndex;
				exitedThisBar = true;
			}
		}
		else if (Position < 0)
		{
			if (cooledDown && (spread > 0 || (_entryPrice > 0 && close > _entryPrice * 1.015m)))
			{
				BuyMarket();
				_lastEntryBar = _barIndex;
				exitedThisBar = true;
			}
		}

		// Entry based on pattern statistics (cooldown between entries)
		if (!exitedThisBar && Position == 0 && cooledDown)
		{
			var total = stats.buyCount + stats.sellCount;
			if (total >= MinMatches)
			{
				if (stats.buyCount > stats.sellCount && spread > 0)
				{
					_entryPrice = close;
					_lastEntryBar = _barIndex;
					BuyMarket();
				}
				else if (stats.sellCount > stats.buyCount && spread < 0)
				{
					_entryPrice = close;
					_lastEntryBar = _barIndex;
					SellMarket();
				}
			}
		}
	}

	private static string BuildPatternKey(IEnumerable<int> values)
	{
		var sb = new StringBuilder();
		var first = true;
		foreach (var v in values)
		{
			if (!first) sb.Append('_');
			sb.Append(v);
			first = false;
		}
		return sb.ToString();
	}
}