Strategie Macd Pattern Trader v02 (StockSharp Port)
Diese Strategie ist eine StockSharp-High-Level-API-Konvertierung des MetaTrader-Experten MacdPatternTraderv02.mq4 (Verzeichnis MQL/8194). Es reproduziert die ursprüngliche MACD-Mustererkennung und die aktiven Positionsverwaltungsregeln und stellt gleichzeitig praktische Parameter für die weitere Optimierung bereit.
Kernidee
- Berechnen Sie die MACD-Hauptlinie mithilfe der schnellen und langsamen EMA-Perioden (
FastEmaPeriod,SlowEmaPeriod) mit einer Signallänge von einer Kerze (entsprechend der MQL-Version). - Überwachen Sie nur abgeschlossene Kerzen. Wenn der Wert MACD eine bestimmte Drei-Takt-Sequenz um die Nulllinie zeichnet, aktivieren Sie entweder das kurze oder das lange Muster:
- Kurzes Muster: erfordert eine positive MACD-Phase, gefolgt von einem negativen Rückzug über
MinThresholdund dann einer Abwärtsbewegung. - Langes Muster: erfordert eine negative MACD-Phase, gefolgt von einem positiven Rückzug unter
MaxThresholdund dann einer Aufwärtsbewegung.
- Kurzes Muster: erfordert eine positive MACD-Phase, gefolgt von einem negativen Rückzug über
- Führen Sie Marktaufträge mit
TradeVolumeaus, sobald das Muster bestätigt wird. - Schützen Sie jede Position mit einem Stop-Loss, der über dem letzten Swing-Extrem liegt (über
StopLossBarsKerzen) plus einem zusätzlichen Offset in Punkten (OffsetPoints). - Definieren Sie das Take-Profit-Niveau, indem Sie aufeinanderfolgende
TakeProfitBars-Segmente scannen und die extremsten erreichten Höchst-/Tiefstwerte auswählen, während die Sequenz weiterhin neue Extremwerte druckt. - Verwalten Sie offene Positionen mit dem aktiven Positionsmanager des ursprünglichen Experten: Nachdem ein Mindestgewinn von fünf Punkten erreicht wurde, schließt die Strategie ein Drittel des Volumens, wenn die vorherige Kerze den Trend bestätigt (Filter
Ema2Period), und eine weitere Hälfte, wenn der Preis mit der Mittellinie vonSmaPeriodundEma3Periodinteragiert.
Parameter
| Parameter | Beschreibung |
|---|---|
StopLossBars |
Anzahl der abgeschlossenen Kerzen, die bei der Berechnung des Stop-Loss-Swing-Extrems überprüft werden. |
TakeProfitBars |
Fenstergröße (in Kerzen) für die sequentielle Suche nach Extremwerten, die das Take-Profit-Ziel bildet. |
OffsetPoints |
Zusätzlicher Offset, ausgedrückt in Instrumentenpunkten, der dem Stop-Loss hinzugefügt wird. |
FastEmaPeriod |
Schnelle EMA-Länge für die MACD-Hauptzeile. |
SlowEmaPeriod |
Langsame EMA-Länge für die MACD-Hauptzeile. |
MaxThreshold |
Positiver MACD-Schwellenwert, der die Kurzmustervorbereitung beendet. |
MinThreshold |
Negativer MACD-Schwellenwert, der die Vorbereitung des langen Musters beendet. |
Ema1Period |
Erster EMA-Zeitraum, der vom ursprünglichen Geldverwaltungsblock verwendet wird (der Vollständigkeit halber beibehalten). |
Ema2Period |
Zweiter Zeitraum EMA, der zur Validierung des Teilgewinns für Long-/Short-Positionen verwendet wird. |
SmaPeriod |
SMA Zeitraum, der im zweiten Teilabschlusstrigger verwendet wird. |
Ema3Period |
Langsamer EMA-Zeitraum gepaart mit SMA, um Mean-Reversion-Exits zu erkennen. |
TradeVolume |
Market-Order-Volumen (Lots). |
CandleType |
Kerzendatentyp, der zur Versorgung aller Indikatoren verwendet wird. |
Handelslogik
- Kurzer Eintrag: wird ausgelöst, wenn die MACD-Sequenz
(prev3, prev2, prev1, current)mit den ursprünglichen Bedingungen übereinstimmt (macdPrev1 < macdPrev3,macdPrev1 > macdPrev2,current < prev1,current < 0und Größenprüfung). Bestehende Long-Positionen werden reduziert, bevor eine neue Short-Position eröffnet wird. - Langer Eintrag: Symmetrische Regeln, bei denen
current > 0, die MACD-Werte das Spiegelbildmuster bilden und die Größenprüfung erfüllt ist. Vorhandene Short-Positionen werden reduziert, bevor eine neue Long-Position eröffnet wird. - Stopps und Ziele: werden unmittelbar nach jedem Einstieg berechnet und nur aktualisiert, wenn ein neuer Trade ausgeführt wird.
- Teilweise Schließungen: Sobald der Gewinn fünf Punkte erreicht (im Verhältnis zur Punktgröße des Instruments), schließt die Strategie ein Drittel des verbleibenden Volumens, wenn die vorherige Kerze über
EMA2schließt. Die nächste Stufe schließt die Hälfte des verbleibenden Volumens, wenn die vorherige Kerze den Durchschnitt vonSMAundEMA3durchbricht. - Vollständiger Ausstieg: Jede Preisberührung des Stop-Loss- oder Take-Profit-Niveaus schließt die gesamte Position. Nach jedem erzwungenen Verlassen wird der interne Zustand automatisch zurückgesetzt.
Notizen
- Die Punktgröße wird aus
Security.PriceStepoder, falls nicht verfügbar, aus den Sicherheitsdezimalstellen abgeleitet. Als sicherer Fallback wird der Standardwert0.0001verwendet. - Der Kerzenverlauf wird gespeichert (bis zu 1024 Einträge), um die MQL-Hilfsfunktionen
iHighest,iLowestund den sequentiellen Extremum-Scan vonTakeProfit()zu replizieren. - Alle Kommentare innerhalb der Strategie bleiben in englischer Sprache, wie in den Repository-Richtlinien gefordert.
- Python-Ports werden für diese Aufgabe absichtlich weggelassen.
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()