Color Fisher M11 策略
概述
Color Fisher M11 策略复刻了 MetaTrader 5 上的 Exp_ColorFisher_m11 专家顾问。它利用改进版的 Fisher Transform 指标,将柱线划分为五个颜色区间,以突出极端的多头与空头动能。信号会延迟若干根已完成的 K 线执行,并提供开仓、平仓方向独立的开关,方便适配不同的交易偏好。
指标原理
策略会实时构建 Color Fisher 指标,步骤如下:
- 在 Range Periods 窗口内寻找最高价和最低价。
- 计算当前 K 线的中间价,并使用 Price Smoothing 系数(类似 EMA)进行平滑。
- 将结果送入 Fisher Transform,并再用 Index Smoothing 进行二次平滑,得到最终振荡值。
- 根据 High Level 与 Low Level 阈值,把振荡值划分为五个颜色状态:
0– 高于上阈值的强势多头区。1– 位于零轴与上阈值之间的普通多头区。2– 零轴附近的中性区。3– 位于零轴与下阈值之间的普通空头区。4– 低于下阈值的强势空头区。
策略会回溯 Signal Bar 根已完成的 K 线评估信号,并保存更早一根的颜色,用于识别刚刚进入极端颜色的时刻,与原始 EA 保持一致。
交易规则
- 做多开仓:启用
Enable Buy Entry时,当延迟后的颜色等于0且上一状态不为0。若当前持有空头仓位,将先平仓再反手做多。 - 做空开仓:启用
Enable Sell Entry时,当延迟后的颜色等于4且上一状态不为4。若当前持有多头仓位,将先平仓再反手做空。 - 多头平仓:启用
Enable Buy Exit时,只要延迟后的颜色落入3或4,视为空头占优即刻市价离场。 - 空头平仓:启用
Enable Sell Exit时,只要延迟后的颜色落入0或1,视为多头占优即刻市价离场。
策略会记录每个方向下一根 K 线的收盘时间,以避免在同一信号上重复下单,必须等到下一根 K 线完成后才允许再次开仓。
风险控制
Stop Loss (pts) 与 Take Profit (pts) 以品种的最小价格步长为单位,将原策略中的点数距离转换为绝对价格距离。当数值大于零时,通过 StartProtection 启用对应的止损或止盈;填入零即可关闭该保护措施。
参数
- Range Periods – 计算高低价区间的窗口长度,默认 10。
- Price Smoothing – Fisher 转换前的平滑系数,范围 0…0.99,默认 0.3。
- Index Smoothing – Fisher 转换后的平滑系数,范围 0…0.99,默认 0.3。
- High Level / Low Level – 确定强势区间的上下阈值,默认 +1.01 / –1.01。
- Signal Bar – 信号延迟的已完成 K 线数量,默认 1。
- Enable Buy Entry / Enable Sell Entry – 是否允许开多 / 开空。
- Enable Buy Exit / Enable Sell Exit – 是否允许根据指标平多 / 平空。
- Stop Loss (pts) / Take Profit (pts) – 以价格步长表示的止损、止盈距离。
- Candle Type – 订阅的 K 线类型,默认四小时周期。
备注
- 策略使用 StockSharp 的高级绑定接口
SubscribeCandles().BindEx,除了最少量的颜色历史外不会存储额外序列数据。 - 本次仅提供 C# 版本,不包含 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;
}
}
}
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()