Открыть на GitHub

Стратегия Color Fisher M11

Обзор

Color Fisher M11 повторяет советник Exp_ColorFisher_m11 для MetaTrader 5 и использует изменённый индикатор Fisher Transform с пятицветной раскраской. Индикатор помогает выделить периоды сильного импульса и задаёт торговые сигналы после задержки на заданное количество закрытых свечей. Дополнительные флаги позволяют отдельно отключать открытия и закрытия для длинной и короткой стороны.

Логика индикатора

Стратегия рассчитывает индикатор Color Fisher на лету:

  • Находит максимум и минимум за окно Range Periods.
  • Нормализует среднюю цену текущей свечи в этом диапазоне и сглаживает её коэффициентом Price Smoothing по принципу EMA.
  • Применяет преобразование Фишера и дополнительное сглаживание Index Smoothing для получения итогового значения.
  • Классифицирует значение в пять дискретных состояний по порогам High Level и Low Level:
    • 0 – сильный бычий импульс выше верхнего порога.
    • 1 – умеренное бычье движение между нулём и верхним порогом.
    • 2 – нейтральная зона возле нуля.
    • 3 – умеренное медвежье движение между нулём и нижним порогом.
    • 4 – сильный медвежий импульс ниже нижнего порога.

Сигнал оценивается по свечам, отстоящим на Signal Bar назад, как и в оригинальном советнике. Предыдущее состояние цвета также хранится, чтобы фиксировать новые переходы в экстремальные зоны.

Правила торговли

  • Вход в лонг – возможен при включённом Enable Buy Entry, если отложенный цвет равен 0, а предыдущее состояние отличается от 0. При наличии шорта позиция полностью разворачивается в лонг.
  • Вход в шорт – возможен при включённом Enable Sell Entry, если отложенный цвет равен 4, а предыдущее состояние отличается от 4. При наличии лонга позиция разворачивается в шорт.
  • Выход из лонга – выполняется при активном Enable Buy Exit, когда отложенный цвет переходит в 3 или 4, что сигнализирует о преимуществе продавцов.
  • Выход из шорта – выполняется при активном Enable Sell Exit, когда отложенный цвет переходит в 0 или 1, что указывает на доминирование покупателей.

Чтобы не выставлять несколько заявок на одном сигнале, стратегия запоминает момент закрытия следующей свечи для каждого направления и запрещает новые входы до завершения следующего бара.

Управление рисками

Параметры Stop Loss (pts) и Take Profit (pts) переводят исходные значения в пунктах в абсолютные шаги цены с учётом шага инструмента. Если указан положительный размер, активируется встроенная защита StartProtection. Нулевое значение отключает соответствующий защитный ордер.

Параметры

  • Range Periods – длина окна для расчёта диапазона максимумов и минимумов (по умолчанию 10).
  • Price Smoothing – коэффициент сглаживания до преобразования Фишера, 0…0.99 (по умолчанию 0.3).
  • Index Smoothing – коэффициент сглаживания после преобразования, 0…0.99 (по умолчанию 0.3).
  • High Level / Low Level – пороги, определяющие бычьи и медвежьи экстремумы (по умолчанию +1.01 и –1.01).
  • Signal Bar – задержка сигнала в количестве закрытых свечей (по умолчанию 1).
  • Enable Buy Entry / Enable Sell Entry – флаги разрешения открытия длинных или коротких позиций.
  • Enable Buy Exit / Enable Sell Exit – флаги разрешения закрытия по индикатору.
  • Stop Loss (pts) / Take Profit (pts) – защитные расстояния в шагах цены.
  • Candle Type – таймфрейм подписки на свечи, по умолчанию четырёхчасовые.

Примечания

  • Используется высокоуровневое API StockSharp (SubscribeCandles().BindEx), дополнительные коллекции не создаются, кроме минимальной истории цветов для задержки сигнала.
  • В данной поставке отсутствует Python-версия, как и требовалось.
  • Для наглядности можно добавить стратегию на график и отрисовать индикатор вместе с ценой.
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;
		}
	}
}