View on GitHub

Color Fisher M11 Strategy

Overview

Color Fisher M11 is a trend-following strategy that replicates the Exp_ColorFisher_m11 expert advisor from MetaTrader 5. It uses a custom Fisher Transform variant that paints candles with five color states to highlight extreme bullish and bearish momentum. Signals are delayed by a configurable number of closed candles to avoid trading on incomplete data, while optional toggles allow disabling entries or exits for each side independently.

Indicator logic

The strategy builds the Color Fisher indicator in real time:

  • Determines the highest high and lowest low over the Range Periods window.
  • Normalizes the mid-price of the current candle inside that range and applies Price Smoothing (EMA-style) to stabilize swings.
  • Applies the Fisher Transform with an additional Index Smoothing factor to create the final oscillator value.
  • Classifies the oscillator into five discrete color bands using the High Level and Low Level thresholds:
    • 0 – strong bullish impulse above the high level.
    • 1 – moderate bullish momentum between zero and the high level.
    • 2 – neutral zone around zero.
    • 3 – moderate bearish momentum between zero and the low level.
    • 4 – strong bearish impulse below the low level.

The signal is evaluated Signal Bar candles back, mimicking the original Expert Advisor behaviour. The previous color state is also tracked to detect fresh transitions into the extreme bands.

Trading rules

  • Long entry – allowed when Enable Buy Entry is true, the delayed color equals 0 (strong bullish) and the previous color is different from 0. Any short exposure is reversed and the position turns long.
  • Short entry – allowed when Enable Sell Entry is true, the delayed color equals 4 (strong bearish) and the previous color is different from 4. Any long exposure is reversed and the position turns short.
  • Long exit – triggered when Enable Buy Exit is true and the delayed color moves to 3 or 4, signalling bearish control.
  • Short exit – triggered when Enable Sell Exit is true and the delayed color moves to 0 or 1, signalling bullish control.

To prevent multiple orders per signal, the strategy remembers the next bar close time for each direction and refuses new entries until the next candle is completed.

Risk management

Stop Loss (pts) and Take Profit (pts) convert the original pip distances into absolute price steps using the instrument step price. When a positive distance is supplied, protective orders are activated through StartProtection. Set either value to zero to disable that protection leg.

Parameters

  • Range Periods – lookback length for the high/low range used by the Fisher Transform (default 10).
  • Price Smoothing – pre-transform smoothing factor, 0…0.99 (default 0.3).
  • Index Smoothing – post-transform smoothing factor, 0…0.99 (default 0.3).
  • High Level / Low Level – thresholds that define bullish and bearish extremes (defaults +1.01 and –1.01).
  • Signal Bar – number of closed candles to delay signal evaluation (default 1).
  • Enable Buy Entry / Enable Sell Entry – toggles for opening new long or short trades.
  • Enable Buy Exit / Enable Sell Exit – toggles for allowing indicator-driven exits.
  • Stop Loss (pts) / Take Profit (pts) – protective distances expressed in price steps.
  • Candle Type – timeframe for the candle subscription; defaults to 4-hour candles.

Notes

  • The strategy uses high-level StockSharp bindings (SubscribeCandles().BindEx) and does not store historical collections beyond the minimal color history required for the delayed signal.
  • No Python port is provided in this release, matching the request specification.
  • Add the strategy to a chart area to visualize both price and the computed Color Fisher oscillator.
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 based on the Color Fisher Transform indicator.
/// Replicates the logic of the MQL5 expert Exp_ColorFisher_m11 with configurable entries and exits.
/// </summary>
public class ColorFisherM11Strategy : Strategy
{
	private readonly StrategyParam<int> _rangePeriods;
	private readonly StrategyParam<decimal> _priceSmoothing;
	private readonly StrategyParam<decimal> _indexSmoothing;
	private readonly StrategyParam<decimal> _highLevel;
	private readonly StrategyParam<decimal> _lowLevel;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<bool> _enableBuyEntry;
	private readonly StrategyParam<bool> _enableSellEntry;
	private readonly StrategyParam<bool> _enableBuyExit;
	private readonly StrategyParam<bool> _enableSellExit;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private ColorFisherM11Indicator _colorFisher;
	private readonly List<int> _colorHistory = new();
	private DateTimeOffset? _nextLongTime;
	private DateTimeOffset? _nextShortTime;

	/// <summary>
	/// Range length used to determine the Fisher Transform input window.
	/// </summary>
	public int RangePeriods
	{
		get => _rangePeriods.Value;
		set => _rangePeriods.Value = value;
	}

	/// <summary>
	/// Price smoothing factor (0..1) applied before the Fisher Transform.
	/// </summary>
	public decimal PriceSmoothing
	{
		get => _priceSmoothing.Value;
		set => _priceSmoothing.Value = value;
	}

