Стратегия Blau Ergodic MDI
Общее описание
Стратегия Blau Ergodic Market Directional Indicator повторяет работу советника MetaTrader Exp_BlauErgodicMDI. Алгоритм получает поток свечей старшего таймфрейма (по умолчанию 4 часа), выполняет тройное сглаживание выбранной ценовой серии и формирует гистограмму момента и сигнальную линию. Торговые решения принимаются в одном из трёх режимов:
- Breakdown – вход при пересечении гистограммой нулевой линии.
- Twist – вход при смене направления наклона гистограммы.
- CloudTwist – вход при пересечении гистограммы и сигнальной линии.
Каждый сигнал может закрывать противоположные позиции и/или открывать новые сделки в зависимости от установленных разрешений.
Логика индикатора
- Сгладить выбранную цену указанным типом средней с периодом
PrimaryLength, получив базовую линию. - Рассчитать момент
(price - baseline) / point_value. - Дважды сгладить момент периодами
FirstSmoothingLengthиSecondSmoothingLength, построив гистограмму. - Ещё раз сгладить гистограмму с периодом
SignalLength, получив сигнальную линию. - Сохранить значения согласно
SignalBarShift, чтобы анализировать только закрытые свечи.
Поддерживаются методы сглаживания EMA, SMA, SMMA/RMA и WMA. Выбор цены полностью соответствует оригинальному индикатору (close, open, high, low, median, typical, weighted, simple, quarter, trend-following варианты).
Параметры
| Параметр | Описание |
|---|---|
Volume |
Объём заявки при открытии позиции. |
StopLossPoints |
Стоп-лосс в пунктах (0 — отключено). |
TakeProfitPoints |
Тейк-профит в пунктах (0 — отключено). |
SlippagePoints |
Допустимое отклонение цены в пунктах для рыночных заявок. |
AllowLongEntries / AllowShortEntries |
Разрешение на открытие длинных / коротких позиций. |
AllowLongExits / AllowShortExits |
Разрешение на закрытие текущих позиций встречным сигналом. |
Mode |
Режим генерации сигналов (Breakdown / Twist / CloudTwist). |
CandleType |
Таймфрейм свечей, используемых в расчётах (по умолчанию 4H). |
SmoothingMethods |
Семейство скользящей средней для всех этапов сглаживания. |
PrimaryLength |
Период сглаживания базовой цены. |
FirstSmoothingLength |
Период первого сглаживания момента. |
SecondSmoothingLength |
Период второго сглаживания (формирование гистограммы). |
SignalLength |
Период сглаживания гистограммы для построения сигнальной линии. |
AppliedPrices |
Тип цены, участвующей в расчёте. |
SignalBarShift |
Число закрытых баров, на основании которых подтверждается сигнал. |
Phase |
Зарезервированный параметр для совместимости, в текущей реализации не используется. |
Условия сделок
- Breakdown
- Покупка: гистограмма на
SignalBarShift> 0 и на предыдущей свече ≤ 0. - Продажа: гистограмма на
SignalBarShift< 0 и на предыдущей свече ≥ 0.
- Покупка: гистограмма на
- Twist
- Покупка: гистограмма разворачивается вверх (предыдущее значение < текущего, а значение двумя барами ранее > предыдущего).
- Продажа: гистограмма разворачивается вниз (предыдущее значение > текущего, а значение двумя барами ранее < предыдущего).
- CloudTwist
- Покупка: гистограмма пересекает сигнальную линию снизу вверх (текущее значение гистограммы > сигнального, предыдущее ≤ сигнального).
- Продажа: гистограмма пересекает сигнальную линию сверху вниз.
При наличии разрешений сначала закрываются противоположные позиции, затем открывается новая сделка заданным объёмом.
Управление рисками
Функция StartProtection активируется со стоп-лоссом и тейк-профитом, пересчитанными из пунктов в денежные значения с помощью шага цены инструмента. Если расстояние равно нулю, соответствующая защита не включается. Параметр SlippagePoints конвертируется аналогичным образом.
Дополнительно
- Сигналы обрабатываются только по завершённым свечам, что повторяет поведение оригинального советника.
SignalBarShiftпозволяет подтверждать сигналы на более старых барах, уменьшая вероятность ложных входов.- Параметр
Phaseсохранён для совместимости и не влияет на расчёты в поддерживаемых методах сглаживания. - Все комментарии в коде написаны на английском языке для удобства международной команды.
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 Market Directional Indicator strategy converted from MetaTrader.
/// Uses a triple-smoothed momentum histogram with configurable entry confirmation modes.
/// </summary>
public class BlauErgodicMdiStrategy : Strategy
{
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private readonly StrategyParam<int> _slippagePoints;
private readonly StrategyParam<bool> _allowLongEntries;
private readonly StrategyParam<bool> _allowShortEntries;
private readonly StrategyParam<bool> _allowLongExits;
private readonly StrategyParam<bool> _allowShortExits;
private readonly StrategyParam<EntryModes> _entryMode;
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<SmoothingMethods> _smoothingMethod;
private readonly StrategyParam<int> _primaryLength;
private readonly StrategyParam<int> _firstSmoothingLength;
private readonly StrategyParam<int> _secondSmoothingLength;
private readonly StrategyParam<int> _signalLength;
private readonly StrategyParam<AppliedPrices> _appliedPrice;
private readonly StrategyParam<int> _signalBarShift;
private readonly StrategyParam<int> _phase;
private IIndicator _priceAverage = null!;
private IIndicator _firstSmoothing = null!;
private IIndicator _secondSmoothing = null!;
private IIndicator _signalSmoothing = null!;
private decimal[] _histogramBuffer = Array.Empty<decimal>();
private decimal[] _signalBuffer = Array.Empty<decimal>();
private int _bufferIndex;
private int _bufferFilled;
private decimal _pointValue = 1m;
private decimal _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
/// <summary>
/// Initializes a new instance of <see cref="BlauErgodicMdiStrategy"/>.
/// </summary>
public BlauErgodicMdiStrategy()
{
_stopLossPoints = Param(nameof(StopLossPoints), 1000)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop loss in points", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
.SetNotNegative()
.SetDisplay("Take Profit", "Take profit in points", "Risk");
_slippagePoints = Param(nameof(SlippagePoints), 10)
.SetNotNegative()
.SetDisplay("Slippage", "Maximum slippage in points", "Risk");
_allowLongEntries = Param(nameof(AllowLongEntries), true)
.SetDisplay("Allow Long Entries", "Enable opening long positions", "Permissions");
_allowShortEntries = Param(nameof(AllowShortEntries), true)
.SetDisplay("Allow Short Entries", "Enable opening short positions", "Permissions");
_allowLongExits = Param(nameof(AllowLongExits), true)
.SetDisplay("Allow Long Exits", "Enable closing long positions", "Permissions");
_allowShortExits = Param(nameof(AllowShortExits), true)
.SetDisplay("Allow Short Exits", "Enable closing short positions", "Permissions");
_entryMode = Param(nameof(Mode), EntryModes.Twist)
.SetDisplay("Entry Mode", "Signal interpretation mode", "Strategy");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Indicator Timeframe", "Timeframe used for calculations", "Data");
_smoothingMethod = Param(nameof(SmoothingMethod), SmoothingMethods.Exponential)
.SetDisplay("Smoothing Method", "Type of moving average", "Indicator");
_primaryLength = Param(nameof(PrimaryLength), 20)
.SetGreaterThanZero()
.SetDisplay("Primary Length", "Base smoothing length", "Indicator")
.SetOptimize(5, 60, 1);
_firstSmoothingLength = Param(nameof(FirstSmoothingLength), 5)
.SetGreaterThanZero()
.SetDisplay("Momentum Smoothing", "First smoothing length", "Indicator")
.SetOptimize(2, 20, 1);
_secondSmoothingLength = Param(nameof(SecondSmoothingLength), 3)
.SetGreaterThanZero()
.SetDisplay("Histogram Smoothing", "Second smoothing length", "Indicator")
.SetOptimize(2, 20, 1);
_signalLength = Param(nameof(SignalLength), 8)
.SetGreaterThanZero()
.SetDisplay("Signal Length", "Signal line smoothing", "Indicator")
.SetOptimize(2, 30, 1);
_appliedPrice = Param(nameof(AppliedPrice), AppliedPrices.Close)
.SetDisplay("Applied Price", "Price source for calculations", "Indicator");
_signalBarShift = Param(nameof(SignalBarShift), 1)
.SetNotNegative()
.SetDisplay("Signal Bar", "Shift of the bar used for signals", "Strategy");
_phase = Param(nameof(Phase), 15)
.SetDisplay("Phase", "Reserved smoothing phase parameter", "Indicator");
}
/// <summary>
/// Stop loss distance expressed in instrument points.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance expressed in instrument points.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Allowed price slippage in points.
/// </summary>
public int SlippagePoints
{
get => _slippagePoints.Value;
set => _slippagePoints.Value = value;
}
/// <summary>
/// Enables opening long positions.
/// </summary>
public bool AllowLongEntries
{
get => _allowLongEntries.Value;
set => _allowLongEntries.Value = value;
}
/// <summary>
/// Enables opening short positions.
/// </summary>
public bool AllowShortEntries
{
get => _allowShortEntries.Value;
set => _allowShortEntries.Value = value;
}
/// <summary>
/// Enables closing existing long positions on opposite signals.
/// </summary>
public bool AllowLongExits
{
get => _allowLongExits.Value;
set => _allowLongExits.Value = value;
}
/// <summary>
/// Enables closing existing short positions on opposite signals.
/// </summary>
public bool AllowShortExits
{
get => _allowShortExits.Value;
set => _allowShortExits.Value = value;
}
/// <summary>
/// Selected entry confirmation mode.
/// </summary>
public EntryModes Mode
{
get => _entryMode.Value;
set => _entryMode.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Moving average family used for smoothing steps.
/// </summary>
public SmoothingMethods SmoothingMethod
{
get => _smoothingMethod.Value;
set => _smoothingMethod.Value = value;
}
/// <summary>
/// Length for the initial smoothing of price.
/// </summary>
public int PrimaryLength
{
get => _primaryLength.Value;
set => _primaryLength.Value = value;
}
/// <summary>
/// Length of the first smoothing applied to momentum.
/// </summary>
public int FirstSmoothingLength
{
get => _firstSmoothingLength.Value;
set => _firstSmoothingLength.Value = value;
}
/// <summary>
/// Length of the second smoothing forming the histogram.
/// </summary>
public int SecondSmoothingLength
{
get => _secondSmoothingLength.Value;
set => _secondSmoothingLength.Value = value;
}
/// <summary>
/// Length of the signal line smoothing.
/// </summary>
public int SignalLength
{
get => _signalLength.Value;
set => _signalLength.Value = value;
}
/// <summary>
/// Applied price selection for calculations.
/// </summary>
public AppliedPrices AppliedPrice
{
get => _appliedPrice.Value;
set => _appliedPrice.Value = value;
}
/// <summary>
/// Offset of the bar used for signal confirmation.
/// </summary>
public int SignalBarShift
{
get => _signalBarShift.Value;
set => _signalBarShift.Value = value;
}
/// <summary>
/// Reserved phase parameter kept for compatibility with the original script.
/// </summary>
public int Phase
{
get => _phase.Value;
set => _phase.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_histogramBuffer = Array.Empty<decimal>();
_signalBuffer = Array.Empty<decimal>();
_bufferIndex = 0;
_bufferFilled = 0;
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointValue = Security?.PriceStep ?? 1m;
if (_pointValue <= 0m)
_pointValue = 1m;
_priceAverage = CreateMovingAverage(SmoothingMethod, PrimaryLength);
_firstSmoothing = CreateMovingAverage(SmoothingMethod, FirstSmoothingLength);
_secondSmoothing = CreateMovingAverage(SmoothingMethod, SecondSmoothingLength);
_signalSmoothing = CreateMovingAverage(SmoothingMethod, SignalLength);
InitializeBuffers();
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
ApplyRiskManagement(candle);
var price = SelectPrice(candle);
var time = candle.CloseTime;
// Smooth the selected price to match the indicator baseline.
var baseValue = _priceAverage.Process(new DecimalIndicatorValue(_priceAverage, price, time) { IsFinal = true });
if (!baseValue.IsFormed)
return;
var basePrice = baseValue.ToDecimal();
var momentum = _pointValue != 0m ? (price - basePrice) / _pointValue : 0m;
// Apply the first momentum smoothing stage.
var firstValue = _firstSmoothing.Process(new DecimalIndicatorValue(_firstSmoothing, momentum, time) { IsFinal = true });
if (!firstValue.IsFormed)
return;
var first = firstValue.ToDecimal();
// Build the histogram with the second smoothing stage.
var secondValue = _secondSmoothing.Process(new DecimalIndicatorValue(_secondSmoothing, first, time) { IsFinal = true });
if (!secondValue.IsFormed)
return;
var histogram = secondValue.ToDecimal();
// Smooth the histogram to generate the signal line.
var signalValue = _signalSmoothing.Process(new DecimalIndicatorValue(_signalSmoothing, histogram, time) { IsFinal = true });
if (!signalValue.IsFormed)
return;
var signal = signalValue.ToDecimal();
// Store values so that shifted comparisons work like in the MQL version.
AddToBuffer(histogram, signal);
if (!TryGetHist(SignalBarShift, out var latestHist) || !TryGetHist(SignalBarShift + 1, out var previousHist))
return;
var currentPosition = Position;
var buySignal = false;
var sellSignal = false;
switch (Mode)
{
case EntryModes.Breakdown:
{
buySignal = latestHist > 0m && previousHist <= 0m;
sellSignal = latestHist < 0m && previousHist >= 0m;
break;
}
case EntryModes.Twist:
{
if (!TryGetHist(SignalBarShift + 2, out var olderHist))
return;
buySignal = previousHist < latestHist && olderHist > previousHist;
sellSignal = previousHist > latestHist && olderHist < previousHist;
break;
}
case EntryModes.CloudTwist:
{
if (!TryGetSignal(SignalBarShift, out var latestSignal) || !TryGetSignal(SignalBarShift + 1, out var previousSignal))
return;
buySignal = latestHist > latestSignal && previousHist <= previousSignal;
sellSignal = latestHist < latestSignal && previousHist >= previousSignal;
break;
}
}
if (buySignal)
{
ExecuteBuy(currentPosition, candle.ClosePrice);
}
else if (sellSignal)
{
ExecuteSell(currentPosition, candle.ClosePrice);
}
}
private void ExecuteBuy(decimal currentPosition, decimal price)
{
var volume = 0m;
if (AllowShortExits && currentPosition < 0m)
volume += Math.Abs(currentPosition);
if (AllowLongEntries && (currentPosition <= 0m || (AllowShortExits && currentPosition < 0m)))
volume += Volume;
if (volume > 0m)
{
BuyMarket(volume);
_entryPrice = price;
var slDist = StopLossPoints > 0 ? StopLossPoints * _pointValue : 0m;
var tpDist = TakeProfitPoints > 0 ? TakeProfitPoints * _pointValue : 0m;
_stopPrice = slDist > 0m ? price - slDist : null;
_takePrice = tpDist > 0m ? price + tpDist : null;
}
}
private void ExecuteSell(decimal currentPosition, decimal price)
{
var volume = 0m;
if (AllowLongExits && currentPosition > 0m)
volume += Math.Abs(currentPosition);
if (AllowShortEntries && (currentPosition >= 0m || (AllowLongExits && currentPosition > 0m)))
volume += Volume;
if (volume > 0m)
{
SellMarket(volume);
_entryPrice = price;
var slDist = StopLossPoints > 0 ? StopLossPoints * _pointValue : 0m;
var tpDist = TakeProfitPoints > 0 ? TakeProfitPoints * _pointValue : 0m;
_stopPrice = slDist > 0m ? price + slDist : null;
_takePrice = tpDist > 0m ? price - tpDist : null;
}
}
private void ApplyRiskManagement(ICandleMessage candle)
{
if (Position > 0)
{
if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
{
SellMarket(Position);
ResetTargets();
return;
}
if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
{
SellMarket(Position);
ResetTargets();
}
}
else if (Position < 0)
{
if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetTargets();
return;
}
if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
{
BuyMarket(Math.Abs(Position));
ResetTargets();
}
}
}
private void ResetTargets()
{
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
}
private void InitializeBuffers()
{
var size = Math.Max(3, SignalBarShift + 3);
_histogramBuffer = new decimal[size];
_signalBuffer = new decimal[size];
_bufferIndex = 0;
_bufferFilled = 0;
}
private void AddToBuffer(decimal histogram, decimal signal)
{
if (_histogramBuffer.Length == 0)
return;
_histogramBuffer[_bufferIndex] = histogram;
_signalBuffer[_bufferIndex] = signal;
_bufferIndex = (_bufferIndex + 1) % _histogramBuffer.Length;
if (_bufferFilled < _histogramBuffer.Length)
_bufferFilled++;
}
private bool TryGetHist(int shift, out decimal value)
{
return TryGetBufferedValue(_histogramBuffer, shift, out value);
}
private bool TryGetSignal(int shift, out decimal value)
{
return TryGetBufferedValue(_signalBuffer, shift, out value);
}
private bool TryGetBufferedValue(decimal[] buffer, int shift, out decimal value)
{
value = default;
if (shift < 0 || shift >= _bufferFilled)
return false;
var index = _bufferIndex - 1 - shift;
if (index < 0)
index += buffer.Length;
value = buffer[index];
return true;
}
private decimal SelectPrice(ICandleMessage candle)
{
return AppliedPrice switch
{
AppliedPrices.Open => candle.OpenPrice,
AppliedPrices.High => candle.HighPrice,
AppliedPrices.Low => candle.LowPrice,
AppliedPrices.Median => (candle.HighPrice + candle.LowPrice) / 2m,
AppliedPrices.Typical => (candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 3m,
AppliedPrices.Weighted => (2m * candle.ClosePrice + candle.HighPrice + candle.LowPrice) / 4m,
AppliedPrices.Simple => (candle.OpenPrice + candle.ClosePrice) / 2m,
AppliedPrices.Quarter => (candle.OpenPrice + candle.HighPrice + candle.LowPrice + candle.ClosePrice) / 4m,
AppliedPrices.TrendFollow0 => candle.ClosePrice > candle.OpenPrice ? candle.HighPrice : candle.ClosePrice < candle.OpenPrice ? candle.LowPrice : candle.ClosePrice,
AppliedPrices.TrendFollow1 => candle.ClosePrice > candle.OpenPrice ? (candle.HighPrice + candle.ClosePrice) / 2m : candle.ClosePrice < candle.OpenPrice ? (candle.LowPrice + candle.ClosePrice) / 2m : candle.ClosePrice,
_ => candle.ClosePrice,
};
}
private static IIndicator CreateMovingAverage(SmoothingMethods method, int length)
{
return method switch
{
SmoothingMethods.Simple => new SimpleMovingAverage { Length = length },
SmoothingMethods.Smoothed => new SmoothedMovingAverage { Length = length },
SmoothingMethods.Weighted => new WeightedMovingAverage { Length = length },
_ => new ExponentialMovingAverage { Length = length },
};
}
/// <summary>
/// Entry confirmation modes replicated from the original expert advisor.
/// </summary>
public enum EntryModes
{
/// <summary>
/// Histogram breaks above or below the zero line.
/// </summary>
Breakdown,
/// <summary>
/// Histogram changes slope direction.
/// </summary>
Twist,
/// <summary>
/// Histogram crosses the signal line.
/// </summary>
CloudTwist
}
/// <summary>
/// Supported smoothing families.
/// </summary>
public enum SmoothingMethods
{
/// <summary>
/// Exponential moving average.
/// </summary>
Exponential,
/// <summary>
/// Simple moving average.
/// </summary>
Simple,
/// <summary>
/// Smoothed (RMA) moving average.
/// </summary>
Smoothed,
/// <summary>
/// Weighted moving average.
/// </summary>
Weighted
}
/// <summary>
/// Applied price sources identical to the MetaTrader version.
/// </summary>
public enum AppliedPrices
{
/// <summary>
/// Close price.
/// </summary>
Close,
/// <summary>
/// Open price.
/// </summary>
Open,
/// <summary>
/// High price.
/// </summary>
High,
/// <summary>
/// Low price.
/// </summary>
Low,
/// <summary>
/// Median price (high + low) / 2.
/// </summary>
Median,
/// <summary>
/// Typical price (close + high + low) / 3.
/// </summary>
Typical,
/// <summary>
/// Weighted price (2 * close + high + low) / 4.
/// </summary>
Weighted,
/// <summary>
/// Simple price (open + close) / 2.
/// </summary>
Simple,
/// <summary>
/// Quarted price (open + high + low + close) / 4.
/// </summary>
Quarter,
/// <summary>
/// Trend-following price using candle extremes.
/// </summary>
TrendFollow0,
/// <summary>
/// Half-trend-following price.
/// </summary>
TrendFollow1
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math, Decimal
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import (
SimpleMovingAverage,
ExponentialMovingAverage,
SmoothedMovingAverage,
WeightedMovingAverage,
)
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class blau_ergodic_mdi_strategy(Strategy):
MODE_BREAKDOWN = 0
MODE_TWIST = 1
MODE_CLOUD_TWIST = 2
SMOOTH_EMA = 0
SMOOTH_SMA = 1
SMOOTH_SMMA = 2
SMOOTH_WMA = 3
AP_CLOSE = 0
AP_OPEN = 1
AP_HIGH = 2
AP_LOW = 3
AP_MEDIAN = 4
AP_TYPICAL = 5
AP_WEIGHTED = 6
AP_SIMPLE = 7
AP_QUARTER = 8
AP_TREND0 = 9
AP_TREND1 = 10
def __init__(self):
super(blau_ergodic_mdi_strategy, self).__init__()
self._stop_loss_points = self.Param("StopLossPoints", 1000)
self._take_profit_points = self.Param("TakeProfitPoints", 2000)
self._slippage_points = self.Param("SlippagePoints", 10)
self._allow_long_entries = self.Param("AllowLongEntries", True)
self._allow_short_entries = self.Param("AllowShortEntries", True)
self._allow_long_exits = self.Param("AllowLongExits", True)
self._allow_short_exits = self.Param("AllowShortExits", True)
self._entry_mode = self.Param("Mode", self.MODE_TWIST)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._smoothing_method = self.Param("SmoothingMethod", self.SMOOTH_EMA)
self._primary_length = self.Param("PrimaryLength", 20)
self._first_smoothing_length = self.Param("FirstSmoothingLength", 5)
self._second_smoothing_length = self.Param("SecondSmoothingLength", 3)
self._signal_length = self.Param("SignalLength", 8)
self._applied_price = self.Param("AppliedPrice", self.AP_CLOSE)
self._signal_bar_shift = self.Param("SignalBarShift", 1)
self._phase = self.Param("Phase", 15)
self._price_average = None
self._first_smoothing = None
self._second_smoothing = None
self._signal_smoothing = None
self._histogram_buffer = []
self._signal_buffer = []
self._buffer_index = 0
self._buffer_filled = 0
self._point_value = 1.0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def AllowLongEntries(self):
return self._allow_long_entries.Value
@property
def AllowShortEntries(self):
return self._allow_short_entries.Value
@property
def AllowLongExits(self):
return self._allow_long_exits.Value
@property
def AllowShortExits(self):
return self._allow_short_exits.Value
@property
def Mode(self):
return self._entry_mode.Value
@property
def CandleType(self):
return self._candle_type.Value
@property
def SmoothingMethod(self):
return self._smoothing_method.Value
@property
def PrimaryLength(self):
return self._primary_length.Value
@property
def FirstSmoothingLength(self):
return self._first_smoothing_length.Value
@property
def SecondSmoothingLength(self):
return self._second_smoothing_length.Value
@property
def SignalLength(self):
return self._signal_length.Value
@property
def AppliedPrice(self):
return self._applied_price.Value
@property
def SignalBarShift(self):
return self._signal_bar_shift.Value
@property
def Phase(self):
return self._phase.Value
def OnStarted2(self, time):
super(blau_ergodic_mdi_strategy, self).OnStarted2(time)
sec = self.Security
self._point_value = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
self._price_average = self._create_ma(self.SmoothingMethod, self.PrimaryLength)
self._first_smoothing = self._create_ma(self.SmoothingMethod, self.FirstSmoothingLength)
self._second_smoothing = self._create_ma(self.SmoothingMethod, self.SecondSmoothingLength)
self._signal_smoothing = self._create_ma(self.SmoothingMethod, self.SignalLength)
self._initialize_buffers()
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._apply_risk_management(candle)
price = self._select_price(candle)
t = candle.ServerTime
base_val = process_float(self._price_average, Decimal(float(price)), t, True)
if not self._price_average.IsFormed:
return
base_price = float(base_val.Value)
momentum = (float(price) - base_price) / self._point_value if self._point_value != 0 else 0.0
first_val = process_float(self._first_smoothing, Decimal(momentum), t, True)
if not self._first_smoothing.IsFormed:
return
second_val = process_float(self._second_smoothing, Decimal(float(first_val.Value)), t, True)
if not self._second_smoothing.IsFormed:
return
histogram = float(second_val.Value)
signal_val = process_float(self._signal_smoothing, Decimal(histogram), t, True)
if not self._signal_smoothing.IsFormed:
return
signal = float(signal_val.Value)
self._add_to_buffer(histogram, signal)
shift = self.SignalBarShift
latest_hist = self._try_get_hist(shift)
prev_hist = self._try_get_hist(shift + 1)
if latest_hist is None or prev_hist is None:
return
pos = float(self.Position)
buy_signal = False
sell_signal = False
mode = self.Mode
if mode == self.MODE_BREAKDOWN:
buy_signal = latest_hist > 0 and prev_hist <= 0
sell_signal = latest_hist < 0 and prev_hist >= 0
elif mode == self.MODE_TWIST:
older_hist = self._try_get_hist(shift + 2)
if older_hist is None:
return
buy_signal = prev_hist < latest_hist and older_hist > prev_hist
sell_signal = prev_hist > latest_hist and older_hist < prev_hist
elif mode == self.MODE_CLOUD_TWIST:
latest_sig = self._try_get_signal(shift)
prev_sig = self._try_get_signal(shift + 1)
if latest_sig is None or prev_sig is None:
return
buy_signal = latest_hist > latest_sig and prev_hist <= prev_sig
sell_signal = latest_hist < latest_sig and prev_hist >= prev_sig
if buy_signal:
self._execute_buy(pos, float(candle.ClosePrice))
elif sell_signal:
self._execute_sell(pos, float(candle.ClosePrice))
def _execute_buy(self, current_pos, price):
volume = 0.0
if self.AllowShortExits and current_pos < 0:
volume += abs(current_pos)
if self.AllowLongEntries and (current_pos <= 0 or (self.AllowShortExits and current_pos < 0)):
volume += float(self.Volume)
if volume > 0:
self.BuyMarket(volume)
self._entry_price = price
sl_dist = self.StopLossPoints * self._point_value if self.StopLossPoints > 0 else 0.0
tp_dist = self.TakeProfitPoints * self._point_value if self.TakeProfitPoints > 0 else 0.0
self._stop_price = price - sl_dist if sl_dist > 0 else None
self._take_price = price + tp_dist if tp_dist > 0 else None
def _execute_sell(self, current_pos, price):
volume = 0.0
if self.AllowLongExits and current_pos > 0:
volume += abs(current_pos)
if self.AllowShortEntries and (current_pos >= 0 or (self.AllowLongExits and current_pos > 0)):
volume += float(self.Volume)
if volume > 0:
self.SellMarket(volume)
self._entry_price = price
sl_dist = self.StopLossPoints * self._point_value if self.StopLossPoints > 0 else 0.0
tp_dist = self.TakeProfitPoints * self._point_value if self.TakeProfitPoints > 0 else 0.0
self._stop_price = price + sl_dist if sl_dist > 0 else None
self._take_price = price - tp_dist if tp_dist > 0 else None
def _apply_risk_management(self, candle):
pos = float(self.Position)
if pos > 0:
if self._stop_price is not None and float(candle.LowPrice) <= self._stop_price:
self.SellMarket(pos)
self._reset_targets()
return
if self._take_price is not None and float(candle.HighPrice) >= self._take_price:
self.SellMarket(pos)
self._reset_targets()
elif pos < 0:
if self._stop_price is not None and float(candle.HighPrice) >= self._stop_price:
self.BuyMarket(abs(pos))
self._reset_targets()
return
if self._take_price is not None and float(candle.LowPrice) <= self._take_price:
self.BuyMarket(abs(pos))
self._reset_targets()
def _reset_targets(self):
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def _initialize_buffers(self):
size = max(3, self.SignalBarShift + 3)
self._histogram_buffer = [0.0] * size
self._signal_buffer = [0.0] * size
self._buffer_index = 0
self._buffer_filled = 0
def _add_to_buffer(self, histogram, signal):
if len(self._histogram_buffer) == 0:
return
self._histogram_buffer[self._buffer_index] = histogram
self._signal_buffer[self._buffer_index] = signal
self._buffer_index = (self._buffer_index + 1) % len(self._histogram_buffer)
if self._buffer_filled < len(self._histogram_buffer):
self._buffer_filled += 1
def _try_get_hist(self, shift):
return self._try_get_buffered(self._histogram_buffer, shift)
def _try_get_signal(self, shift):
return self._try_get_buffered(self._signal_buffer, shift)
def _try_get_buffered(self, buf, shift):
if shift < 0 or shift >= self._buffer_filled:
return None
idx = self._buffer_index - 1 - shift
if idx < 0:
idx += len(buf)
return buf[idx]
def _select_price(self, candle):
ap = self.AppliedPrice
if ap == self.AP_OPEN:
return candle.OpenPrice
elif ap == self.AP_HIGH:
return candle.HighPrice
elif ap == self.AP_LOW:
return candle.LowPrice
elif ap == self.AP_MEDIAN:
return (float(candle.HighPrice) + float(candle.LowPrice)) / 2.0
elif ap == self.AP_TYPICAL:
return (float(candle.ClosePrice) + float(candle.HighPrice) + float(candle.LowPrice)) / 3.0
elif ap == self.AP_WEIGHTED:
return (2.0 * float(candle.ClosePrice) + float(candle.HighPrice) + float(candle.LowPrice)) / 4.0
elif ap == self.AP_SIMPLE:
return (float(candle.OpenPrice) + float(candle.ClosePrice)) / 2.0
elif ap == self.AP_QUARTER:
return (float(candle.OpenPrice) + float(candle.HighPrice) + float(candle.LowPrice) + float(candle.ClosePrice)) / 4.0
elif ap == self.AP_TREND0:
if float(candle.ClosePrice) > float(candle.OpenPrice):
return candle.HighPrice
elif float(candle.ClosePrice) < float(candle.OpenPrice):
return candle.LowPrice
else:
return candle.ClosePrice
elif ap == self.AP_TREND1:
if float(candle.ClosePrice) > float(candle.OpenPrice):
return (float(candle.HighPrice) + float(candle.ClosePrice)) / 2.0
elif float(candle.ClosePrice) < float(candle.OpenPrice):
return (float(candle.LowPrice) + float(candle.ClosePrice)) / 2.0
else:
return candle.ClosePrice
else:
return candle.ClosePrice
def _create_ma(self, method, length):
if method == self.SMOOTH_SMA:
ma = SimpleMovingAverage()
ma.Length = length
return ma
elif method == self.SMOOTH_SMMA:
ma = SmoothedMovingAverage()
ma.Length = length
return ma
elif method == self.SMOOTH_WMA:
ma = WeightedMovingAverage()
ma.Length = length
return ma
else:
ma = ExponentialMovingAverage()
ma.Length = length
return ma
def OnReseted(self):
super(blau_ergodic_mdi_strategy, self).OnReseted()
self._histogram_buffer = []
self._signal_buffer = []
self._buffer_index = 0
self._buffer_filled = 0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def CreateClone(self):
return blau_ergodic_mdi_strategy()