Ver en GitHub

Estrategia Color Fisher M11

Descripción general

Color Fisher M11 es una estrategia de seguimiento de tendencia que replica el asesor experto Exp_ColorFisher_m11 de MetaTrader 5. Utiliza una variante personalizada del Fisher Transform que colorea las velas con cinco estados de color para resaltar el momentum extremo alcista y bajista. Las señales se retrasan un número configurable de velas cerradas para evitar operar sobre datos incompletos, mientras que interruptores opcionales permiten desactivar entradas o salidas de cada lado de forma independiente.

Lógica del indicador

La estrategia construye el indicador Color Fisher en tiempo real:

  • Determina el máximo más alto y el mínimo más bajo en la ventana Range Periods.
  • Normaliza el precio medio de la vela actual dentro de ese rango y aplica Price Smoothing (estilo EMA) para estabilizar las oscilaciones.
  • Aplica el Fisher Transform con un factor adicional Index Smoothing para crear el valor final del oscilador.
  • Clasifica el oscilador en cinco bandas de color discretas usando los umbrales High Level y Low Level:
    • 0 – fuerte impulso alcista por encima del nivel alto.
    • 1 – momentum alcista moderado entre cero y el nivel alto.
    • 2 – zona neutral alrededor de cero.
    • 3 – momentum bajista moderado entre cero y el nivel bajo.
    • 4 – fuerte impulso bajista por debajo del nivel bajo.

La señal se evalúa Signal Bar velas atrás, imitando el comportamiento del Asesor Experto original. El estado de color anterior también se rastrea para detectar transiciones nuevas hacia las bandas extremas.

Reglas de trading

  • Entrada larga – permitida cuando Enable Buy Entry es verdadero, el color retrasado es igual a 0 (fuerte alcista) y el color previo es diferente de 0. Cualquier exposición corta se revierte y la posición se vuelve larga.
  • Entrada corta – permitida cuando Enable Sell Entry es verdadero, el color retrasado es igual a 4 (fuerte bajista) y el color previo es diferente de 4. Cualquier exposición larga se revierte y la posición se vuelve corta.
  • Salida larga – activada cuando Enable Buy Exit es verdadero y el color retrasado pasa a 3 o 4, señalando control bajista.
  • Salida corta – activada cuando Enable Sell Exit es verdadero y el color retrasado pasa a 0 o 1, señalando control alcista.

Para evitar múltiples órdenes por señal, la estrategia recuerda el tiempo de cierre de la siguiente barra para cada dirección y rechaza nuevas entradas hasta que se complete la siguiente vela.

Gestión de riesgos

Stop Loss (pts) y Take Profit (pts) convierten las distancias en pips originales en pasos de precio absolutos usando el precio de paso del instrumento. Cuando se proporciona una distancia positiva, las órdenes protectoras se activan a través de StartProtection. Establezca cualquier valor en cero para desactivar esa protección.

Parámetros

  • Range Periods – longitud del lookback para el rango alto/bajo usado por el Fisher Transform (por defecto 10).
  • Price Smoothing – factor de suavizado previo a la transformación, 0…0.99 (por defecto 0.3).
  • Index Smoothing – factor de suavizado posterior a la transformación, 0…0.99 (por defecto 0.3).
  • High Level / Low Level – umbrales que definen los extremos alcistas y bajistas (por defecto +1.01 y –1.01).
  • Signal Bar – número de velas cerradas para retrasar la evaluación de señales (por defecto 1).
  • Enable Buy Entry / Enable Sell Entry – interruptores para abrir nuevas operaciones largas o cortas.
  • Enable Buy Exit / Enable Sell Exit – interruptores para permitir salidas impulsadas por el indicador.
  • Stop Loss (pts) / Take Profit (pts) – distancias protectoras expresadas en pasos de precio.
  • Candle Type – marco temporal para la suscripción de velas; por defecto velas de 4 horas.

Notas

  • La estrategia usa bindings de alto nivel de StockSharp (SubscribeCandles().BindEx) y no almacena colecciones históricas más allá del historial mínimo de colores requerido para la señal retrasada.
  • En esta versión no se proporciona un puerto Python, de acuerdo con la especificación.
  • Agrega la estrategia a un área del gráfico para visualizar tanto el precio como el oscilador Color Fisher calculado.
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;
		}
	}
}