Blau Ergodic MDI 策略
概述
Blau Ergodic Market Directional Indicator(MDI)策略完整复刻了 MetaTrader 智能交易系统 Exp_BlauErgodicMDI 的逻辑。策略读取较高时间周期的蜡烛图(默认 4 小时),对选定的价格源执行三次平滑处理,从而构建动量直方图和信号线。依据所选的入场模式,策略会在以下三种情形中触发信号:
- Breakdown – 直方图穿越零轴时交易。
- Twist – 直方图斜率发生反转时交易。
- CloudTwist – 直方图与信号线发生交叉时交易。
每一次信号都可以根据权限设置选择性地平掉反向仓位并开立新的头寸。
指标流程
- 使用指定的移动平均类型与
PrimaryLength对所选价格进行平滑,得到基准价格。 - 计算动量差值
(price - baseline) / point_value。 - 使用
FirstSmoothingLength与SecondSmoothingLength对动量进行两次平滑,形成直方图。 - 再次以
SignalLength平滑直方图,得到信号线。 - 按照
SignalBarShift缓存历史数值,确保只在已收盘的蜡烛上确认信号。
支持的平滑类型包括 EMA、SMA、SMMA/RMA 与 WMA。价格源的选择与原始 MQL 指标完全一致(收盘价、开盘价、最高价、最低价、中位价、典型价、加权价、简单价、四分价、趋势跟随价等)。
主要参数
| 参数 | 说明 |
|---|---|
Volume |
开仓使用的下单数量。 |
StopLossPoints |
以点数表示的止损距离(0 表示禁用)。 |
TakeProfitPoints |
以点数表示的止盈距离(0 表示禁用)。 |
SlippagePoints |
市价单允许的最大滑点(点数)。 |
AllowLongEntries / AllowShortEntries |
是否允许开多 / 开空。 |
AllowLongExits / AllowShortExits |
是否允许在反向信号出现时平仓。 |
Mode |
入场模式(Breakdown / Twist / CloudTwist)。 |
CandleType |
计算所用蜡烛图的时间框架(默认 4 小时)。 |
SmoothingMethods |
各个平滑步骤使用的移动平均类型。 |
PrimaryLength |
基准价格的平滑周期。 |
FirstSmoothingLength |
动量第一次平滑的周期。 |
SecondSmoothingLength |
构建直方图的第二次平滑周期。 |
SignalLength |
生成信号线的平滑周期。 |
AppliedPrices |
指标计算所使用的价格类型。 |
SignalBarShift |
回溯确认信号的已收盘蜡烛数量。 |
Phase |
为兼容原始脚本保留的参数,当前实现未使用。 |
信号判定
- Breakdown
- 多头:
SignalBarShift对应的直方图大于 0,且前一根直方图不大于 0。 - 空头:
SignalBarShift对应的直方图小于 0,且前一根直方图不小于 0。
- 多头:
- Twist
- 多头:直方图先下降后回升(前一值 < 当前值,且前两根 > 前一根)。
- 空头:直方图先上升后回落(前一值 > 当前值,且前两根 < 前一根)。
- CloudTwist
- 多头:直方图向上穿越信号线(当前直方图 > 当前信号线,前一根直方图 ≤ 前一根信号线)。
- 空头:直方图向下穿越信号线。
若允许平仓,信号会先平掉反向仓位;随后根据权限再开立新仓。
风险控制
策略调用 StartProtection,按照交易品种的最小报价单位(tick size)将止损、止盈和滑点的点数转换成价格距离。当对应参数为 0 时,保护不会启用。
备注
- 仅在蜡烛收盘后处理信号,与 MetaTrader 实现保持一致。
- 通过调整
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()