GitHub で見る

Color Fisher M11戦略

概要

Color Fisher M11は、MetaTrader 5のExp_ColorFisher_m11エキスパートアドバイザーを再現したトレンドフォロー戦略です。カスタムFisher Transform変種を使用し、5つのカラー状態でローソク足を塗り分けることで、極端な強気・弱気のモメンタムを強調します。不完全なデータでの取引を避けるため、シグナルは設定可能な数の確定足だけ遅延され、オプションのトグルにより各サイドのエントリーやエグジットを個別に無効化できます。

インジケーターロジック

戦略はColor Fisherインジケーターをリアルタイムで構築します:

  • Range Periodsウィンドウ内の最高値と最安値を決定する。
  • 現在のローソク足の中間価格をその範囲内で正規化し、Price Smoothing(EMAスタイル)を適用して変動を安定させる。
  • 追加のIndex Smoothingファクターを持つFisher Transformを適用して最終的なオシレーター値を生成する。
  • High LevelLow Levelの閾値を使用してオシレーターを5つの離散的なカラーバンドに分類する:
    • 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)は、インストゥルメントのステップ価格を使用して元のpips距離を絶対的な価格ステップに変換します。正の距離が指定されると、StartProtectionを通じて保護注文が有効化されます。いずれかの値をゼロに設定するとその保護機能が無効になります。

パラメーター

  • Range Periods – Fisher Transformが使用する高値/安値レンジのルックバック長(デフォルト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 – ローソク足サブスクリプションの時間軸;デフォルト: 4時間足。

注意事項

  • 戦略はStockSharpの高レベルバインディング(SubscribeCandles().BindEx)を使用し、遅延シグナルに必要な最小限のカラー履歴を超えた履歴コレクションを保存しません。
  • この版ではPythonポートは提供されていません(仕様に従っています)。
  • 価格とColor Fisherオシレーターの両方を視覚化するために、戦略をチャートエリアに追加してください。
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;
		}
	}
}