Открыть на GitHub

Стратегия Rich Kohonen Map

Обзор

Стратегия Rich Kohonen Map является портом эксперта MetaTrader 4 «Rich.mq4». Исходный алгоритм формирует самоорганизующуюся карту (сеть Кохонена) на основе векторов признаков из пивотов Tom DeMark и классифицирует следующую свечу как сигнал на покупку, продажу или удержание позиции. Версия для StockSharp сохраняет обучение сети, интегрируясь с высокоуровневым API и работая исключительно с завершёнными свечами и рыночными ордерами.

Рыночные данные

  • Инструмент — задаётся связанной сущностью Security в приложении.
  • Тип свечей — параметр CandleType (по умолчанию таймфрейм 1 час). Для построения обоих векторов требуется минимум семь завершённых свечей.

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

  1. Поддерживается скользящее окно из семи последних завершённых свечей.
  2. На каждой свече формируются два семиэлементных вектора:
    • Текущий вектор использует последний открытый бар и проекции пивотов Tom DeMark, рассчитанные по предыдущим пяти свечам.
    • Предыдущий вектор смещён на одну свечу назад и соответствует только что закрытому бару — именно он используется для обучения.
  3. Текущий вектор сравнивается с тремя картами Кохонена (покупка, продажа, удержание), для каждой вычисляется расстояние до лучшего соответствия.
  4. Выбирается действие с минимальным расстоянием и задаётся целевая позиция:
    • Покупка → длинная позиция на рассчитанный объём.
    • Продажа → короткая позиция на тот же объём.
    • Удержание → нулевая позиция. Стратегия отправляет рыночные ордера на величину разницы между текущей и целевой позицией, чтобы привести итоговую экспозицию к принятому решению.
  5. Рассчитывается изменение открытия к открытию (в пунктах) между двумя последними свечами, после чего карта обучается:
    • Положительное изменение в диапазоне [MinPips, MaxPips] → предыдущий вектор добавляется в карту покупок.
    • Отрицательное изменение в диапазоне [-MaxPips, -MinPips] → в карту продаж.
    • Остальные случаи → в карту удержания.
  6. Объём позиции рассчитывается динамически по балансу портфеля: floor(balance / 50) / 10. Если результат равен нулю, применяется резервный параметр Lots.

Параметры

  • MinPips — нижняя граница (в пунктах) для учёта положительного изменения как обучающего примера покупки.
  • MaxPips — верхняя граница (в пунктах) для примеров покупки и продажи.
  • TakeProfit, StopLoss — сохранены из оригинального эксперта в информационных целях. В реализации StockSharp закрытие и разворот выполняются рыночными ордерами без отдельных стоп-заявок.
  • Lots — резервный объём, используемый, если расчёт по балансу дал ноль.
  • Slippage — запас по проскальзыванию (на уровне high-level API напрямую не применяется).
  • MapPath — путь к двоичному файлу, в котором сохраняются обученные карты.
  • EAName — текстовый комментарий для ордеров.
  • CandleType — тип свечей, по которым рассчитываются признаки.

Хранение карт

Стратегия записывает обученные карты в двоичный файл MapPath (по умолчанию rl.bin в рабочей директории). В файле последовательно хранятся матрицы покупок, продаж и удержания. При старте данные считываются, и для каждой матрицы подсчитывается количество ненулевых строк, чтобы продолжить обучение с предыдущего состояния. Если файл отсутствует, карты инициализируются нулями.

Отличия от оригинального MQL-эксперта

  • Ордера отправляются через BuyMarket / SellMarket, и стратегия приводит позицию к целевому значению вместо полного закрытия и повторного открытия на каждой свече. Это уменьшает количество операций, сохраняя итоговое поведение.
  • Параметры стоп-лосса и тейк-профита сохранены для совместимости, но отдельные заявки не выставляются — позиция закрывается при смене классификации.
  • Работа с файлами реализована средствами .NET, однако формат данных (последовательность чисел double) остался прежним.

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

  • Убедитесь, что у выбранного инструмента задан корректный PriceStep; при отсутствии шага стратегия использует значение 1, что влияет на пересчёт пунктов.
  • Карты могут занимать до 2,5 МБ (10000 строк для покупок и продаж, 25000 для удержания), поэтому необходимо достаточное место на диске.
  • Перед торговлей в реальном времени рекомендуется прогнать стратегию на исторических данных, чтобы заполнить карты репрезентативными примерами.
using System;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Rich Kohonen Map: EMA crossover with ATR stops.
/// </summary>
public class RichKohonenMapStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _fastEmaLength;
	private readonly StrategyParam<int> _slowEmaLength;
	private readonly StrategyParam<int> _atrLength;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;

	public RichKohonenMapStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe.", "General");
		_fastEmaLength = Param(nameof(FastEmaLength), 10)
			.SetDisplay("Fast EMA Length", "Fast EMA period.", "Indicators");
		_slowEmaLength = Param(nameof(SlowEmaLength), 30)
			.SetDisplay("Slow EMA Length", "Slow EMA period.", "Indicators");
		_atrLength = Param(nameof(AtrLength), 14)
			.SetDisplay("ATR Length", "ATR period.", "Indicators");
	}

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int FastEmaLength { get => _fastEmaLength.Value; set => _fastEmaLength.Value = value; }
	public int SlowEmaLength { get => _slowEmaLength.Value; set => _slowEmaLength.Value = value; }
	public int AtrLength { get => _atrLength.Value; set => _atrLength.Value = value; }

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

		_prevFast = 0; _prevSlow = 0; _entryPrice = 0;
	}

		protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0;
		var fastEma = new ExponentialMovingAverage { Length = FastEmaLength };
		var slowEma = new ExponentialMovingAverage { Length = SlowEmaLength };
		var atr = new AverageTrueRange { Length = AtrLength };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(fastEma, slowEma, atr, 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 fastVal, decimal slowVal, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished) return;
		if (_prevFast == 0 || _prevSlow == 0 || atrVal <= 0) { _prevFast = fastVal; _prevSlow = slowVal; return; }
		var close = candle.ClosePrice;

		if (Position > 0)
		{
			if ((fastVal < slowVal && _prevFast >= _prevSlow) || close <= _entryPrice - atrVal * 2m) { SellMarket(); _entryPrice = 0; }
		}
		else if (Position < 0)
		{
			if ((fastVal > slowVal && _prevFast <= _prevSlow) || close >= _entryPrice + atrVal * 2m) { BuyMarket(); _entryPrice = 0; }
		}

		if (Position == 0)
		{
			if (fastVal > slowVal && _prevFast <= _prevSlow) { _entryPrice = close; BuyMarket(); }
			else if (fastVal < slowVal && _prevFast >= _prevSlow) { _entryPrice = close; SellMarket(); }
		}
		_prevFast = fastVal; _prevSlow = slowVal;
	}
}