Macd Pattern Trader v02 (puerto StockSharp)
Esta estrategia es una conversión API de alto nivel de StockSharp del experto MetaTrader MacdPatternTraderv02.mq4 (directorio MQL/8194). Reproduce la detección de patrones MACD original y las reglas de gestión de posición activa al tiempo que expone parámetros convenientes para una mayor optimización.
Idea central
- Calcule la línea principal MACD usando los períodos EMA rápido y lento (
FastEmaPeriod,SlowEmaPeriod) con una longitud de señal de una vela (que coincide con la versión MQL). - Supervise únicamente las velas completadas. Cuando el valor MACD pinte una secuencia específica de tres barras alrededor de la línea cero, active el patrón corto o largo:
- Patrón corto: requiere una fase MACD positiva seguida de un retroceso negativo por encima de
MinThresholdy luego una inflexión a la baja. - Patrón largo: requiere una fase MACD negativa seguida de un retroceso positivo por debajo de
MaxThresholdy luego una inflexión alcista.
- Patrón corto: requiere una fase MACD positiva seguida de un retroceso negativo por encima de
- Ejecute órdenes de mercado usando
TradeVolumeuna vez que se confirme el patrón. - Proteja cada posición con un stop-loss colocado más allá del extremo de oscilación reciente (sobre
StopLossBarsvelas) más una compensación adicional en puntos (OffsetPoints). - Defina el nivel de obtención de beneficios escaneando
TakeProfitBarssegmentos consecutivos y seleccionando el máximo/mínimo más extremo alcanzado mientras la secuencia sigue imprimiendo nuevos extremos. - Administre las posiciones abiertas con el administrador de posiciones activo del experto original: después de lograr una ganancia mínima de cinco puntos, la estrategia cierra un tercio del volumen cuando la vela anterior confirma la tendencia (filtro
Ema2Period) y otra mitad cuando el precio interactúa con la línea media deSmaPeriodyEma3Period.
Parámetros
| Parámetro | Descripción |
|---|---|
StopLossBars |
Número de velas completadas inspeccionadas al calcular el extremo de oscilación del stop-loss. |
TakeProfitBars |
Tamaño de ventana (en velas) para la búsqueda secuencial de extremos que construye el objetivo de obtención de beneficios. |
OffsetPoints |
Compensación adicional, expresada en puntos del instrumento, agregada al stop-loss. |
FastEmaPeriod |
Longitud rápida de EMA para la línea principal MACD. |
SlowEmaPeriod |
Longitud lenta de EMA para la línea principal MACD. |
MaxThreshold |
Umbral positivo MACD que finaliza la preparación del patrón corto. |
MinThreshold |
Umbral negativo MACD que finaliza la preparación del patrón largo. |
Ema1Period |
Primer período EMA utilizado por el bloque de administración de dinero original (se conserva para que esté completo). |
Ema2Period |
Segundo período EMA utilizado para validar el beneficio parcial para posiciones largas/cortas. |
SmaPeriod |
SMA período utilizado en el segundo activador de cierre parcial. |
Ema3Period |
Período EMA lenta combinado con SMA para detectar salidas de reversión a la media. |
TradeVolume |
Volumen de órdenes de mercado (lotes). |
CandleType |
Tipo de datos de vela utilizado para alimentar todos los indicadores. |
Lógica de trading
- Entrada corta: se activa cuando la secuencia MACD
(prev3, prev2, prev1, current)coincide con las condiciones originales (macdPrev1 < macdPrev3,macdPrev1 > macdPrev2,current < prev1,current < 0y verificación de magnitud). La exposición larga existente se aplana antes de abrir una nueva posición corta. - Entrada larga: reglas simétricas donde
current > 0, los valores MACD forman el patrón de imagen especular y se cumple la verificación de magnitud. La exposición corta existente se aplana antes de abrir una nueva posición larga. - Paradas y objetivos: se calcula inmediatamente después de cada entrada y se actualiza solo cuando se ejecuta una nueva operación.
- Cierres parciales: una vez que la ganancia alcanza cinco puntos (en relación con el tamaño de puntos del instrumento), la estrategia cierra un tercio del volumen restante si la vela anterior cierra más allá de
EMA2. La siguiente etapa cierra la mitad del volumen restante cuando la vela anterior atraviesa el promedio deSMAyEMA3. - Salida completa: cualquier toque del precio en el nivel de stop-loss o take-profit cierra la posición completa. Después de cada salida forzada, el estado interno se restablece automáticamente.
Notas
- El tamaño en puntos se deriva de
Security.PriceStepo, cuando no esté disponible, de los decimales de seguridad. Se utiliza un valor predeterminado de0.0001como respaldo seguro. - El historial de velas se almacena (hasta 1024 entradas) para replicar las funciones auxiliares MQL
iHighest,iLowesty el escaneo extremo secuencial deTakeProfit(). - Todos los comentarios dentro de la estrategia permanecen en inglés, como lo exigen las pautas del repositorio.
- Los puertos de Python se omiten intencionalmente para esta tarea.
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 pattern trader strategy converted from the "MacdPatternTraderv02" MetaTrader expert.
/// The strategy monitors the MACD main line and opens positions when the characteristic reversal pattern appears.
/// It also manages open positions using the original partial close logic based on moving averages.
/// </summary>
public class MacdPatternTraderV02Strategy : Strategy
{
private readonly StrategyParam<decimal> _profitThresholdPoints;
private readonly StrategyParam<int> _maxHistory;
private readonly StrategyParam<int> _stopLossBars;
private readonly StrategyParam<int> _takeProfitBars;
private readonly StrategyParam<int> _offsetPoints;
private readonly StrategyParam<int> _fastEmaPeriod;
private readonly StrategyParam<int> _slowEmaPeriod;
private readonly StrategyParam<decimal> _maxThreshold;
private readonly StrategyParam<decimal> _minThreshold;
private readonly StrategyParam<int> _ema1Period;
private readonly StrategyParam<int> _ema2Period;
private readonly StrategyParam<int> _smaPeriod;
private readonly StrategyParam<int> _ema3Period;
private readonly StrategyParam<decimal> _tradeVolume;
private readonly StrategyParam<DataType> _candleType;
private MovingAverageConvergenceDivergence _macd = null!;
private ExponentialMovingAverage _ema1 = null!;
private ExponentialMovingAverage _ema2 = null!;
private SimpleMovingAverage _sma = null!;
private ExponentialMovingAverage _ema3 = null!;
private readonly List<ICandleMessage> _history = new();
private decimal? _ema1Prev;
private decimal? _ema2Prev;
private decimal? _smaPrev;
private decimal? _ema3Prev;
private decimal? _ema1Last;
private decimal? _ema2Last;
private decimal? _smaLast;
private decimal? _ema3Last;
private decimal? _macdPrev1;
private decimal? _macdPrev2;
private decimal? _macdPrev3;
private bool _maxThresholdReached;
private bool _minThresholdReached;
private bool _sellPatternReady;
private bool _buyPatternReady;
private decimal _patternMinValue;
private decimal _patternMaxValue;
private decimal _pointSize;
private int _entryDirection;
private decimal _entryPrice;
private decimal _openVolume;
private decimal? _stopLossPrice;
private decimal? _takeProfitPrice;
private int _longPartialStage;
private int _shortPartialStage;
/// <summary>
/// Number of bars used to calculate protective stop-loss levels.
/// </summary>
public int StopLossBars
{
get => _stopLossBars.Value;
set => _stopLossBars.Value = value;
}
/// <summary>
/// Number of bars evaluated when searching for take-profit targets.
/// </summary>
public int TakeProfitBars
{
get => _takeProfitBars.Value;
set => _takeProfitBars.Value = value;
}
/// <summary>
/// Offset applied to stop-loss levels expressed in price points.
/// </summary>
public int OffsetPoints
{
get => _offsetPoints.Value;
set => _offsetPoints.Value = value;
}
/// <summary>
/// Minimal profit in points required before partial exits are considered.
/// </summary>
public decimal ProfitThresholdPoints
{
get => _profitThresholdPoints.Value;
set => _profitThresholdPoints.Value = value;
}
/// <summary>
/// Fast EMA period for the MACD main line.
/// </summary>
public int FastEmaPeriod
{
get => _fastEmaPeriod.Value;
set => _fastEmaPeriod.Value = value;
}
/// <summary>
/// Slow EMA period for the MACD main line.
/// </summary>
public int SlowEmaPeriod
{
get => _slowEmaPeriod.Value;
set => _slowEmaPeriod.Value = value;
}
/// <summary>
/// Upper MACD threshold that arms the short pattern.
/// </summary>
public decimal MaxThreshold
{
get => _maxThreshold.Value;
set => _maxThreshold.Value = value;
}
/// <summary>
/// Lower MACD threshold that arms the long pattern.
/// </summary>
public decimal MinThreshold
{
get => _minThreshold.Value;
set => _minThreshold.Value = value;
}
/// <summary>
/// Period of the first EMA used in the partial close logic.
/// </summary>
public int Ema1Period
{
get => _ema1Period.Value;
set => _ema1Period.Value = value;
}
/// <summary>
/// Period of the second EMA used in the partial close logic.
/// </summary>
public int Ema2Period
{
get => _ema2Period.Value;
set => _ema2Period.Value = value;
}
/// <summary>
/// Period of the SMA used to detect profit taking levels.
/// </summary>
public int SmaPeriod
{
get => _smaPeriod.Value;
set => _smaPeriod.Value = value;
}
/// <summary>
/// Period of the slow EMA used in the partial close logic.
/// </summary>
public int Ema3Period
{
get => _ema3Period.Value;
set => _ema3Period.Value = value;
}
/// <summary>
/// Trading volume applied to market orders.
/// </summary>
public decimal TradeVolume
{
get => _tradeVolume.Value;
set => _tradeVolume.Value = value;
}
/// <summary>
/// Candle type used for indicator calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Maximum number of finished candles stored in the sliding history window.
/// </summary>
public int MaxHistory
{
get => _maxHistory.Value;
set => _maxHistory.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="MacdPatternTraderV02Strategy"/> class.
/// </summary>
public MacdPatternTraderV02Strategy()
{
_stopLossBars = Param(nameof(StopLossBars), 6)
.SetGreaterThanZero()
.SetDisplay("Stop-Loss Bars", "Number of candles for stop-loss calculation", "Risk");
_takeProfitBars = Param(nameof(TakeProfitBars), 20)
.SetGreaterThanZero()
.SetDisplay("Take-Profit Bars", "Window used when scanning for take-profit", "Risk");
_offsetPoints = Param(nameof(OffsetPoints), 10)
.SetGreaterThanZero()
.SetDisplay("Offset Points", "Additional protective offset in points", "Risk");
_profitThresholdPoints = Param(nameof(ProfitThresholdPoints), 500m)
.SetGreaterThanZero()
.SetDisplay("Profit Threshold Points", "Minimal profit in points before partial exits", "Risk");
_fastEmaPeriod = Param(nameof(FastEmaPeriod), 12)
.SetGreaterThanZero()
.SetDisplay("Fast EMA", "Fast EMA period for MACD", "Indicators");
_slowEmaPeriod = Param(nameof(SlowEmaPeriod), 26)
.SetGreaterThanZero()
.SetDisplay("Slow EMA", "Slow EMA period for MACD", "Indicators");
_maxThreshold = Param(nameof(MaxThreshold), 50m)
.SetDisplay("Upper Threshold", "Maximum MACD threshold for longs", "Signals");
_minThreshold = Param(nameof(MinThreshold), -50m)
.SetDisplay("Lower Threshold", "Minimum MACD threshold for shorts", "Signals");
_ema1Period = Param(nameof(Ema1Period), 7)
.SetGreaterThanZero()
.SetDisplay("EMA 1", "First EMA period for management", "Management");
_ema2Period = Param(nameof(Ema2Period), 21)
.SetGreaterThanZero()
.SetDisplay("EMA 2", "Second EMA period for management", "Management");
_smaPeriod = Param(nameof(SmaPeriod), 98)
.SetGreaterThanZero()
.SetDisplay("SMA", "SMA period for management", "Management");
_ema3Period = Param(nameof(Ema3Period), 365)
.SetGreaterThanZero()
.SetDisplay("EMA 3", "Slow EMA period for management", "Management");
_tradeVolume = Param(nameof(TradeVolume), 0.1m)
.SetGreaterThanZero()
.SetDisplay("Trade Volume", "Market order volume", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
.SetDisplay("Candle Type", "Candle type used for indicators", "General");
_maxHistory = Param(nameof(MaxHistory), 1024)
.SetGreaterThanZero()
.SetDisplay("History Limit", "Maximum candles stored for pattern recognition", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_history.Clear();
_macdPrev1 = null;
_macdPrev2 = null;
_macdPrev3 = null;
_ema1Prev = null;
_ema2Prev = null;
_smaPrev = null;
_ema3Prev = null;
_ema1Last = null;
_ema2Last = null;
_smaLast = null;
_ema3Last = null;
_maxThresholdReached = false;
_minThresholdReached = false;
_sellPatternReady = false;
_buyPatternReady = false;
_patternMinValue = 0m;
_patternMaxValue = 0m;
_pointSize = 0m;
ResetPositionState();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_pointSize = Security?.PriceStep ?? 0m;
if (_pointSize <= 0m)
{
var decimals = Security?.Decimals;
if (decimals.HasValue)
_pointSize = (decimal)Math.Pow(10, -decimals.Value);
}
if (_pointSize <= 0m)
_pointSize = 0.0001m;
_macd = new MovingAverageConvergenceDivergence();
_macd.ShortMa.Length = FastEmaPeriod;
_macd.LongMa.Length = SlowEmaPeriod;
_ema1 = new EMA { Length = Ema1Period };
_ema2 = new EMA { Length = Ema2Period };
_sma = new SMA { Length = SmaPeriod };
_ema3 = new EMA { Length = Ema3Period };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_macd, _ema1, _ema2, _sma, _ema3, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _macd);
DrawIndicator(area, _ema1);
DrawIndicator(area, _ema2);
DrawIndicator(area, _sma);
DrawIndicator(area, _ema3);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal macdLine, decimal ema1Value, decimal ema2Value, decimal smaValue, decimal ema3Value)
{
if (candle.State != CandleStates.Finished)
return;
_ema1Prev = _ema1Last;
_ema2Prev = _ema2Last;
_smaPrev = _smaLast;
_ema3Prev = _ema3Last;
_ema1Last = ema1Value;
_ema2Last = ema2Value;
_smaLast = smaValue;
_ema3Last = ema3Value;
if (!_macd.IsFormed)
return;
var macdLast = _macdPrev1;
var macdLast2 = _macdPrev2;
var macdLast3 = _macdPrev3;
if (macdLast is null || macdLast2 is null || macdLast3 is null)
{
_macdPrev3 = _macdPrev2;
_macdPrev2 = _macdPrev1;
_macdPrev1 = macdLine;
AddCandle(candle);
return;
}
AddCandle(candle);
ExecutePatternLogic(candle, macdLine, macdLast.Value, macdLast2.Value, macdLast3.Value);
_macdPrev3 = _macdPrev2;
_macdPrev2 = _macdPrev1;
_macdPrev1 = macdLine;
}
private void ExecutePatternLogic(ICandleMessage candle, decimal macdCurrent, decimal macdPrev1, decimal macdPrev2, decimal macdPrev3)
{
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (_pointSize <= 0m)
return;
if (macdCurrent > 0m)
{
_maxThresholdReached = true;
_sellPatternReady = false;
}
if (macdCurrent > macdPrev1 && macdPrev1 < macdPrev3 && _maxThresholdReached && macdCurrent > MinThreshold && macdCurrent < 0m && !_sellPatternReady)
{
_sellPatternReady = true;
_patternMinValue = Math.Abs(macdPrev1 * 10000m);
}
var currentMagnitude = Math.Abs(macdCurrent * 10000m);
if (_sellPatternReady && macdCurrent < macdPrev1 && macdPrev1 > macdPrev3 && macdCurrent < 0m && _patternMinValue <= currentMagnitude)
{
_maxThresholdReached = false;
}
if (_sellPatternReady && macdCurrent < macdPrev1 && macdPrev1 > macdPrev3 && macdCurrent < 0m)
{
TryOpenShort(candle);
_sellPatternReady = false;
_maxThresholdReached = false;
}
if (macdCurrent < 0m)
{
_minThresholdReached = true;
_buyPatternReady = false;
}
if (macdCurrent < MaxThreshold && macdCurrent < macdPrev1 && macdPrev1 > macdPrev3 && _minThresholdReached && macdCurrent > 0m && !_buyPatternReady)
{
_buyPatternReady = true;
_patternMaxValue = Math.Abs(macdPrev1 * 10000m);
}
if (_buyPatternReady && macdCurrent > macdPrev1 && macdPrev1 < macdPrev3 && macdCurrent > 0m && _patternMaxValue <= currentMagnitude)
{
_minThresholdReached = false;
}
if (_buyPatternReady && macdCurrent > macdPrev1 && macdPrev1 < macdPrev3 && macdCurrent > 0m)
{
TryOpenLong(candle);
_buyPatternReady = false;
_minThresholdReached = false;
}
ManagePosition(candle);
}
private void TryOpenShort(ICandleMessage candle)
{
if (Position > 0m)
{
var closeVolume = NormalizeVolume(Math.Abs(Position));
if (closeVolume > 0m)
{
SellMarket(closeVolume);
ResetPositionState();
}
}
if (Position < 0m)
return;
var volume = NormalizeVolume(TradeVolume);
if (volume <= 0m)
return;
var entryPrice = candle.ClosePrice;
SellMarket(volume);
RegisterEntry(-1, entryPrice, volume);
}
private void TryOpenLong(ICandleMessage candle)
{
if (Position < 0m)
{
var closeVolume = NormalizeVolume(Math.Abs(Position));
if (closeVolume > 0m)
{
BuyMarket(closeVolume);
ResetPositionState();
}
}
if (Position > 0m)
return;
var volume = NormalizeVolume(TradeVolume);
if (volume <= 0m)
return;
var entryPrice = candle.ClosePrice;
BuyMarket(volume);
RegisterEntry(1, entryPrice, volume);
}
private void RegisterEntry(int direction, decimal entryPrice, decimal volume)
{
_entryDirection = direction;
_entryPrice = entryPrice;
_openVolume = volume;
_stopLossPrice = direction > 0 ? CalculateLongStop() : CalculateShortStop();
_takeProfitPrice = direction > 0 ? CalculateLongTarget() : CalculateShortTarget();
_longPartialStage = 0;
_shortPartialStage = 0;
}
private void ManagePosition(ICandleMessage candle)
{
if (_entryDirection == 0 || _openVolume <= 0m)
return;
if (CheckRiskManagement(candle))
return;
var previousCandle = GetCandle(1);
if (previousCandle is null || _ema2Prev is null || _ema3Prev is null || _smaPrev is null)
return;
var ema2Prev = _ema2Prev.Value;
var ema3Prev = _ema3Prev.Value;
var smaPrev = _smaPrev.Value;
var profitPoints = CalculateOpenProfitPoints(candle.ClosePrice);
if (_entryDirection > 0)
{
if (profitPoints > ProfitThresholdPoints && previousCandle.ClosePrice > ema2Prev && _longPartialStage == 0)
{
var volume = NormalizeVolume(_openVolume / 3m);
if (volume > 0m)
{
SellMarket(volume);
RegisterClose(volume, candle.ClosePrice);
_longPartialStage = 1;
}
}
else if (profitPoints > ProfitThresholdPoints && previousCandle.HighPrice > (smaPrev + ema3Prev) / 2m && _longPartialStage == 1)
{
var volume = NormalizeVolume(_openVolume / 2m);
if (volume > 0m)
{
SellMarket(volume);
RegisterClose(volume, candle.ClosePrice);
_longPartialStage = 2;
}
}
}
else if (_entryDirection < 0)
{
if (profitPoints > ProfitThresholdPoints && previousCandle.ClosePrice < ema2Prev && _shortPartialStage == 0)
{
var volume = NormalizeVolume(_openVolume / 3m);
if (volume > 0m)
{
BuyMarket(volume);
RegisterClose(volume, candle.ClosePrice);
_shortPartialStage = 1;
}
}
else if (profitPoints > ProfitThresholdPoints && previousCandle.LowPrice < (smaPrev + ema3Prev) / 2m && _shortPartialStage == 1)
{
var volume = NormalizeVolume(_openVolume / 2m);
if (volume > 0m)
{
BuyMarket(volume);
RegisterClose(volume, candle.ClosePrice);
_shortPartialStage = 2;
}
}
}
}
private bool CheckRiskManagement(ICandleMessage candle)
{
if (_entryDirection == 0 || _openVolume <= 0m)
return false;
if (_entryDirection > 0)
{
if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
{
SellMarket(_openVolume);
ResetPositionState();
return true;
}
if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
{
SellMarket(_openVolume);
ResetPositionState();
return true;
}
}
else
{
if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
{
BuyMarket(_openVolume);
ResetPositionState();
return true;
}
if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
{
BuyMarket(_openVolume);
ResetPositionState();
return true;
}
}
return false;
}
private decimal CalculateOpenProfitPoints(decimal currentPrice)
{
if (_pointSize <= 0m)
return 0m;
var difference = _entryDirection > 0 ? currentPrice - _entryPrice : _entryPrice - currentPrice;
return Math.Abs(difference / _pointSize);
}
private void RegisterClose(decimal volume, decimal price)
{
_openVolume -= volume;
if (_openVolume <= 0m || Math.Abs(Position) < 1e-6m)
ResetPositionState();
}
private void ResetPositionState()
{
_entryDirection = 0;
_entryPrice = 0m;
_openVolume = 0m;
_stopLossPrice = null;
_takeProfitPrice = null;
_longPartialStage = 0;
_shortPartialStage = 0;
}
private decimal? CalculateShortStop()
{
var candles = GetCandlesRange(StopLossBars, 1);
if (candles.Count == 0)
return null;
var highest = decimal.MinValue;
foreach (var candle in candles)
highest = Math.Max(highest, candle.HighPrice);
return highest + OffsetPoints * _pointSize;
}
private decimal? CalculateLongStop()
{
var candles = GetCandlesRange(StopLossBars, 1);
if (candles.Count == 0)
return null;
var lowest = decimal.MaxValue;
foreach (var candle in candles)
lowest = Math.Min(lowest, candle.LowPrice);
return lowest - OffsetPoints * _pointSize;
}
private decimal? CalculateShortTarget()
{
return ScanSequentialExtremum(TakeProfitBars, true);
}
private decimal? CalculateLongTarget()
{
return ScanSequentialExtremum(TakeProfitBars, false);
}
private decimal? ScanSequentialExtremum(int window, bool isShort)
{
if (window <= 0)
return null;
decimal? best = null;
var shift = 0;
while (true)
{
var candles = GetCandlesRange(window, shift);
if (candles.Count == 0)
break;
decimal candidate;
if (isShort)
{
candidate = decimal.MaxValue;
foreach (var candle in candles)
candidate = Math.Min(candidate, candle.LowPrice);
if (best is null || candidate < best)
{
best = candidate;
shift += window;
continue;
}
}
else
{
candidate = decimal.MinValue;
foreach (var candle in candles)
candidate = Math.Max(candidate, candle.HighPrice);
if (best is null || candidate > best)
{
best = candidate;
shift += window;
continue;
}
}
break;
}
return best;
}
private List<ICandleMessage> GetCandlesRange(int length, int shift)
{
var result = new List<ICandleMessage>();
if (length <= 0)
return result;
var startIndex = _history.Count - 1 - shift;
for (var i = startIndex; i >= 0 && result.Count < length; i--)
result.Add(_history[i]);
return result;
}
private ICandleMessage GetCandle(int shift)
{
var index = _history.Count - 1 - shift;
if (index < 0 || index >= _history.Count)
return null;
return _history[index];
}
private void AddCandle(ICandleMessage candle)
{
_history.Add(candle);
if (_history.Count > MaxHistory)
_history.RemoveAt(0);
}
private decimal NormalizeVolume(decimal volume)
{
var security = Security;
if (security?.VolumeStep is { } step && step > 0m)
volume = Math.Round(volume / step) * step;
if (security?.MinVolume is { } minVolume && minVolume > 0m && volume < minVolume)
return 0m;
return volume.Max(0m);
}
}
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 (
MovingAverageConvergenceDivergence, ExponentialMovingAverage,
SimpleMovingAverage
)
class macd_pattern_trader_v02_strategy(Strategy):
def __init__(self):
super(macd_pattern_trader_v02_strategy, self).__init__()
self._stop_loss_bars = self.Param("StopLossBars", 6).SetDisplay("Stop-Loss Bars", "Number of candles for stop-loss calculation", "Risk")
self._take_profit_bars = self.Param("TakeProfitBars", 20).SetDisplay("Take-Profit Bars", "Window used when scanning for take-profit", "Risk")
self._offset_points = self.Param("OffsetPoints", 10).SetDisplay("Offset Points", "Additional protective offset in points", "Risk")
self._profit_threshold_points = self.Param("ProfitThresholdPoints", 500.0).SetDisplay("Profit Threshold Points", "Minimal profit in points before partial exits", "Risk")
self._fast_ema_period = self.Param("FastEmaPeriod", 12).SetDisplay("Fast EMA", "Fast EMA period for MACD", "Indicators")
self._slow_ema_period = self.Param("SlowEmaPeriod", 26).SetDisplay("Slow EMA", "Slow EMA period for MACD", "Indicators")
self._max_threshold = self.Param("MaxThreshold", 50.0).SetDisplay("Upper Threshold", "Maximum MACD threshold for longs", "Signals")
self._min_threshold = self.Param("MinThreshold", -50.0).SetDisplay("Lower Threshold", "Minimum MACD threshold for shorts", "Signals")
self._ema1_period = self.Param("Ema1Period", 7).SetDisplay("EMA 1", "First EMA period for management", "Management")
self._ema2_period = self.Param("Ema2Period", 21).SetDisplay("EMA 2", "Second EMA period for management", "Management")
self._sma_period = self.Param("SmaPeriod", 98).SetDisplay("SMA", "SMA period for management", "Management")
self._ema3_period = self.Param("Ema3Period", 365).SetDisplay("EMA 3", "Slow EMA period for management", "Management")
self._trade_volume = self.Param("TradeVolume", 0.1).SetDisplay("Trade Volume", "Market order volume", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30))).SetDisplay("Candle Type", "Candle type used for indicators", "General")
self._max_history_param = self.Param("MaxHistory", 1024).SetDisplay("History Limit", "Maximum candles stored", "General")
self._history = []
self._macd_prev1 = None
self._macd_prev2 = None
self._macd_prev3 = None
self._ema1_prev = None
self._ema2_prev = None
self._sma_prev = None
self._ema3_prev = None
self._ema1_last = None
self._ema2_last = None
self._sma_last = None
self._ema3_last = None
self._max_threshold_reached = False
self._min_threshold_reached = False
self._sell_pattern_ready = False
self._buy_pattern_ready = False
self._pattern_min_value = 0.0
self._pattern_max_value = 0.0
self._point_size = 0.0
self._entry_direction = 0
self._entry_price = 0.0
self._open_volume = 0.0
self._stop_loss_price = None
self._take_profit_price = None
self._long_partial_stage = 0
self._short_partial_stage = 0
@property
def StopLossBars(self): return self._stop_loss_bars.Value
@property
def TakeProfitBars(self): return self._take_profit_bars.Value
@property
def OffsetPoints(self): return self._offset_points.Value
@property
def ProfitThresholdPoints(self): return self._profit_threshold_points.Value
@property
def FastEmaPeriod(self): return self._fast_ema_period.Value
@property
def SlowEmaPeriod(self): return self._slow_ema_period.Value
@property
def MaxThreshold(self): return self._max_threshold.Value
@property
def MinThreshold(self): return self._min_threshold.Value
@property
def Ema1Period(self): return self._ema1_period.Value
@property
def Ema2Period(self): return self._ema2_period.Value
@property
def SmaPeriod(self): return self._sma_period.Value
@property
def Ema3Period(self): return self._ema3_period.Value
@property
def TradeVolume(self): return self._trade_volume.Value
@property
def CandleType(self): return self._candle_type.Value
@property
def MaxHistoryParam(self): return self._max_history_param.Value
def OnStarted2(self, time):
super(macd_pattern_trader_v02_strategy, self).OnStarted2(time)
ps = self.Security.PriceStep if self.Security is not None else None
self._point_size = float(ps) if ps is not None and float(ps) > 0 else 0.0001
macd = MovingAverageConvergenceDivergence()
macd.ShortMa.Length = self.FastEmaPeriod
macd.LongMa.Length = self.SlowEmaPeriod
ema1 = ExponentialMovingAverage()
ema1.Length = self.Ema1Period
ema2 = ExponentialMovingAverage()
ema2.Length = self.Ema2Period
sma = SimpleMovingAverage()
sma.Length = self.SmaPeriod
ema3 = ExponentialMovingAverage()
ema3.Length = self.Ema3Period
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(macd, ema1, ema2, sma, ema3, self.ProcessCandle).Start()
def ProcessCandle(self, candle, macd_line, ema1_value, ema2_value, sma_value, ema3_value):
if candle.State != CandleStates.Finished:
return
mv = float(macd_line)
self._ema1_prev = self._ema1_last
self._ema2_prev = self._ema2_last
self._sma_prev = self._sma_last
self._ema3_prev = self._ema3_last
self._ema1_last = float(ema1_value)
self._ema2_last = float(ema2_value)
self._sma_last = float(sma_value)
self._ema3_last = float(ema3_value)
if self._macd_prev1 is None or self._macd_prev2 is None or self._macd_prev3 is None:
self._macd_prev3 = self._macd_prev2
self._macd_prev2 = self._macd_prev1
self._macd_prev1 = mv
self._add_candle(candle)
return
ml, ml2, ml3 = self._macd_prev1, self._macd_prev2, self._macd_prev3
self._add_candle(candle)
self._execute_pattern_logic(candle, mv, ml, ml2, ml3)
self._macd_prev3 = self._macd_prev2
self._macd_prev2 = self._macd_prev1
self._macd_prev1 = mv
def _execute_pattern_logic(self, candle, mc, mp1, mp2, mp3):
if self._point_size <= 0:
return
min_thr = float(self.MinThreshold)
max_thr = float(self.MaxThreshold)
if mc > 0:
self._max_threshold_reached = True
self._sell_pattern_ready = False
if mc > mp1 and mp1 < mp3 and self._max_threshold_reached and mc > min_thr and mc < 0 and not self._sell_pattern_ready:
self._sell_pattern_ready = True
self._pattern_min_value = abs(mp1 * 10000.0)
cm = abs(mc * 10000.0)
if self._sell_pattern_ready and mc < mp1 and mp1 > mp3 and mc < 0 and self._pattern_min_value <= cm:
self._max_threshold_reached = False
if self._sell_pattern_ready and mc < mp1 and mp1 > mp3 and mc < 0:
self._try_open_short(candle)
self._sell_pattern_ready = False
self._max_threshold_reached = False
if mc < 0:
self._min_threshold_reached = True
self._buy_pattern_ready = False
if mc < max_thr and mc < mp1 and mp1 > mp3 and self._min_threshold_reached and mc > 0 and not self._buy_pattern_ready:
self._buy_pattern_ready = True
self._pattern_max_value = abs(mp1 * 10000.0)
if self._buy_pattern_ready and mc > mp1 and mp1 < mp3 and mc > 0 and self._pattern_max_value <= cm:
self._min_threshold_reached = False
if self._buy_pattern_ready and mc > mp1 and mp1 < mp3 and mc > 0:
self._try_open_long(candle)
self._buy_pattern_ready = False
self._min_threshold_reached = False
self._manage_position(candle)
def _try_open_short(self, candle):
if self.Position > 0:
self.SellMarket(abs(self.Position))
self._reset_position_state()
if self.Position < 0:
return
volume = float(self.TradeVolume)
if volume <= 0:
return
self.SellMarket(volume)
self._register_entry(-1, float(candle.ClosePrice), volume)
def _try_open_long(self, candle):
if self.Position < 0:
self.BuyMarket(abs(self.Position))
self._reset_position_state()
if self.Position > 0:
return
volume = float(self.TradeVolume)
if volume <= 0:
return
self.BuyMarket(volume)
self._register_entry(1, float(candle.ClosePrice), volume)
def _register_entry(self, direction, entry_price, volume):
self._entry_direction = direction
self._entry_price = entry_price
self._open_volume = volume
self._stop_loss_price = self._calculate_long_stop() if direction > 0 else self._calculate_short_stop()
self._take_profit_price = self._calculate_long_target() if direction > 0 else self._calculate_short_target()
self._long_partial_stage = 0
self._short_partial_stage = 0
def _manage_position(self, candle):
if self._entry_direction == 0 or self._open_volume <= 0:
return
if self._check_risk_management(candle):
return
prev_candle = self._get_candle(1)
if prev_candle is None or self._ema2_prev is None or self._ema3_prev is None or self._sma_prev is None:
return
pp = self._calculate_open_profit_points(float(candle.ClosePrice))
pt = float(self.ProfitThresholdPoints)
if self._entry_direction > 0:
if pp > pt and float(prev_candle.ClosePrice) > self._ema2_prev and self._long_partial_stage == 0:
v = self._open_volume / 3.0
if v > 0:
self.SellMarket(v)
self._register_close(v)
self._long_partial_stage = 1
elif pp > pt and float(prev_candle.HighPrice) > (self._sma_prev + self._ema3_prev) / 2.0 and self._long_partial_stage == 1:
v = self._open_volume / 2.0
if v > 0:
self.SellMarket(v)
self._register_close(v)
self._long_partial_stage = 2
elif self._entry_direction < 0:
if pp > pt and float(prev_candle.ClosePrice) < self._ema2_prev and self._short_partial_stage == 0:
v = self._open_volume / 3.0
if v > 0:
self.BuyMarket(v)
self._register_close(v)
self._short_partial_stage = 1
elif pp > pt and float(prev_candle.LowPrice) < (self._sma_prev + self._ema3_prev) / 2.0 and self._short_partial_stage == 1:
v = self._open_volume / 2.0
if v > 0:
self.BuyMarket(v)
self._register_close(v)
self._short_partial_stage = 2
def _check_risk_management(self, candle):
if self._entry_direction == 0 or self._open_volume <= 0:
return False
if self._entry_direction > 0:
if self._stop_loss_price is not None and float(candle.LowPrice) <= self._stop_loss_price:
self.SellMarket(self._open_volume)
self._reset_position_state()
return True
if self._take_profit_price is not None and float(candle.HighPrice) >= self._take_profit_price:
self.SellMarket(self._open_volume)
self._reset_position_state()
return True
else:
if self._stop_loss_price is not None and float(candle.HighPrice) >= self._stop_loss_price:
self.BuyMarket(self._open_volume)
self._reset_position_state()
return True
if self._take_profit_price is not None and float(candle.LowPrice) <= self._take_profit_price:
self.BuyMarket(self._open_volume)
self._reset_position_state()
return True
return False
def _calculate_open_profit_points(self, current_price):
if self._point_size <= 0:
return 0.0
diff = current_price - self._entry_price if self._entry_direction > 0 else self._entry_price - current_price
return abs(diff / self._point_size)
def _register_close(self, volume):
self._open_volume -= volume
if self._open_volume <= 0 or abs(self.Position) < 1e-6:
self._reset_position_state()
def _reset_position_state(self):
self._entry_direction = 0
self._entry_price = 0.0
self._open_volume = 0.0
self._stop_loss_price = None
self._take_profit_price = None
self._long_partial_stage = 0
self._short_partial_stage = 0
def _calculate_short_stop(self):
candles = self._get_candles_range(int(self.StopLossBars), 1)
if len(candles) == 0:
return None
return max(float(c.HighPrice) for c in candles) + int(self.OffsetPoints) * self._point_size
def _calculate_long_stop(self):
candles = self._get_candles_range(int(self.StopLossBars), 1)
if len(candles) == 0:
return None
return min(float(c.LowPrice) for c in candles) - int(self.OffsetPoints) * self._point_size
def _calculate_short_target(self):
return self._scan_sequential_extremum(int(self.TakeProfitBars), True)
def _calculate_long_target(self):
return self._scan_sequential_extremum(int(self.TakeProfitBars), False)
def _scan_sequential_extremum(self, window, is_short):
if window <= 0:
return None
best = None
shift = 0
while True:
candles = self._get_candles_range(window, shift)
if len(candles) == 0:
break
if is_short:
candidate = min(float(c.LowPrice) for c in candles)
if best is None or candidate < best:
best = candidate
shift += window
continue
else:
candidate = max(float(c.HighPrice) for c in candles)
if best is None or candidate > best:
best = candidate
shift += window
continue
break
return best
def _get_candles_range(self, length, shift):
result = []
if length <= 0:
return result
i = len(self._history) - 1 - shift
while i >= 0 and len(result) < length:
result.append(self._history[i])
i -= 1
return result
def _get_candle(self, shift):
idx = len(self._history) - 1 - shift
if idx < 0 or idx >= len(self._history):
return None
return self._history[idx]
def _add_candle(self, candle):
self._history.append(candle)
mh = int(self.MaxHistoryParam)
if len(self._history) > mh:
self._history.pop(0)
def OnReseted(self):
super(macd_pattern_trader_v02_strategy, self).OnReseted()
self._history = []
self._macd_prev1 = None
self._macd_prev2 = None
self._macd_prev3 = None
self._ema1_prev = None
self._ema2_prev = None
self._sma_prev = None
self._ema3_prev = None
self._ema1_last = None
self._ema2_last = None
self._sma_last = None
self._ema3_last = None
self._max_threshold_reached = False
self._min_threshold_reached = False
self._sell_pattern_ready = False
self._buy_pattern_ready = False
self._pattern_min_value = 0.0
self._pattern_max_value = 0.0
self._point_size = 0.0
self._reset_position_state()
def CreateClone(self):
return macd_pattern_trader_v02_strategy()