	/// <summary>
	/// Fisher index smoothing factor (0..1) applied after the transform.
	/// </summary>
	public decimal IndexSmoothing
	{
		get => _indexSmoothing.Value;
		set => _indexSmoothing.Value = value;
	}

	/// <summary>
	/// Upper threshold used for bullish color classification.
	/// </summary>
	public decimal HighLevel
	{
		get => _highLevel.Value;
		set => _highLevel.Value = value;
	}

	/// <summary>
	/// Lower threshold used for bearish color classification.
	/// </summary>
	public decimal LowLevel
	{
		get => _lowLevel.Value;
		set => _lowLevel.Value = value;
	}

	/// <summary>
	/// Number of closed bars to wait before acting on a signal.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Enable long entries.
	/// </summary>
	public bool EnableBuyEntry
	{
		get => _enableBuyEntry.Value;
		set => _enableBuyEntry.Value = value;
	}

	/// <summary>
	/// Enable short entries.
	/// </summary>
	public bool EnableSellEntry
	{
		get => _enableSellEntry.Value;
		set => _enableSellEntry.Value = value;
	}

	/// <summary>
	/// Enable closing of existing long positions based on the indicator.
	/// </summary>
	public bool EnableBuyExit
	{
		get => _enableBuyExit.Value;
		set => _enableBuyExit.Value = value;
	}

	/// <summary>
	/// Enable closing of existing short positions based on the indicator.
	/// </summary>
	public bool EnableSellExit
	{
		get => _enableSellExit.Value;
		set => _enableSellExit.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Candle type and timeframe used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ColorFisherM11Strategy"/> class.
	/// </summary>
	public ColorFisherM11Strategy()
	{
		_rangePeriods = Param(nameof(RangePeriods), 3)
			.SetGreaterThanZero()
			.SetDisplay("Range Periods", "Lookback window for highs and lows", "Indicator");

		_priceSmoothing = Param(nameof(PriceSmoothing), 0.3m)
			.SetNotNegative()
			.SetRange(0.0001m, 0.99m)
			.SetDisplay("Price Smoothing", "Smoothing factor applied before Fisher transform", "Indicator");

		_indexSmoothing = Param(nameof(IndexSmoothing), 0.3m)
			.SetNotNegative()
			.SetRange(0.0001m, 0.99m)
			.SetDisplay("Index Smoothing", "Smoothing factor applied after Fisher transform", "Indicator");

		_highLevel = Param(nameof(HighLevel), 0.05m)
			.SetDisplay("High Level", "Upper level for bullish color", "Indicator");

		_lowLevel = Param(nameof(LowLevel), -0.05m)
			.SetDisplay("Low Level", "Lower level for bearish color", "Indicator");

		_signalBar = Param(nameof(SignalBar), 0)
			.SetNotNegative()
			.SetDisplay("Signal Bar", "Bars to delay signal execution", "Trading");

		_enableBuyEntry = Param(nameof(EnableBuyEntry), true)
			.SetDisplay("Enable Buy Entry", "Allow opening long positions", "Trading");

		_enableSellEntry = Param(nameof(EnableSellEntry), true)
			.SetDisplay("Enable Sell Entry", "Allow opening short positions", "Trading");

		_enableBuyExit = Param(nameof(EnableBuyExit), true)
			.SetDisplay("Enable Buy Exit", "Allow closing long positions", "Trading");

		_enableSellExit = Param(nameof(EnableSellExit), true)
			.SetDisplay("Enable Sell Exit", "Allow closing short positions", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pts)", "Protective stop distance in price steps", "Protection");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
			.SetNotNegative()
			.SetDisplay("Take Profit (pts)", "Target distance in price steps", "Protection");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for indicator calculation", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_colorFisher?.Reset();
		_colorHistory.Clear();
		_nextLongTime = null;
		_nextShortTime = null;
	}

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

		_colorFisher = new ColorFisherM11Indicator
		{
			RangePeriods = RangePeriods,
			PriceSmoothing = PriceSmoothing,
			IndexSmoothing = IndexSmoothing,
			HighLevel = HighLevel,
			LowLevel = LowLevel,
			MinRange = Security?.PriceStep ?? 0.0001m
		};

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

		var step = Security?.PriceStep ?? 1m;
		Unit stopLossUnit = StopLossPoints > 0 ? new Unit(step * StopLossPoints, UnitTypes.Absolute) : null;
		Unit takeProfitUnit = TakeProfitPoints > 0 ? new Unit(step * TakeProfitPoints, UnitTypes.Absolute) : null;

		if (stopLossUnit != null || takeProfitUnit != null)
			StartProtection(stopLoss: stopLossUnit, takeProfit: takeProfitUnit);

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

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

		_colorFisher.Process(new CandleIndicatorValue(_colorFisher, candle));
		UpdateHistory(_colorFisher.LastColor);

		if (!_colorFisher.IsFormed)
			return;

		// indicator already checked via IsFormed above

		var signalColor = GetColor(SignalBar);
		var previousColor = GetColor(SignalBar + 1);

		if (signalColor is null || previousColor is null)
			return;

		if (EnableSellExit && signalColor < 2 && Position < 0)
		{
			BuyMarket();
		}

		if (EnableBuyExit && signalColor > 2 && Position > 0)
		{
			SellMarket();
		}

		var allowLong = !_nextLongTime.HasValue || candle.CloseTime >= _nextLongTime.Value;
		var allowShort = !_nextShortTime.HasValue || candle.CloseTime >= _nextShortTime.Value;

		if (EnableBuyEntry && allowLong && signalColor <= 1 && previousColor > 1 && Position <= 0)
		{
			var volume = Volume + Math.Abs(Position);
			BuyMarket();
			_nextLongTime = candle.CloseTime;
		}
		else if (EnableSellEntry && allowShort && signalColor >= 3 && previousColor < 3 && Position >= 0)
		{
			var volume = Volume + Math.Abs(Position);
			SellMarket();
			_nextShortTime = candle.CloseTime;
		}
	}

