Blau Ergodic MDI 时间窗口策略
概述
Blau Ergodic MDI Time Strategy 是 MetaTrader 专家顾问 Exp_BlauErgodicMDI_Tm.mq5 的 StockSharp 版本。策略在较高周期的蜡烛图上运行,并完整重现原策略的三个信号模式:Breakdown、Twist 与 CloudTwist。指标计算完全在策略内部完成,通过四层指数移动平均 (EMA) 管道复现 Blau Ergodic MDI 振荡器,同时符合 StockSharp 高层 API 的要求。
指标流水线如下:
- 使用
BaseLength周期的 EMA 对选定价格进行平滑。 - 将平滑结果从原始价格中扣除得到差值序列。
- 依次对差值应用三条 EMA (
FirstSmoothingLength、SecondSmoothingLength、ThirdSmoothingLength)。 - 将中间值(直方图)与最终值(信号线)按品种最小跳动价进行缩放,并将这些值用于交易逻辑。
信号模式
Breakdown 模式
- 分析
SignalBar指定的历史直方图值及其再往前一根的值。 - 若上一根直方图为正、当前检测的历史柱转为非正,则准备做多,并可按需平掉空头。
- 若上一根直方图为负、当前检测柱转为非负,则准备做空,并可按需平掉多头。
Twist 模式
- 比较直方图斜率的变化。
- 当斜率上升(
SignalBar + 1的值小于SignalBar + 2)且当前检测柱高于上一根时,生成做多信号,同时允许平掉空头。 - 当斜率下降(
SignalBar + 1的值大于SignalBar + 2)且当前检测柱低于上一根时,生成做空信号,同时允许平掉多头。
CloudTwist 模式
- 同时使用直方图与信号线。
- 若上一根直方图高于信号线,但当前检测柱跌破信号线,则准备做多并可平空。
- 若上一根直方图低于信号线,但当前检测柱上穿信号线,则准备做空并可平多。
交易时段过滤
策略提供与原版一致的交易时段过滤器,通过 UseTimeFilter、StartHour、StartMinute、EndHour、EndMinute 控制:
- 当开始时间早于结束时间时,交易窗口位于同一天。
- 当开始与结束小时相等时,分钟设置形成该小时内的短窗口。
- 当开始时间晚于结束时间时,交易窗口跨越午夜。
在交易窗口外策略会立即平掉所有仓位,并禁止新的开仓直到窗口重新开启。
风险控制
StopLossPoints 与 TakeProfitPoints 以最小跳动价为单位设置止损与止盈距离。每次开仓后即刻计算保护价格。策略在每根完成的蜡烛上检查价格区间是否触及保护位,一旦触发立刻平仓。
价格来源
PriceMode 列出与 MetaTrader 指标完全一致的价格选项:
| 模式 | 说明 |
|---|---|
| Close | 收盘价。 |
| Open | 开盘价。 |
| High | 最高价。 |
| Low | 最低价。 |
| Median | (High + Low) / 2。 |
| Typical | (High + Low + Close) / 3。 |
| Weighted | (High + Low + 2 × Close) / 4。 |
| Simple | (Open + Close) / 2。 |
| Quarter | (Open + High + Low + Close) / 4。 |
| TrendFollow0 | 多头蜡烛取 High,空头取 Low,十字取 Close。 |
| TrendFollow1 | Close 与蜡烛趋势方向极值的平均。 |
| Demark | Demark 价格计算。 |
参数
| 参数 | 默认值 | 说明 |
|---|---|---|
Mode |
Twist | 选择 Breakdown / Twist / CloudTwist 模式。 |
PriceMode |
Close | 指标使用的价格。 |
BaseLength |
20 | 原始价格 EMA 周期。 |
FirstSmoothingLength |
5 | 差值第一次平滑 EMA 周期。 |
SecondSmoothingLength |
3 | 差值第二次平滑 EMA 周期。 |
ThirdSmoothingLength |
8 | 差值第三次平滑 EMA 周期。 |
SignalBar |
1 | 信号参考的历史柱偏移量。 |
AllowLongEntry / AllowShortEntry |
true | 是否允许开多 / 开空。 |
AllowLongExit / AllowShortExit |
true | 是否允许平多 / 平空。 |
UseTimeFilter |
true | 是否启用交易时段过滤。 |
StartHour, StartMinute, EndHour, EndMinute |
0/0/23/59 | 交易时段设置。 |
StopLossPoints |
1000 | 止损距离(0 代表关闭)。 |
TakeProfitPoints |
2000 | 止盈距离(0 代表关闭)。 |
CandleType |
4 小时 | 指标使用的蜡烛类型。 |
Volume |
0.1 | 下单数量,对应原策略的 MM 参数。 |
交易流程
- 订阅所选时间框架的蜡烛数据。
- 在每根完成的蜡烛上更新四级 EMA 管道,并维护最小所需的历史缓冲。
- 等待历史数据达到要求,再按照所选模式对
SignalBar对应的历史柱进行判断。 - 若触发离场条件或时段过滤关闭交易,则优先平仓。
- 只有在信号触发、交易窗口打开且当前仓位方向允许的情况下才开仓。若需要反向,订单数量会覆盖当前持仓并增加设定的下单量。
- 每根蜡烛都检查止损止盈是否被价格区间触发,并立即执行。
其他说明
- 代码全部使用制表符缩进,符合仓库规范。
StartProtection()在启动时调用一次,以便 StockSharp 的保护机制正确跟踪仓位。- 仅存储信号所需的最少历史值,不会累积大型集合。
- 指标目前使用 EMA 平滑,若需要其他平滑方法,可通过调整周期来近似 MetaTrader 中的 JJMA、VIDYA 或 AMA 版本。
使用步骤
- 将策略类添加到 StockSharp 解决方案并编译。
- 设置品种、蜡烛周期、信号模式、交易时段以及风控参数。
- 将策略连接到提供行情的连接器。
- 启动策略,程序会自动订阅蜡烛并按上述规则管理委托与仓位。
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>
/// Blau Ergodic MDI strategy converted from MetaTrader version 21013.
/// </summary>
public class BlauErgodicMdiTimeStrategy : Strategy
{
/// <summary>
/// Trading strategy based on the Blau Ergodic MDI oscillator with three signal modes.
/// The strategy replicates the behaviour of the original MetaTrader expert advisor
/// by evaluating the oscillator on a higher timeframe and optionally restricting
/// trading to a custom time window.
/// </summary>
public enum BlauErgodicMdiModes
{
/// <summary>
/// Generates entries when the histogram crosses the zero line.
/// </summary>
Breakdown,
/// <summary>
/// Generates entries when the histogram twists and changes slope.
/// </summary>
Twist,
/// <summary>
/// Generates entries when the histogram and its smoothed copy cross.
/// </summary>
CloudTwist
}
/// <summary>
/// Price source used to feed the Blau Ergodic MDI calculation.
/// </summary>
public enum PriceInputModes
{
/// <summary>
/// Candle close price.
/// </summary>
Close,
/// <summary>
/// Candle open price.
/// </summary>
Open,
/// <summary>
/// Candle high price.
/// </summary>
High,
/// <summary>
/// Candle low price.
/// </summary>
Low,
/// <summary>
/// Median price (high + low) / 2.
/// </summary>
Median,
/// <summary>
/// Typical price (high + low + close) / 3.
/// </summary>
Typical,
/// <summary>
/// Weighted close price (high + low + 2 * close) / 4.
/// </summary>
Weighted,
/// <summary>
/// Simplified price (open + close) / 2.
/// </summary>
Simple,
/// <summary>
/// Quarter price (open + high + low + close) / 4.
/// </summary>
Quarter,
/// <summary>
/// Trend follow price - picks the high on bullish candles and the low on bearish candles.
/// </summary>
TrendFollow0,
/// <summary>
/// Half trend follow price - averages close with the extreme price of the candle.
/// </summary>
TrendFollow1,
/// <summary>
/// Demark price calculation.
/// </summary>
Demark
}
private readonly StrategyParam<BlauErgodicMdiModes> _mode;
private readonly StrategyParam<PriceInputModes> _priceMode;
private readonly StrategyParam<int> _baseLength;
private readonly StrategyParam<int> _firstSmoothingLength;
private readonly StrategyParam<int> _secondSmoothingLength;
private readonly StrategyParam<int> _thirdSmoothingLength;
private readonly StrategyParam<int> _signalBar;
private readonly StrategyParam<bool> _allowLongEntry;
private readonly StrategyParam<bool> _allowShortEntry;
private readonly StrategyParam<bool> _allowLongExit;
private readonly StrategyParam<bool> _allowShortExit;
private readonly StrategyParam<bool> _useTimeFilter;
private readonly StrategyParam<int> _startHour;
private readonly StrategyParam<int> _startMinute;
private readonly StrategyParam<int> _endHour;
private readonly StrategyParam<int> _endMinute;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<DataType> _candleType;
private decimal? _priceEma;
private decimal? _diffEma1;
private decimal? _diffEma2;
private decimal? _diffEma3;
private decimal[] _histBuffer = Array.Empty<decimal>();
private decimal[] _signalBuffer = Array.Empty<decimal>();
private DateTimeOffset[] _timeBuffer = Array.Empty<DateTimeOffset>();
private int _bufferCount;
private decimal? _entryPrice;
private decimal? _longStopPrice;
private decimal? _longTakePrice;
private decimal? _shortStopPrice;
private decimal? _shortTakePrice;
private TimeSpan _candleSpan;
private int _barsProcessed;
/// <summary>
/// Selected signal mode.
/// </summary>
public BlauErgodicMdiModes Mode
{
get => _mode.Value;
set => _mode.Value = value;
}
/// <summary>
/// Selected price source.
/// </summary>
public PriceInputModes PriceMode
{
get => _priceMode.Value;
set => _priceMode.Value = value;
}
/// <summary>
/// Base smoothing length applied to the price series.
/// </summary>
public int BaseLength
{
get => _baseLength.Value;
set => _baseLength.Value = value;
}
/// <summary>
/// Length of the first smoothing of the price difference.
/// </summary>
public int FirstSmoothingLength
{
get => _firstSmoothingLength.Value;
set => _firstSmoothingLength.Value = value;
}
/// <summary>
/// Length of the second smoothing stage.
/// </summary>
public int SecondSmoothingLength
{
get => _secondSmoothingLength.Value;
set => _secondSmoothingLength.Value = value;
}
/// <summary>
/// Length of the third smoothing stage.
/// </summary>
public int ThirdSmoothingLength
{
get => _thirdSmoothingLength.Value;
set => _thirdSmoothingLength.Value = value;
}
/// <summary>
/// Number of bars back used for signal evaluation.
/// </summary>
public int SignalBar
{
get => _signalBar.Value;
set => _signalBar.Value = Math.Max(0, value);
}
/// <summary>
/// Enables long entries.
/// </summary>
public bool AllowLongEntry
{
get => _allowLongEntry.Value;
set => _allowLongEntry.Value = value;
}
/// <summary>
/// Enables short entries.
/// </summary>
public bool AllowShortEntry
{
get => _allowShortEntry.Value;
set => _allowShortEntry.Value = value;
}
/// <summary>
/// Enables exits from long positions.
/// </summary>
public bool AllowLongExit
{
get => _allowLongExit.Value;
set => _allowLongExit.Value = value;
}
/// <summary>
/// Enables exits from short positions.
/// </summary>
public bool AllowShortExit
{
get => _allowShortExit.Value;
set => _allowShortExit.Value = value;
}
/// <summary>
/// Enables trading within the configured time range only.
/// </summary>
public bool UseTimeFilter
{
get => _useTimeFilter.Value;
set => _useTimeFilter.Value = value;
}
/// <summary>
/// Start hour of the trading window.
/// </summary>
public int StartHour
{
get => _startHour.Value;
set => _startHour.Value = value;
}
/// <summary>
/// Start minute of the trading window.
/// </summary>
public int StartMinute
{
get => _startMinute.Value;
set => _startMinute.Value = value;
}
/// <summary>
/// End hour of the trading window.
/// </summary>
public int EndHour
{
get => _endHour.Value;
set => _endHour.Value = value;
}
/// <summary>
/// End minute of the trading window.
/// </summary>
public int EndMinute
{
get => _endMinute.Value;
set => _endMinute.Value = value;
}
/// <summary>
/// Stop-loss distance expressed in points (price steps).
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = Math.Max(0, value);
}
/// <summary>
/// Take-profit distance expressed in points (price steps).
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = Math.Max(0, value);
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Constructor.
/// </summary>
public BlauErgodicMdiTimeStrategy()
{
_mode = Param(nameof(Mode), BlauErgodicMdiModes.Twist)
.SetDisplay("Mode", "Signal mode", "General");
_priceMode = Param(nameof(PriceMode), PriceInputModes.Close)
.SetDisplay("Price Mode", "Price input used for the oscillator", "General");
_baseLength = Param(nameof(BaseLength), 20)
.SetGreaterThanZero()
.SetDisplay("Base Length", "Length of the base EMA", "Indicator")
;
_firstSmoothingLength = Param(nameof(FirstSmoothingLength), 5)
.SetGreaterThanZero()
.SetDisplay("First Smooth", "Length of the first smoothing", "Indicator")
;
_secondSmoothingLength = Param(nameof(SecondSmoothingLength), 3)
.SetGreaterThanZero()
.SetDisplay("Second Smooth", "Length of the second smoothing", "Indicator")
;
_thirdSmoothingLength = Param(nameof(ThirdSmoothingLength), 8)
.SetGreaterThanZero()
.SetDisplay("Third Smooth", "Length of the third smoothing", "Indicator")
;
_signalBar = Param(nameof(SignalBar), 1)
.SetDisplay("Signal Bar", "Number of bars back used for the signal", "Indicator");
_allowLongEntry = Param(nameof(AllowLongEntry), true)
.SetDisplay("Allow Long Entry", "Enable long entries", "Trading");
_allowShortEntry = Param(nameof(AllowShortEntry), true)
.SetDisplay("Allow Short Entry", "Enable short entries", "Trading");
_allowLongExit = Param(nameof(AllowLongExit), true)
.SetDisplay("Allow Long Exit", "Enable exits from long positions", "Trading");
_allowShortExit = Param(nameof(AllowShortExit), true)
.SetDisplay("Allow Short Exit", "Enable exits from short positions", "Trading");
_useTimeFilter = Param(nameof(UseTimeFilter), true)
.SetDisplay("Use Time Filter", "Restrict trading to the configured session", "Time Filter");
_startHour = Param(nameof(StartHour), 0)
.SetDisplay("Start Hour", "Hour when trading can start", "Time Filter");
_startMinute = Param(nameof(StartMinute), 0)
.SetDisplay("Start Minute", "Minute when trading can start", "Time Filter");
_endHour = Param(nameof(EndHour), 23)
.SetDisplay("End Hour", "Hour when trading stops", "Time Filter");
_endMinute = Param(nameof(EndMinute), 59)
.SetDisplay("End Minute", "Minute when trading stops", "Time Filter");
_stopLossPoints = Param(nameof(StopLossPoints), 1000)
.SetDisplay("Stop Loss", "Protective stop distance in points", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
.SetDisplay("Take Profit", "Target distance in points", "Risk");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_priceEma = null;
_diffEma1 = null;
_diffEma2 = null;
_diffEma3 = null;
_histBuffer = Array.Empty<decimal>();
_signalBuffer = Array.Empty<decimal>();
_timeBuffer = Array.Empty<DateTimeOffset>();
_bufferCount = 0;
_entryPrice = null;
_longStopPrice = null;
_longTakePrice = null;
_shortStopPrice = null;
_shortTakePrice = null;
_candleSpan = TimeSpan.Zero;
_barsProcessed = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_candleSpan = GetCandleSpan();
var subscription = SubscribeCandles(CandleType);
subscription.Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
{
return;
}
if (CheckRiskManagement(candle))
{
return;
}
var price = GetAppliedPrice(candle);
var baseLength = Math.Max(1, BaseLength);
var firstLength = Math.Max(1, FirstSmoothingLength);
var secondLength = Math.Max(1, SecondSmoothingLength);
var thirdLength = Math.Max(1, ThirdSmoothingLength);
var baseSmoothed = UpdateEma(ref _priceEma, price, baseLength);
var diff = price - baseSmoothed;
var diffSmoothed1 = UpdateEma(ref _diffEma1, diff, firstLength);
var diffSmoothed2 = UpdateEma(ref _diffEma2, diffSmoothed1, secondLength);
var diffSmoothed3 = UpdateEma(ref _diffEma3, diffSmoothed2, thirdLength);
var point = GetPointValue();
var histValue = diffSmoothed2 / point;
var signalValue = diffSmoothed3 / point;
var closeTime = candle.CloseTime != default ? candle.CloseTime : candle.OpenTime + _candleSpan;
_barsProcessed++;
var minimumBars = GetMinimumBars();
var requiredLength = GetRequiredBufferLength();
PushValues(histValue, signalValue, closeTime, requiredLength);
if (_barsProcessed < minimumBars)
{
return;
}
if (!IsFormedAndOnlineAndAllowTrading())
{
return;
}
var tradeWindow = !UseTimeFilter || InTradeWindow(closeTime);
if (UseTimeFilter && !tradeWindow && Position != 0)
{
CloseAllPositions();
return;
}
var signalBar = Math.Max(0, SignalBar);
var buyOpen = false;
var sellOpen = false;
var buyClose = false;
var sellClose = false;
DateTimeOffset? upSignalTime = null;
DateTimeOffset? downSignalTime = null;
switch (Mode)
{
case BlauErgodicMdiModes.Breakdown:
{
if (!HasSufficientData(signalBar + 1))
{
break;
}
var current = _histBuffer[signalBar];
var previous = _histBuffer[signalBar + 1];
var signalTime = _timeBuffer[signalBar];
if (previous > 0m)
{
if (AllowLongEntry && current <= 0m)
{
buyOpen = true;
}
if (AllowShortExit)
{
sellClose = true;
}
upSignalTime = signalTime;
}
if (previous < 0m)
{
if (AllowShortEntry && current >= 0m)
{
sellOpen = true;
}
if (AllowLongExit)
{
buyClose = true;
}
downSignalTime = signalTime;
}
break;
}
case BlauErgodicMdiModes.Twist:
{
if (!HasSufficientData(signalBar + 2))
{
break;
}
var current = _histBuffer[signalBar];
var prev1 = _histBuffer[signalBar + 1];
var prev2 = _histBuffer[signalBar + 2];
var signalTime = _timeBuffer[signalBar];
if (prev1 < prev2)
{
if (AllowLongEntry && current > prev1)
{
buyOpen = true;
}
if (AllowShortExit)
{
sellClose = true;
}
upSignalTime = signalTime;
}
if (prev1 > prev2)
{
if (AllowShortEntry && current < prev1)
{
sellOpen = true;
}
if (AllowLongExit)
{
buyClose = true;
}
downSignalTime = signalTime;
}
break;
}
case BlauErgodicMdiModes.CloudTwist:
{
if (!HasSufficientData(signalBar + 1))
{
break;
}
var currentUp = _histBuffer[signalBar];
var currentDown = _signalBuffer[signalBar];
var prevUp = _histBuffer[signalBar + 1];
var prevDown = _signalBuffer[signalBar + 1];
var signalTime = _timeBuffer[signalBar];
if (prevUp > prevDown)
{
if (AllowLongEntry && currentUp <= currentDown)
{
buyOpen = true;
}
if (AllowShortExit)
{
sellClose = true;
}
upSignalTime = signalTime;
}
if (prevUp < prevDown)
{
if (AllowShortEntry && currentUp >= currentDown)
{
sellOpen = true;
}
if (AllowLongExit)
{
buyClose = true;
}
downSignalTime = signalTime;
}
break;
}
}
if (buyClose && Position > 0)
{
SellMarket(Math.Abs(Position));
ResetRiskLevels();
}
if (sellClose && Position < 0)
{
BuyMarket(Math.Abs(Position));
ResetRiskLevels();
}
if (!tradeWindow)
{
return;
}
if (buyOpen && Position <= 0)
{
var volume = Volume + (Position < 0 ? -Position : 0m);
BuyMarket(volume);
_entryPrice = candle.ClosePrice;
SetRiskForLong(candle.ClosePrice);
}
if (sellOpen && Position >= 0)
{
var volume = Volume + (Position > 0 ? Position : 0m);
SellMarket(volume);
_entryPrice = candle.ClosePrice;
SetRiskForShort(candle.ClosePrice);
}
}
private decimal GetAppliedPrice(ICandleMessage candle)
{
var open = candle.OpenPrice;
var high = candle.HighPrice;
var low = candle.LowPrice;
var close = candle.ClosePrice;
return PriceMode switch
{
PriceInputModes.Open => open,
PriceInputModes.High => high,
PriceInputModes.Low => low,
PriceInputModes.Median => (high + low) / 2m,
PriceInputModes.Typical => (close + high + low) / 3m,
PriceInputModes.Weighted => (2m * close + high + low) / 4m,
PriceInputModes.Simple => (open + close) / 2m,
PriceInputModes.Quarter => (open + high + low + close) / 4m,
PriceInputModes.TrendFollow0 => close > open ? high : close < open ? low : close,
PriceInputModes.TrendFollow1 => close > open ? (high + close) / 2m : close < open ? (low + close) / 2m : close,
PriceInputModes.Demark => CalculateDemarkPrice(open, high, low, close),
_ => close,
};
}
private static decimal CalculateDemarkPrice(decimal open, decimal high, decimal low, decimal close)
{
var res = high + low + close;
if (close < open)
{
res = (res + low) / 2m;
}
else if (close > open)
{
res = (res + high) / 2m;
}
else
{
res = (res + close) / 2m;
}
return ((res - low) + (res - high)) / 2m;
}
private static decimal UpdateEma(ref decimal? previous, decimal value, int length)
{
if (length <= 1)
{
previous = value;
return value;
}
var alpha = 2m / (length + 1m);
var current = previous.HasValue ? previous.Value + alpha * (value - previous.Value) : value;
previous = current;
return current;
}
private decimal GetPointValue()
{
var step = Security?.PriceStep ?? 0m;
return step > 0m ? step : 1m;
}
private int GetMinimumBars()
{
var baseCount = BaseLength + FirstSmoothingLength + SecondSmoothingLength + ThirdSmoothingLength + SignalBar + 3;
return Math.Max(baseCount, GetRequiredBufferLength());
}
private int GetRequiredBufferLength()
{
var signalBar = Math.Max(0, SignalBar);
return Mode switch
{
BlauErgodicMdiModes.Twist => signalBar + 3,
_ => signalBar + 2,
};
}
private void PushValues(decimal hist, decimal signal, DateTimeOffset time, int requiredLength)
{
if (requiredLength <= 0)
{
requiredLength = 1;
}
if (_histBuffer.Length < requiredLength)
{
Array.Resize(ref _histBuffer, requiredLength);
}
if (_signalBuffer.Length < requiredLength)
{
Array.Resize(ref _signalBuffer, requiredLength);
}
if (_timeBuffer.Length < requiredLength)
{
Array.Resize(ref _timeBuffer, requiredLength);
}
var limit = Math.Min(_bufferCount, requiredLength - 1);
for (var i = limit; i > 0; i--)
{
_histBuffer[i] = _histBuffer[i - 1];
_signalBuffer[i] = _signalBuffer[i - 1];
_timeBuffer[i] = _timeBuffer[i - 1];
}
_histBuffer[0] = hist;
_signalBuffer[0] = signal;
_timeBuffer[0] = time;
_bufferCount = Math.Min(requiredLength, _bufferCount + 1);
}
private bool HasSufficientData(int index)
{
return _bufferCount > index;
}
private bool InTradeWindow(DateTimeOffset time)
{
if (!UseTimeFilter)
{
return true;
}
var hour = time.Hour;
var minute = time.Minute;
if (StartHour < EndHour)
{
if (hour == StartHour && minute >= StartMinute)
{
return true;
}
if (hour > StartHour && hour < EndHour)
{
return true;
}
if (hour > StartHour && hour == EndHour && minute < EndMinute)
{
return true;
}
return false;
}
if (StartHour == EndHour)
{
return hour == StartHour && minute >= StartMinute && minute < EndMinute;
}
if (hour >= StartHour && minute >= StartMinute)
{
return true;
}
if (hour < EndHour)
{
return true;
}
if (hour == EndHour && minute < EndMinute)
{
return true;
}
return false;
}
private void CloseAllPositions()
{
if (Position > 0)
{
SellMarket(Math.Abs(Position));
ResetRiskLevels();
}
else if (Position < 0)
{
BuyMarket(Math.Abs(Position));
ResetRiskLevels();
}
}
private void SetRiskForLong(decimal entryPrice)
{
var step = GetPointValue();
_longStopPrice = StopLossPoints > 0 ? entryPrice - StopLossPoints * step : null;
_longTakePrice = TakeProfitPoints > 0 ? entryPrice + TakeProfitPoints * step : null;
_shortStopPrice = null;
_shortTakePrice = null;
}
private void SetRiskForShort(decimal entryPrice)
{
var step = GetPointValue();
_shortStopPrice = StopLossPoints > 0 ? entryPrice + StopLossPoints * step : null;
_shortTakePrice = TakeProfitPoints > 0 ? entryPrice - TakeProfitPoints * step : null;
_longStopPrice = null;
_longTakePrice = null;
}
private void ResetRiskLevels()
{
_entryPrice = null;
_longStopPrice = null;
_longTakePrice = null;
_shortStopPrice = null;
_shortTakePrice = null;
}
private bool CheckRiskManagement(ICandleMessage candle)
{
if (Position > 0)
{
if (_longStopPrice is decimal stop && candle.LowPrice <= stop)
{
SellMarket(Math.Abs(Position));
ResetRiskLevels();
return true;
}
if (_longTakePrice is decimal take && candle.HighPrice >= take)
{
SellMarket(Math.Abs(Position));
ResetRiskLevels();
return true;
}
}
else if (Position < 0)
{
if (_shortStopPrice is decimal stop && candle.HighPrice >= stop)
{
BuyMarket(Math.Abs(Position));
ResetRiskLevels();
return true;
}
if (_shortTakePrice is decimal take && candle.LowPrice <= take)
{
BuyMarket(Math.Abs(Position));
ResetRiskLevels();
return true;
}
}
return false;
}
private TimeSpan GetCandleSpan()
{
return CandleType.Arg switch
{
TimeSpan span => span,
_ => TimeSpan.Zero,
};
}
}
import clr
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
from StockSharp.Algo.Strategies import Strategy
class blau_ergodic_mdi_time_strategy(Strategy):
"""Blau Ergodic MDI with time filter. Computes a custom triple-smoothed
momentum oscillator and generates signals via Twist mode (slope reversal)."""
def __init__(self):
super(blau_ergodic_mdi_time_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe used for calculations", "General")
self._base_length = self.Param("BaseLength", 20) \
.SetDisplay("Base Length", "Length of the base EMA", "Indicator")
self._first_smooth = self.Param("FirstSmooth", 5) \
.SetDisplay("First Smooth", "Length of the first smoothing", "Indicator")
self._second_smooth = self.Param("SecondSmooth", 3) \
.SetDisplay("Second Smooth", "Length of the second smoothing", "Indicator")
self._third_smooth = self.Param("ThirdSmooth", 8) \
.SetDisplay("Third Smooth", "Length of the third smoothing", "Indicator")
self._signal_bar = self.Param("SignalBar", 1) \
.SetDisplay("Signal Bar", "Number of bars back used for the signal", "Indicator")
self._use_time_filter = self.Param("UseTimeFilter", True) \
.SetDisplay("Use Time Filter", "Restrict trading to configured session", "Time Filter")
self._start_hour = self.Param("StartHour", 0) \
.SetDisplay("Start Hour", "Hour when trading can start", "Time Filter")
self._end_hour = self.Param("EndHour", 23) \
.SetDisplay("End Hour", "Hour when trading stops", "Time Filter")
self._price_ema = None
self._diff_ema1 = None
self._diff_ema2 = None
self._diff_ema3 = None
self._hist_buffer = []
self._bars_processed = 0
@property
def CandleType(self):
return self._candle_type.Value
@property
def BaseLength(self):
return self._base_length.Value
@property
def FirstSmooth(self):
return self._first_smooth.Value
@property
def SecondSmooth(self):
return self._second_smooth.Value
@property
def ThirdSmooth(self):
return self._third_smooth.Value
@property
def SignalBar(self):
return self._signal_bar.Value
@property
def UseTimeFilter(self):
return self._use_time_filter.Value
@property
def StartHour(self):
return self._start_hour.Value
@property
def EndHour(self):
return self._end_hour.Value
def OnReseted(self):
super(blau_ergodic_mdi_time_strategy, self).OnReseted()
self._price_ema = None
self._diff_ema1 = None
self._diff_ema2 = None
self._diff_ema3 = None
self._hist_buffer = []
self._bars_processed = 0
def OnStarted2(self, time):
super(blau_ergodic_mdi_time_strategy, self).OnStarted2(time)
self._price_ema = None
self._diff_ema1 = None
self._diff_ema2 = None
self._diff_ema3 = None
self._hist_buffer = []
self._bars_processed = 0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._on_process).Start()
def _update_ema(self, previous, value, length):
if length <= 1:
return value
alpha = 2.0 / (length + 1.0)
if previous is None:
return value
return previous + alpha * (value - previous)
def _on_process(self, candle):
if candle.State != CandleStates.Finished:
return
price = float(candle.ClosePrice)
base_len = max(1, self.BaseLength)
first_len = max(1, self.FirstSmooth)
second_len = max(1, self.SecondSmooth)
third_len = max(1, self.ThirdSmooth)
base_smoothed = self._update_ema(self._price_ema, price, base_len)
self._price_ema = base_smoothed
diff = price - base_smoothed
diff_s1 = self._update_ema(self._diff_ema1, diff, first_len)
self._diff_ema1 = diff_s1
diff_s2 = self._update_ema(self._diff_ema2, diff_s1, second_len)
self._diff_ema2 = diff_s2
diff_s3 = self._update_ema(self._diff_ema3, diff_s2, third_len)
self._diff_ema3 = diff_s3
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
if step <= 0:
step = 1.0
hist_value = diff_s2 / step
self._bars_processed += 1
signal_bar = max(0, self.SignalBar)
required = signal_bar + 3
self._hist_buffer.insert(0, hist_value)
if len(self._hist_buffer) > required:
self._hist_buffer = self._hist_buffer[:required]
minimum_bars = base_len + first_len + second_len + third_len + signal_bar + 3
if self._bars_processed < minimum_bars:
return
if self.UseTimeFilter:
hour = candle.OpenTime.Hour
if not (self.StartHour <= hour <= self.EndHour):
if self.Position != 0:
if self.Position > 0:
self.SellMarket()
else:
self.BuyMarket()
return
if len(self._hist_buffer) < signal_bar + 3:
return
current = self._hist_buffer[signal_bar]
prev1 = self._hist_buffer[signal_bar + 1]
prev2 = self._hist_buffer[signal_bar + 2]
if prev1 < prev2 and current > prev1 and self.Position <= 0:
self.BuyMarket()
elif prev1 > prev2 and current < prev1 and self.Position >= 0:
self.SellMarket()
def CreateClone(self):
return blau_ergodic_mdi_time_strategy()