Стратегия Expert Master EURUSD
Обзор
Стратегия Expert Master EURUSD повторяет работу советника Expert Master для MetaTrader 4.
Она отслеживает конфигурацию из четырёх завершённых свечей по основной и сигнальной линиям MACD (EMA 5/15/3).
Индикатор должен накопить импульс в одном направлении, после чего стратегия входит по пробою в противоположную сторону.
Логика торговли
Условия для покупок
- Сигнальная линия MACD три свечи подряд снижается и на текущей свече разворачивается вверх.
- Основная линия MACD образует «V»-образную форму, а текущее значение выше предыдущих трёх.
- Предыдущее значение основной линии ниже настраиваемого нижнего порога (по умолчанию −0.00020).
- Самое старое значение основной линии ниже нуля, а текущее превышает верхний порог (по умолчанию 0.00020).
Условия для продаж
- Сигнальная линия MACD три свечи подряд растёт и на текущей свече разворачивается вниз.
- Основная линия MACD образует перевёрнутую «V», а текущее значение ниже предыдущих трёх.
- Предыдущее значение основной линии выше верхнего порога (0.00020).
- Самое старое значение основной линии выше нуля, а текущее опускается ниже порога для коротких позиций (−0.00035).
Управление позицией
- Выход при потере импульса. Длинная позиция закрывается, когда текущее значение основной линии MACD падает ниже предыдущего.
Короткая позиция закрывается, когда текущее значение превышает предыдущий бар. - Трейлинг-стоп. После прохождения ценой заданного количества пунктов активируется плавающий стоп.
На каждой закрытой свече стоп обновляется на величину «цена закрытия ± трейлинг».
При возврате цены к стопу позиция закрывается рыночной заявкой.
Управление рисками
- По умолчанию ордер открывается фиксированным объёмом.
Параметр Risk Percent включает динамическое определение объёма как доли стоимости портфеля, что воспроизводит поведение исходного советника.
Параметры
| Имя | Описание | Значение по умолчанию |
|---|---|---|
TrailingPoints |
Расстояние трейлинг-стопа в пунктах. | 25 |
FixedVolume |
Резервный объём сделки, если риск-менеджмент недоступен. | 1 |
RiskPercent |
Доля капитала, используемая для расчёта объёма. | 0.01 |
MacdFastPeriod |
Длина быстрой EMA для основной линии MACD. | 5 |
MacdSlowPeriod |
Длина медленной EMA для основной линии MACD. | 15 |
MacdSignalPeriod |
Период EMA сигнальной линии MACD. | 3 |
UpperMacdThreshold |
Порог положительного MACD для входа. | 0.00020 |
LowerMacdThreshold |
Порог отрицательного MACD для длинных сигналов. | −0.00020 |
ShortCurrentThreshold |
Отрицательный порог MACD для текущего значения при шортах. | −0.00035 |
CandleType |
Тип свечей для расчётов. | Таймфрейм 1 минута |
Примечания
- Решения принимаются только после закрытия свечи, что соответствует высокоуровневому API 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>
/// MACD-based reversal breakout strategy converted from the Expert Master EURUSD MQL4 expert.
/// It observes four-bar patterns on the MACD main and signal lines to detect momentum shifts and enter trades.
/// </summary>
public class ExpertMasterEurusdStrategy : Strategy
{
private readonly StrategyParam<int> _trailingPoints;
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _macdFastPeriod;
private readonly StrategyParam<int> _macdSlowPeriod;
private readonly StrategyParam<int> _macdSignalPeriod;
private readonly StrategyParam<decimal> _upperMacdThreshold;
private readonly StrategyParam<decimal> _lowerMacdThreshold;
private readonly StrategyParam<decimal> _shortCurrentThreshold;
private readonly StrategyParam<DataType> _candleType;
private MovingAverageConvergenceDivergenceSignal _macd = null!;
private decimal? _macdMain0;
private decimal? _macdMain1;
private decimal? _macdMain2;
private decimal? _macdMain3;
private decimal? _signal0;
private decimal? _signal1;
private decimal? _signal2;
private decimal? _signal3;
private decimal _longEntryPrice;
private decimal _shortEntryPrice;
private decimal? _longTrailingStop;
private decimal? _shortTrailingStop;
/// <summary>
/// Trailing stop distance expressed in price points.
/// </summary>
public int TrailingPoints
{
get => _trailingPoints.Value;
set => _trailingPoints.Value = value;
}
/// <summary>
/// Fallback trade volume used when risk sizing returns zero.
/// </summary>
public decimal FixedVolume
{
get => _fixedVolume.Value;
set => _fixedVolume.Value = value;
}
/// <summary>
/// Percentage of portfolio value used to size positions.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Fast EMA period for the MACD main line.
/// </summary>
public int MacdFastPeriod
{
get => _macdFastPeriod.Value;
set => _macdFastPeriod.Value = value;
}
/// <summary>
/// Slow EMA period for the MACD main line.
/// </summary>
public int MacdSlowPeriod
{
get => _macdSlowPeriod.Value;
set => _macdSlowPeriod.Value = value;
}
/// <summary>
/// Signal line period for the MACD indicator.
/// </summary>
public int MacdSignalPeriod
{
get => _macdSignalPeriod.Value;
set => _macdSignalPeriod.Value = value;
}
/// <summary>
/// Positive MACD threshold required for entries.
/// </summary>
public decimal UpperMacdThreshold
{
get => _upperMacdThreshold.Value;
set => _upperMacdThreshold.Value = value;
}
/// <summary>
/// Negative MACD threshold used when building long signals.
/// </summary>
public decimal LowerMacdThreshold
{
get => _lowerMacdThreshold.Value;
set => _lowerMacdThreshold.Value = value;
}
/// <summary>
/// Negative MACD threshold applied to the current value for short entries.
/// </summary>
public decimal ShortCurrentThreshold
{
get => _shortCurrentThreshold.Value;
set => _shortCurrentThreshold.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initialize strategy parameters.
/// </summary>
public ExpertMasterEurusdStrategy()
{
_trailingPoints = Param(nameof(TrailingPoints), 25)
.SetDisplay("Trailing", "Trailing stop distance in points", "Risk")
.SetRange(0, 1000);
_fixedVolume = Param(nameof(FixedVolume), 1m)
.SetDisplay("Fixed Volume", "Fallback trade volume", "Risk")
.SetRange(0.01m, 100m);
_riskPercent = Param(nameof(RiskPercent), 0.01m)
.SetDisplay("Risk Percent", "Portfolio percentage used to size positions", "Risk")
.SetRange(0m, 100m);
_macdFastPeriod = Param(nameof(MacdFastPeriod), 5)
.SetDisplay("MACD Fast", "Fast EMA period", "Indicators")
.SetGreaterThanZero();
_macdSlowPeriod = Param(nameof(MacdSlowPeriod), 15)
.SetDisplay("MACD Slow", "Slow EMA period", "Indicators")
.SetGreaterThanZero();
_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 3)
.SetDisplay("MACD Signal", "Signal EMA period", "Indicators")
.SetGreaterThanZero();
_upperMacdThreshold = Param(nameof(UpperMacdThreshold), 10m)
.SetDisplay("Upper MACD", "Positive MACD threshold", "Logic");
_lowerMacdThreshold = Param(nameof(LowerMacdThreshold), -10m)
.SetDisplay("Lower MACD", "Negative MACD threshold for longs", "Logic");
_shortCurrentThreshold = Param(nameof(ShortCurrentThreshold), -20m)
.SetDisplay("Short MACD", "Negative MACD threshold for shorts", "Logic");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Candle type for MACD", "Data");
}
/// <inheritdoc />
protected override void OnReseted()
{
ResetState();
base.OnReseted();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_macd = new MovingAverageConvergenceDivergenceSignal { Macd = { ShortMa = { Length = MacdFastPeriod }, LongMa = { Length = MacdSlowPeriod } }, SignalMa = { Length = MacdSignalPeriod } };
var subscription = SubscribeCandles(CandleType);
subscription
.BindEx(_macd, ProcessCandle)
.Start();
StartProtection(null, null);
}
private void ProcessCandle(ICandleMessage candle, IIndicatorValue indicatorValue)
{
if (candle.State != CandleStates.Finished)
return;
if (indicatorValue is not MovingAverageConvergenceDivergenceSignalValue macdValue)
return;
if (macdValue.Macd is not decimal macdMain || macdValue.Signal is not decimal macdSignal)
return;
// Cache MACD main and signal values to reproduce the MQL shift logic.
ShiftBuffer(ref _macdMain3, ref _macdMain2, ref _macdMain1, ref _macdMain0, macdMain);
ShiftBuffer(ref _signal3, ref _signal2, ref _signal1, ref _signal0, macdSignal);
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (ManageTrailing(candle))
return;
if (_macdMain3 is null || _macdMain2 is null || _macdMain1 is null || _macdMain0 is null ||
_signal3 is null || _signal2 is null || _signal1 is null || _signal0 is null)
{
return;
}
var mac1 = _macdMain0.Value;
var mac2 = _macdMain1.Value;
var mac3 = _macdMain2.Value;
var mac4 = _macdMain3.Value;
var sig1 = _signal0.Value;
var sig2 = _signal1.Value;
var sig3 = _signal2.Value;
var sig4 = _signal3.Value;
// Long signal replicates the original MACD pattern.
var longSignal = sig4 > sig3 &&
sig3 > sig2 &&
sig2 < sig1 &&
mac4 > mac3 &&
mac3 < mac2 &&
mac2 < mac1 &&
mac2 < LowerMacdThreshold &&
mac4 < 0m &&
mac1 > UpperMacdThreshold;
// Short signal mirrors the MQL condition set.
var shortSignal = sig4 < sig3 &&
sig3 < sig2 &&
sig2 > sig1 &&
mac4 < mac3 &&
mac3 > mac2 &&
mac2 > mac1 &&
mac2 > UpperMacdThreshold &&
mac4 > 0m &&
mac1 < ShortCurrentThreshold;
if (Position == 0)
{
if (longSignal)
{
var volume = GetTradeVolume();
if (volume > 0m)
{
BuyMarket(volume);
_longEntryPrice = candle.ClosePrice;
_longTrailingStop = null;
ResetShort();
}
}
else if (shortSignal)
{
var volume = GetTradeVolume();
if (volume > 0m)
{
SellMarket(volume);
_shortEntryPrice = candle.ClosePrice;
_shortTrailingStop = null;
ResetLong();
}
}
}
else if (Position > 0)
{
if (mac1 < mac2)
{
SellMarket(Position);
ResetLong();
}
}
else if (Position < 0)
{
if (mac1 > mac2)
{
BuyMarket(-Position);
ResetShort();
}
}
}
private void ShiftBuffer(ref decimal? oldest, ref decimal? older, ref decimal? previous, ref decimal? current, decimal value)
{
oldest = older;
older = previous;
previous = current;
current = value;
}
private bool ManageTrailing(ICandleMessage candle)
{
var trailingDistance = GetPriceByPoints(TrailingPoints);
if (TrailingPoints <= 0 || trailingDistance <= 0m)
return false;
if (Position > 0 && _longEntryPrice > 0m)
{
if (candle.HighPrice >= _longEntryPrice + trailingDistance)
{
var newStop = candle.ClosePrice - trailingDistance;
if (!_longTrailingStop.HasValue || newStop > _longTrailingStop.Value)
_longTrailingStop = newStop;
}
if (_longTrailingStop.HasValue && candle.LowPrice <= _longTrailingStop.Value)
{
SellMarket(Position);
ResetLong();
return true;
}
}
else if (Position < 0 && _shortEntryPrice > 0m)
{
if (candle.LowPrice <= _shortEntryPrice - trailingDistance)
{
var newStop = candle.ClosePrice + trailingDistance;
if (!_shortTrailingStop.HasValue || newStop < _shortTrailingStop.Value)
_shortTrailingStop = newStop;
}
if (_shortTrailingStop.HasValue && candle.HighPrice >= _shortTrailingStop.Value)
{
BuyMarket(-Position);
ResetShort();
return true;
}
}
return false;
}
private decimal GetPriceByPoints(int points)
{
if (points <= 0)
return 0m;
var step = Security?.PriceStep ?? 0m;
if (step <= 0m)
step = 1m;
return points * step;
}
private decimal GetTradeVolume()
{
var volume = FixedVolume;
if (RiskPercent > 0m && Portfolio is not null)
{
var equity = Portfolio.CurrentValue ?? 0m;
if (equity > 0m)
{
var riskVolume = equity * (RiskPercent / 100m);
volume = Math.Round(riskVolume, 1, MidpointRounding.AwayFromZero);
}
}
if (volume <= 0m)
volume = FixedVolume;
return Math.Max(volume, 0m);
}
private void ResetLong()
{
_longEntryPrice = 0m;
_longTrailingStop = null;
}
private void ResetShort()
{
_shortEntryPrice = 0m;
_shortTrailingStop = null;
}
private void ResetState()
{
_macdMain0 = null;
_macdMain1 = null;
_macdMain2 = null;
_macdMain3 = null;
_signal0 = null;
_signal1 = null;
_signal2 = null;
_signal3 = null;
ResetLong();
ResetShort();
}
/// <inheritdoc />
protected override void OnStopped()
{
ResetState();
base.OnStopped();
}
}
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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import MovingAverageConvergenceDivergenceSignal
class expert_master_eurusd_strategy(Strategy):
def __init__(self):
super(expert_master_eurusd_strategy, self).__init__()
self._trailing_points = self.Param("TrailingPoints", 25) \
.SetDisplay("Trailing", "Trailing stop distance in points", "Risk")
self._fixed_volume = self.Param("FixedVolume", 1.0) \
.SetDisplay("Fixed Volume", "Fallback trade volume", "Risk")
self._risk_percent = self.Param("RiskPercent", 0.01) \
.SetDisplay("Risk Percent", "Portfolio percentage used to size positions", "Risk")
self._macd_fast_period = self.Param("MacdFastPeriod", 5) \
.SetDisplay("MACD Fast", "Fast EMA period", "Indicators")
self._macd_slow_period = self.Param("MacdSlowPeriod", 15) \
.SetDisplay("MACD Slow", "Slow EMA period", "Indicators")
self._macd_signal_period = self.Param("MacdSignalPeriod", 3) \
.SetDisplay("MACD Signal", "Signal EMA period", "Indicators")
self._upper_macd_threshold = self.Param("UpperMacdThreshold", 10.0) \
.SetDisplay("Upper MACD", "Positive MACD threshold", "Logic")
self._lower_macd_threshold = self.Param("LowerMacdThreshold", -10.0) \
.SetDisplay("Lower MACD", "Negative MACD threshold for longs", "Logic")
self._short_current_threshold = self.Param("ShortCurrentThreshold", -20.0) \
.SetDisplay("Short MACD", "Negative MACD threshold for shorts", "Logic")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2))) \
.SetDisplay("Candle Type", "Candle type for MACD", "Data")
self._macd_main0 = None
self._macd_main1 = None
self._macd_main2 = None
self._macd_main3 = None
self._signal0 = None
self._signal1 = None
self._signal2 = None
self._signal3 = None
self._long_entry_price = 0.0
self._short_entry_price = 0.0
self._long_trailing_stop = None
self._short_trailing_stop = None
self._price_step = 1.0
@property
def TrailingPoints(self):
return self._trailing_points.Value
@property
def FixedVolume(self):
return self._fixed_volume.Value
@property
def RiskPercent(self):
return self._risk_percent.Value
@property
def MacdFastPeriod(self):
return self._macd_fast_period.Value
@property
def MacdSlowPeriod(self):
return self._macd_slow_period.Value
@property
def MacdSignalPeriod(self):
return self._macd_signal_period.Value
@property
def UpperMacdThreshold(self):
return self._upper_macd_threshold.Value
@property
def LowerMacdThreshold(self):
return self._lower_macd_threshold.Value
@property
def ShortCurrentThreshold(self):
return self._short_current_threshold.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(expert_master_eurusd_strategy, self).OnStarted2(time)
self._price_step = 1.0
if self.Security is not None and self.Security.PriceStep is not None:
ps = float(self.Security.PriceStep)
if ps > 0:
self._price_step = ps
self._macd = MovingAverageConvergenceDivergenceSignal()
self._macd.Macd.ShortMa.Length = self.MacdFastPeriod
self._macd.Macd.LongMa.Length = self.MacdSlowPeriod
self._macd.SignalMa.Length = self.MacdSignalPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.BindEx(self._macd, self.ProcessCandle).Start()
def ProcessCandle(self, candle, indicator_value):
if candle.State != CandleStates.Finished:
return
if not indicator_value.IsFinal:
return
macd_main = None
macd_signal = None
if hasattr(indicator_value, 'Macd') and hasattr(indicator_value, 'Signal'):
m = indicator_value.Macd
s = indicator_value.Signal
if m is not None:
macd_main = float(m)
if s is not None:
macd_signal = float(s)
if macd_main is None or macd_signal is None:
return
# Shift buffer: oldest <- older <- previous <- current <- new
self._macd_main3 = self._macd_main2
self._macd_main2 = self._macd_main1
self._macd_main1 = self._macd_main0
self._macd_main0 = macd_main
self._signal3 = self._signal2
self._signal2 = self._signal1
self._signal1 = self._signal0
self._signal0 = macd_signal
if self._manage_trailing(candle):
return
if (self._macd_main3 is None or self._macd_main2 is None or
self._macd_main1 is None or self._macd_main0 is None or
self._signal3 is None or self._signal2 is None or
self._signal1 is None or self._signal0 is None):
return
mac1 = self._macd_main0
mac2 = self._macd_main1
mac3 = self._macd_main2
mac4 = self._macd_main3
sig1 = self._signal0
sig2 = self._signal1
sig3 = self._signal2
sig4 = self._signal3
upper_thresh = float(self.UpperMacdThreshold)
lower_thresh = float(self.LowerMacdThreshold)
short_thresh = float(self.ShortCurrentThreshold)
long_signal = (sig4 > sig3 and sig3 > sig2 and sig2 < sig1 and
mac4 > mac3 and mac3 < mac2 and mac2 < mac1 and
mac2 < lower_thresh and mac4 < 0 and mac1 > upper_thresh)
short_signal = (sig4 < sig3 and sig3 < sig2 and sig2 > sig1 and
mac4 < mac3 and mac3 > mac2 and mac2 > mac1 and
mac2 > upper_thresh and mac4 > 0 and mac1 < short_thresh)
if self.Position == 0:
if long_signal:
volume = self._get_trade_volume()
if volume > 0:
self.BuyMarket(volume)
self._long_entry_price = float(candle.ClosePrice)
self._long_trailing_stop = None
self._reset_short()
elif short_signal:
volume = self._get_trade_volume()
if volume > 0:
self.SellMarket(volume)
self._short_entry_price = float(candle.ClosePrice)
self._short_trailing_stop = None
self._reset_long()
elif self.Position > 0:
if mac1 < mac2:
self.SellMarket(Math.Abs(self.Position))
self._reset_long()
elif self.Position < 0:
if mac1 > mac2:
self.BuyMarket(Math.Abs(self.Position))
self._reset_short()
def _manage_trailing(self, candle):
trailing_pts = int(self.TrailingPoints)
if trailing_pts <= 0:
return False
trailing_distance = trailing_pts * self._price_step
if trailing_distance <= 0:
return False
if self.Position > 0 and self._long_entry_price > 0:
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if high >= self._long_entry_price + trailing_distance:
new_stop = close - trailing_distance
if self._long_trailing_stop is None or new_stop > self._long_trailing_stop:
self._long_trailing_stop = new_stop
if self._long_trailing_stop is not None and low <= self._long_trailing_stop:
self.SellMarket(Math.Abs(self.Position))
self._reset_long()
return True
elif self.Position < 0 and self._short_entry_price > 0:
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
if low <= self._short_entry_price - trailing_distance:
new_stop = close + trailing_distance
if self._short_trailing_stop is None or new_stop < self._short_trailing_stop:
self._short_trailing_stop = new_stop
if self._short_trailing_stop is not None and high >= self._short_trailing_stop:
self.BuyMarket(Math.Abs(self.Position))
self._reset_short()
return True
return False
def _get_trade_volume(self):
volume = float(self.FixedVolume)
risk_pct = float(self.RiskPercent)
if risk_pct > 0 and self.Portfolio is not None:
cv = self.Portfolio.CurrentValue
equity = float(cv) if cv is not None and float(cv) > 0 else 0.0
if equity > 0:
risk_volume = equity * (risk_pct / 100.0)
volume = round(risk_volume, 1)
if volume <= 0:
volume = float(self.FixedVolume)
return max(volume, 0.0)
def _reset_long(self):
self._long_entry_price = 0.0
self._long_trailing_stop = None
def _reset_short(self):
self._short_entry_price = 0.0
self._short_trailing_stop = None
def OnReseted(self):
super(expert_master_eurusd_strategy, self).OnReseted()
self._macd_main0 = None
self._macd_main1 = None
self._macd_main2 = None
self._macd_main3 = None
self._signal0 = None
self._signal1 = None
self._signal2 = None
self._signal3 = None
self._long_entry_price = 0.0
self._short_entry_price = 0.0
self._long_trailing_stop = None
self._short_trailing_stop = None
self._price_step = 1.0
def CreateClone(self):
return expert_master_eurusd_strategy()