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;
}
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class color_fisher_m11_strategy(Strategy):
"""Color Fisher Transform strategy with configurable entries/exits and SL/TP protection."""
def __init__(self):
super(color_fisher_m11_strategy, self).__init__()
self._range_periods = self.Param("RangePeriods", 3) \
.SetGreaterThanZero() \
.SetDisplay("Range Periods", "Lookback window for highs and lows", "Indicator")
self._price_smoothing = self.Param("PriceSmoothing", 0.3) \
.SetDisplay("Price Smoothing", "Smoothing factor before Fisher transform", "Indicator")
self._index_smoothing = self.Param("IndexSmoothing", 0.3) \
.SetDisplay("Index Smoothing", "Smoothing factor after Fisher transform", "Indicator")
self._high_level = self.Param("HighLevel", 0.05) \
.SetDisplay("High Level", "Upper level for bullish color", "Indicator")
self._low_level = self.Param("LowLevel", -0.05) \
.SetDisplay("Low Level", "Lower level for bearish color", "Indicator")
self._signal_bar = self.Param("SignalBar", 0) \
.SetDisplay("Signal Bar", "Bars to delay signal execution", "Trading")
self._enable_buy_entry = self.Param("EnableBuyEntry", True) \
.SetDisplay("Enable Buy Entry", "Allow opening long positions", "Trading")
self._enable_sell_entry = self.Param("EnableSellEntry", True) \
.SetDisplay("Enable Sell Entry", "Allow opening short positions", "Trading")
self._enable_buy_exit = self.Param("EnableBuyExit", True) \
.SetDisplay("Enable Buy Exit", "Allow closing long positions", "Trading")
self._enable_sell_exit = self.Param("EnableSellExit", True) \
.SetDisplay("Enable Sell Exit", "Allow closing short positions", "Trading")
self._stop_loss_points = self.Param("StopLossPoints", 1000) \
.SetDisplay("Stop Loss (pts)", "Protective stop distance in price steps", "Protection")
self._take_profit_points = self.Param("TakeProfitPoints", 2000) \
.SetDisplay("Take Profit (pts)", "Target distance in price steps", "Protection")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe for indicator calculation", "General")
# Fisher indicator state
self._highs = []
self._lows = []
self._prev_fish = 0.0
self._prev_index = 0.0
self._has_prev = False
self._fisher_count = 0
self._last_color = 2
# Color history (most recent first)
self._color_history = []
@property
def RangePeriods(self):
return int(self._range_periods.Value)
@property
def PriceSmoothing(self):
return float(self._price_smoothing.Value)
@property
def IndexSmoothing(self):
return float(self._index_smoothing.Value)
@property
def HighLevel(self):
return float(self._high_level.Value)
@property
def LowLevel(self):
return float(self._low_level.Value)
@property
def SignalBar(self):
return int(self._signal_bar.Value)
@property
def EnableBuyEntry(self):
return self._enable_buy_entry.Value
@property
def EnableSellEntry(self):
return self._enable_sell_entry.Value
@property
def EnableBuyExit(self):
return self._enable_buy_exit.Value
@property
def EnableSellExit(self):
return self._enable_sell_exit.Value
@property
def StopLossPoints(self):
return int(self._stop_loss_points.Value)
@property
def TakeProfitPoints(self):
return int(self._take_profit_points.Value)
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(color_fisher_m11_strategy, self).OnStarted2(time)
self._highs = []
self._lows = []
self._prev_fish = 0.0
self._prev_index = 0.0
self._has_prev = False
self._fisher_count = 0
self._last_color = 2
self._color_history = []
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
sl = self.StopLossPoints * step if self.StopLossPoints > 0 else 0.0
tp = self.TakeProfitPoints * step if self.TakeProfitPoints > 0 else 0.0
if sl > 0 or tp > 0:
self.StartProtection(
stopLoss=Unit(sl, UnitTypes.Absolute) if sl > 0 else None,
takeProfit=Unit(tp, UnitTypes.Absolute) if tp > 0 else None
)
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _calc_fisher(self, candle):
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
self._highs.append(h)
self._lows.append(lo)
self._fisher_count += 1
length = max(1, self.RangePeriods)
while len(self._highs) > length:
self._highs.pop(0)
self._lows.pop(0)
highest = max(self._highs)
lowest = min(self._lows)
sec = self.Security
min_range = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 0.0001
rng = highest - lowest
if rng < min_range:
rng = min_range
mid = (h + lo) / 2.0
price_loc = (mid - lowest) / rng if rng != 0 else 0.99
price_loc = 2.0 * price_loc - 1.0
prev_fish = self._prev_fish if self._has_prev else price_loc
fish = self.PriceSmoothing * prev_fish + (1.0 - self.PriceSmoothing) * price_loc
smoothed = min(max(fish, -0.99), 0.99)
diff = 1.0 - smoothed
if diff == 0:
fisher_raw = 0.0
else:
ratio = (1.0 + smoothed) / diff
fisher_raw = math.log(ratio)
prev_idx = self._prev_index if self._has_prev else fisher_raw
value = self.IndexSmoothing * prev_idx + (1.0 - self.IndexSmoothing) * fisher_raw
self._prev_fish = fish
self._prev_index = value
self._has_prev = True
is_formed = self._fisher_count >= length
color = 2
if value > 0:
color = 0 if value > self.HighLevel else 1
elif value < 0:
color = 4 if value < self.LowLevel else 3
self._last_color = color
return color, is_formed
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
color, is_formed = self._calc_fisher(candle)
self._color_history.insert(0, color)
mx = max(self.SignalBar + 2, 5)
while len(self._color_history) > mx:
self._color_history.pop()
if not is_formed:
return
sig_bar = self.SignalBar
signal_color = self._get_color(sig_bar)
prev_color = self._get_color(sig_bar + 1)
if signal_color is None or prev_color is None:
return
if self.EnableSellExit and signal_color < 2 and self.Position < 0:
self.BuyMarket()
if self.EnableBuyExit and signal_color > 2 and self.Position > 0:
self.SellMarket()
if self.EnableBuyEntry and signal_color <= 1 and prev_color > 1 and self.Position <= 0:
self.BuyMarket()
elif self.EnableSellEntry and signal_color >= 3 and prev_color < 3 and self.Position >= 0:
self.SellMarket()
def _get_color(self, index):
if index < 0 or index >= len(self._color_history):
return None
return self._color_history[index]
def OnReseted(self):
super(color_fisher_m11_strategy, self).OnReseted()
self._highs = []
self._lows = []
self._prev_fish = 0.0
self._prev_index = 0.0
self._has_prev = False
self._fisher_count = 0
self._last_color = 2
self._color_history = []
def CreateClone(self):
return color_fisher_m11_strategy()