Ver en GitHub

Rich Kohonen Map Strategy

Overview

The Rich Kohonen Map Strategy is a conversion of the MetaTrader 4 expert advisor "Rich.mq4". The original system builds a self-organizing map (Kohonen network) over feature vectors derived from Tom DeMark pivot calculations and classifies the next bar as a buy, sell or hold opportunity. The StockSharp port preserves the learning approach while integrating with the high-level strategy API, operating exclusively on completed candles and market orders.

Market data

  • Instrument – configured through the linked Security in the host application.
  • Candle type – parameter CandleType (default: 1-hour time frame). The strategy requires at least seven finished candles before producing signals so that both current and previous feature vectors can be assembled.

Trading logic

  1. Maintain a rolling window of the latest seven completed candles.
  2. Build two seven-element vectors on every finished candle:
    • The current vector uses the most recent open together with Tom DeMark pivot projections calculated from the previous five candles.
    • The previous vector shifts the window by one bar and represents the bar that just closed. This vector is used for training.
  3. Compare the current vector with three Kohonen maps (buy, sell, hold) and record the Euclidean distance to each best-matching unit.
  4. Select the action with the smallest distance and set the target position:
    • Buy → long exposure equal to the calculated volume.
    • Sell → short exposure of the same magnitude.
    • Hold → no position. The strategy sends market orders for the difference between the current and target position so that the final exposure matches the decision.
  5. Compute the open-to-open move (in pips) between the latest two candles and train the map:
    • Positive move within [MinPips, MaxPips] → add the previous vector to the buy map.
    • Negative move within [-MaxPips, -MinPips] → add the previous vector to the sell map.
    • Otherwise → store the vector in the hold map.
  6. Position size is determined dynamically from the portfolio balance: floor(balance / 50) / 10. If this produces zero, the Lots fallback parameter is used instead.

Parameters

  • MinPips – lower bound (in pips) for considering a positive open-to-open move as a buy training example.
  • MaxPips – upper bound (in pips) for buy/sell training samples.
  • TakeProfit, StopLoss – preserved from the MQL expert for documentation purposes. The high-level implementation closes or reverses positions via market orders rather than by attaching stops.
  • Lots – fallback volume applied when the balance-based formula yields zero.
  • Slippage – reserved for manual order tuning (not used directly by the high-level API helpers).
  • MapPath – binary file path used to persist the three Kohonen maps between runs.
  • EAName – optional comment stored for reference.
  • CandleType – candle subscription used for feature extraction.

Persistent map storage

The strategy stores the trained map in a binary file defined by MapPath (default rl.bin inside the working directory). The file contains the buy, sell and hold matrices sequentially. On startup the matrices are loaded, and the strategy counts the non-empty rows to resume training from the previous state. Missing files are ignored, which causes the maps to start from zero-filled memory.

Differences from the original MQL expert

  • Orders are issued through StockSharp helpers (BuyMarket / SellMarket) and target the final desired exposure instead of forcing a full close plus reopen on each bar. This keeps the effective behaviour while reducing duplicate transactions in the managed environment.
  • Stop-loss and take-profit levels remain as parameters for documentation but are not registered as separate orders. Position exits occur when the classifier selects the opposite side or the hold action.
  • File handling uses .NET I/O helpers; the map format remains compatible (double precision values ordered identically).

Usage notes

  • Ensure the selected security exposes a valid PriceStep so pip differences are computed correctly. If the step is missing or zero, the strategy falls back to a unit step.
  • The Kohonen maps can grow large (up to 10000 buy/sell entries and 25000 hold entries). Keep the default path on a storage device with sufficient capacity (~2.5 MB when full).
  • Because the algorithm trains continuously, running the strategy on historical data before live deployment helps populate the map with representative samples.
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;
	}
}