	private void UpdateHistory(int color)
	{
		_colorHistory.Insert(0, color);
		var max = Math.Max(SignalBar + 2, 5);
		while (_colorHistory.Count > max)
		{
			try { _colorHistory.RemoveAt(_colorHistory.Count - 1); } catch { break; }
		}
	}

	private int? GetColor(int index)
	{
		if (index < 0 || index >= _colorHistory.Count)
			return null;

		return _colorHistory[index];
	}

	private sealed class ColorFisherM11Indicator : BaseIndicator
	{
		public int RangePeriods { get; set; } = 10;
		public decimal PriceSmoothing { get; set; } = 0.3m;
		public decimal IndexSmoothing { get; set; } = 0.3m;
		public decimal HighLevel { get; set; } = 1.01m;
		public decimal LowLevel { get; set; } = -1.01m;
		public decimal MinRange { get; set; } = 0.0001m;
		public int LastColor { get; private set; } = 2;

		private readonly List<decimal> _highs = new();
		private readonly List<decimal> _lows = new();
		private decimal _prevFish;
		private decimal _prevIndex;
		private bool _hasPrevIndex;
		private int _count;

		protected override IIndicatorValue OnProcess(IIndicatorValue input)
		{
			var candle = input.GetValue<ICandleMessage>();
			if (candle == null)
				return new DecimalIndicatorValue(this, decimal.Zero, input.Time);

			_highs.Add(candle.HighPrice);
			_lows.Add(candle.LowPrice);
			_count++;

			var length = Math.Max(1, RangePeriods);
			while (_highs.Count > length)
			{
				_highs.RemoveAt(0);
				_lows.RemoveAt(0);
			}

			var highest = decimal.MinValue;
			var lowest = decimal.MaxValue;
			for (var i = 0; i < _highs.Count; i++)
			{
				if (_highs[i] > highest) highest = _highs[i];
				if (_lows[i] < lowest) lowest = _lows[i];
			}

			var range = highest - lowest;
			var minRange = MinRange <= 0m ? 0.0001m : MinRange;
			if (range < minRange)
				range = minRange;

			var midPrice = (candle.HighPrice + candle.LowPrice) / 2m;
			var priceLocation = range != 0m ? (midPrice - lowest) / range : 0.99m;
			priceLocation = 2m * priceLocation - 1m;

			var prevFish = _hasPrevIndex ? _prevFish : priceLocation;
			var fish = PriceSmoothing * prevFish + (1m - PriceSmoothing) * priceLocation;
			var smoothed = Math.Min(Math.Max(fish, -0.99m), 0.99m);

			decimal fisherRaw;
			var diff = 1m - smoothed;
			if (diff == 0m)
			{
				fisherRaw = 0m;
			}
			else
			{
				var ratio = (1m + smoothed) / diff;
				fisherRaw = (decimal)Math.Log((double)ratio);
			}

			var prevIndex = _hasPrevIndex ? _prevIndex : fisherRaw;
			var value = IndexSmoothing * prevIndex + (1m - IndexSmoothing) * fisherRaw;

			_prevFish = fish;
			_prevIndex = value;
			_hasPrevIndex = true;

			IsFormed = _count >= length;

			var color = 2;
			if (value > 0m)
				color = value > HighLevel ? 0 : 1;
			else if (value < 0m)
				color = value < LowLevel ? 4 : 3;

			LastColor = color;

			return new DecimalIndicatorValue(this, value, input.Time) { IsFinal = true };
		}

		public override void Reset()
		{
			base.Reset();
			_highs.Clear();
			_lows.Clear();
			_prevFish = 0m;
			_prevIndex = 0m;
			_hasPrevIndex = false;
			_count = 0;
			LastColor = 2;
		}
	}
}