DealersTradeZeroLag MACD-Strategie
Überblick
Diese Strategie portiert den MetaTrader Expert Advisor "Dealers Trade v 7.91 ZeroLag MACD" auf die StockSharp High-Level-API. Sie verfolgt die Steigung eines Zero-Lag-MACDs, um zu entscheiden, ob der Markt in einer Akkumulationsphase für Longs oder Shorts ist, und baut ein Positions-Grid mit adaptivem Abstand und Risikomanagement auf. Der Standard-Zeitrahmen sind Vier-Stunden-Kerzen, wie vom ursprünglichen Autor empfohlen, aber jeder von StockSharp unterstützte Kerzentyp kann ausgewählt werden.
Handelslogik
- Signalerkennung. Zwei Zero-Lag-exponentielle gleitende Durchschnitte (schnell und langsam) erzeugen eine MACD-Linie. Wenn der MACD im Vergleich zum vorherigen Balken steigt, behandelt die Strategie den Markt als bullisch; wenn er fällt, behandelt sie ihn als bärisch. Das Signal kann über den Parameter
ReverseConditioninvertiert werden. - Positions-Grid. Der Algorithmus skaliert in die erkannte Richtung. Abstände zwischen Einstiegen werden in Pips gemessen und nach jedem Füllen mit
IntervalCoefficientmultipliziert. Die Lot-Größe wird bei jedem zusätzlichen Einstieg mitLotMultipliermultipliziert, was das Martingal-Schema der MQL-Version nachahmt. - Volumenkontrolle. Wenn
BaseVolumegrößer als null ist, wird es als anfängliche Ordermenge verwendet. Andernfalls leitet der Engine die Größe ausRiskPercent, der Stop-Distanz und den Instrument-Schritt-Parametern ab. Jedes berechnete Volumen wird gegen die Instrument-Grenzen geprüft und durchMaxVolumegedeckelt. - Ordermanamgent. Jeder Einstieg kann mit einem Stop-Loss, Take-Profit und Trailing-Stop (alles in Pips) ausgestattet werden. Die Take-Profit-Distanz wird für aufeinanderfolgende Einstiege mit
TakeProfitCoefficientmultipliziert, um Ziele zu erweitern. - Kontoschutz. Wenn die Gesamtanzahl offener Positionen
PositionsForProtectionüberschreitet und ihr kombinierter GewinnSecureProfiterreicht, schließt die Strategie den Trade mit dem größten Gewinn, um Gewinne zu sichern. Wenn die Gesamtanzahl der PositionenMaxPositionsübersteigt, schließt sie den schlechtesten Trade, bevor neue Einstiege akzeptiert werden.
Positionshandling
- Stops, Trailing-Logik und Ziele werden bei abgeschlossenen Kerzen mit Schlusskurs, Hoch- und Tiefpreisen bewertet.
- Alle offenen Positionen werden mit eigenem Volumen, Einstiegspreis und Trailing-Zustand verfolgt. Der letzte Füllpreis wird wiederverwendet, um den Mindestabstand für zukünftige Einstiege durchzusetzen.
- Wenn der Kontostand unter
MinimumBalancefällt, hält die Strategie sich selbst an, um Übertrading auf kleinen Konten zu vermeiden.
Parameter
| Parameter | Beschreibung |
|---|---|
BaseVolume |
Anfängliche Ordergröße. Auf null setzen, um risikobasiertes Sizing über RiskPercent zu aktivieren. |
RiskPercent |
Prozentsatz des Portfolio-Eigenkapitals, das riskiert wird, wenn die Positionsgröße aus der Stop-Distanz abgeleitet wird. |
MaxPositions |
Maximale Anzahl gleichzeitig offener Einstiege. |
IntervalPips |
Anfänglicher Abstand zwischen Grid-Einstiegen in Pips. |
IntervalCoefficient |
Multiplikator, der nach jedem zusätzlichen Einstieg auf den Abstand angewendet wird. |
StopLossPips |
Stop-Loss-Distanz in Pips. Auf null setzen, um zu deaktivieren. |
TakeProfitPips |
Basis-Take-Profit-Distanz in Pips. Pro Einstieg mit TakeProfitCoefficient multipliziert. |
TrailingStopPips / TrailingStepPips |
Trailing-Stop-Distanz und erforderlicher Vorschub, bevor der Trail angepasst wird. |
TakeProfitCoefficient |
Multiplikator für das Erweitern von Take-Profit-Distanzen bei späteren Einstiegen. |
SecureProfit |
Gewinnschwelle, die den Kontoschutz auslöst, sobald genügend Positionen offen sind. |
AccountProtection |
Aktiviert automatisches Gewinnsichern durch Schließen des besten Trades. |
PositionsForProtection |
Mindestanzahl offener Positionen, die erforderlich ist, bevor der Kontoschutz aktiv wird. |
ReverseCondition |
Invertiert die MACD-Steigungs-Interpretation. |
FastLength, SlowLength, SignalLength |
Perioden der Zero-Lag-exponentiellen gleitenden Durchschnitte. |
MaxVolume |
Obergrenze für das Volumen eines einzelnen Einstiegs. |
LotMultiplier |
Multiplikativer Faktor für das Skalieren der Positionsgröße mit jedem Grid-Einstieg. |
MinimumBalance |
Mindest-Kontostand, der für die Fortsetzung des Handels erforderlich ist. |
CandleType |
Kerzendatentyp für Berechnungen. |
Verwendungshinweise
- Verbinden Sie die Strategie mit einem Portfolio und einem Instrument, bevor Sie sie starten.
- Überprüfen Sie den Instrument-Schritt und die Preiseinstellungen, um sicherzustellen, dass Pip-Konvertierungen korrekt sind.
- Die Standard-Parameter replizieren das Verhalten des ursprünglichen Expert Advisors, können aber durch StockSharp-Optimierer optimiert werden.
- Eine Python-Übersetzung ist für diese Strategie nicht enthalten.
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>
/// Grid strategy based on zero lag MACD slope with adaptive spacing and money management.
/// </summary>
public class DealersTradeZeroLagMacdStrategy : Strategy
{
private sealed class PositionEntry
{
public PositionEntry(Sides side, decimal volume)
{
Side = side;
Volume = volume;
}
public Sides Side { get; }
public decimal Volume { get; set; }
public decimal EntryPrice { get; set; }
public decimal? StopLoss { get; set; }
public decimal? TakeProfit { get; set; }
public decimal TrailingDistance { get; set; }
public decimal TrailingStep { get; set; }
public decimal? TrailingStop { get; set; }
public decimal PendingCloseVolume { get; set; }
}
private sealed class PendingEntry
{
public PendingEntry(Sides side, decimal volume)
{
Side = side;
Volume = volume;
}
public Sides Side { get; }
public decimal Volume { get; }
public decimal StopLossDistance { get; set; }
public decimal TakeProfitDistance { get; set; }
public decimal TrailingDistance { get; set; }
public decimal TrailingStep { get; set; }
public decimal FilledVolume { get; set; }
public PositionEntry Entry { get; set; }
}
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<int> _intervalPips;
private readonly StrategyParam<decimal> _intervalCoefficient;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<decimal> _takeProfitCoefficient;
private readonly StrategyParam<decimal> _secureProfit;
private readonly StrategyParam<bool> _accountProtection;
private readonly StrategyParam<int> _positionsForProtection;
private readonly StrategyParam<bool> _reverseCondition;
private readonly StrategyParam<int> _fastLength;
private readonly StrategyParam<int> _slowLength;
private readonly StrategyParam<int> _signalLength;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _lotMultiplier;
private readonly StrategyParam<decimal> _minimumBalance;
private readonly StrategyParam<DataType> _candleType;
private readonly List<PositionEntry> _longEntries = new();
private readonly List<PositionEntry> _shortEntries = new();
private ZeroLagExponentialMovingAverage _fastZlema = null!;
private ZeroLagExponentialMovingAverage _slowZlema = null!;
private ExponentialMovingAverage _signalEma = null!;
private PendingEntry _pendingBuyEntry;
private PendingEntry _pendingSellEntry;
private decimal _pipSize;
private decimal _lastLongEntryPrice;
private decimal _lastShortEntryPrice;
private decimal _previousMacd;
private bool _hasPreviousMacd;
/// <summary>
/// Base order volume. Set to zero to enable risk-based sizing.
/// </summary>
public decimal BaseVolume
{
get => _baseVolume.Value;
set => _baseVolume.Value = value;
}
/// <summary>
/// Risk percent used when <see cref="BaseVolume"/> is zero.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Maximum number of simultaneously open entries.
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
/// <summary>
/// Initial spacing between entries in pips.
/// </summary>
public int IntervalPips
{
get => _intervalPips.Value;
set => _intervalPips.Value = value;
}
/// <summary>
/// Multiplier applied to the spacing after each additional entry.
/// </summary>
public decimal IntervalCoefficient
{
get => _intervalCoefficient.Value;
set => _intervalCoefficient.Value = value;
}
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Trailing stop distance in pips.
/// </summary>
public int TrailingStopPips
{
get => _trailingStopPips.Value;
set => _trailingStopPips.Value = value;
}
/// <summary>
/// Minimum price advance before the trailing stop starts to follow the price.
/// </summary>
public int TrailingStepPips
{
get => _trailingStepPips.Value;
set => _trailingStepPips.Value = value;
}
/// <summary>
/// Multiplier applied to the take profit distance for each additional entry.
/// </summary>
public decimal TakeProfitCoefficient
{
get => _takeProfitCoefficient.Value;
set => _takeProfitCoefficient.Value = value;
}
/// <summary>
/// Target profit used when account protection is enabled.
/// </summary>
public decimal SecureProfit
{
get => _secureProfit.Value;
set => _secureProfit.Value = value;
}
/// <summary>
/// Enables closing the most profitable position once cumulative profit reaches <see cref="SecureProfit"/>.
/// </summary>
public bool AccountProtection
{
get => _accountProtection.Value;
set => _accountProtection.Value = value;
}
/// <summary>
/// Minimum number of entries required before account protection can trigger.
/// </summary>
public int PositionsForProtection
{
get => _positionsForProtection.Value;
set => _positionsForProtection.Value = value;
}
/// <summary>
/// Reverses the MACD slope interpretation when set to true.
/// </summary>
public bool ReverseCondition
{
get => _reverseCondition.Value;
set => _reverseCondition.Value = value;
}
/// <summary>
/// Fast length of the zero lag EMA.
/// </summary>
public int FastLength
{
get => _fastLength.Value;
set => _fastLength.Value = value;
}
/// <summary>
/// Slow length of the zero lag EMA.
/// </summary>
public int SlowLength
{
get => _slowLength.Value;
set => _slowLength.Value = value;
}
/// <summary>
/// Signal length used for smoothing MACD line.
/// </summary>
public int SignalLength
{
get => _signalLength.Value;
set => _signalLength.Value = value;
}
/// <summary>
/// Maximum allowed volume for a single entry.
/// </summary>
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
/// <summary>
/// Multiplier applied to the base volume when stacking positions.
/// </summary>
public decimal LotMultiplier
{
get => _lotMultiplier.Value;
set => _lotMultiplier.Value = value;
}
/// <summary>
/// Minimum portfolio balance required to keep trading.
/// </summary>
public decimal MinimumBalance
{
get => _minimumBalance.Value;
set => _minimumBalance.Value = value;
}
/// <summary>
/// Candle type used for calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="DealersTradeZeroLagMacdStrategy"/> class.
/// </summary>
public DealersTradeZeroLagMacdStrategy()
{
_baseVolume = Param(nameof(BaseVolume), 0.1m)
.SetDisplay("Base Volume", "Initial order volume", "Trading")
;
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetDisplay("Risk Percent", "Risk per trade when base volume is zero", "Trading")
;
_maxPositions = Param(nameof(MaxPositions), 2)
.SetDisplay("Max Positions", "Maximum simultaneous entries", "Risk")
.SetGreaterThanZero()
;
_intervalPips = Param(nameof(IntervalPips), 50)
.SetDisplay("Interval (pips)", "Base spacing between entries", "Grid")
.SetNotNegative()
;
_intervalCoefficient = Param(nameof(IntervalCoefficient), 1.2m)
.SetDisplay("Interval Coefficient", "Spacing multiplier for additional entries", "Grid")
.SetGreaterThanZero()
;
_stopLossPips = Param(nameof(StopLossPips), 0)
.SetDisplay("Stop Loss (pips)", "Distance to protective stop", "Risk")
.SetNotNegative();
_takeProfitPips = Param(nameof(TakeProfitPips), 50)
.SetDisplay("Take Profit (pips)", "Base take profit distance", "Risk")
.SetNotNegative()
;
_trailingStopPips = Param(nameof(TrailingStopPips), 0)
.SetDisplay("Trailing Stop (pips)", "Trailing distance", "Risk")
.SetNotNegative();
_trailingStepPips = Param(nameof(TrailingStepPips), 5)
.SetDisplay("Trailing Step (pips)", "Extra move required to tighten trail", "Risk")
.SetNotNegative();
_takeProfitCoefficient = Param(nameof(TakeProfitCoefficient), 1.2m)
.SetDisplay("TP Coefficient", "Take profit multiplier per entry", "Risk")
.SetGreaterThanZero()
;
_secureProfit = Param(nameof(SecureProfit), 300m)
.SetDisplay("Secure Profit", "Cumulative profit to trigger protection", "Risk")
.SetNotNegative();
_accountProtection = Param(nameof(AccountProtection), true)
.SetDisplay("Account Protection", "Enable profit locking", "Risk");
_positionsForProtection = Param(nameof(PositionsForProtection), 3)
.SetDisplay("Positions For Protection", "Entries required for protection", "Risk")
.SetNotNegative();
_reverseCondition = Param(nameof(ReverseCondition), false)
.SetDisplay("Reverse Condition", "Invert MACD slope logic", "General");
_fastLength = Param(nameof(FastLength), 14)
.SetDisplay("Fast Length", "Fast ZLEMA length", "Indicators")
.SetGreaterThanZero()
;
_slowLength = Param(nameof(SlowLength), 26)
.SetDisplay("Slow Length", "Slow ZLEMA length", "Indicators")
.SetGreaterThanZero()
;
_signalLength = Param(nameof(SignalLength), 9)
.SetDisplay("Signal Length", "Signal smoothing length", "Indicators")
.SetGreaterThanZero()
;
_maxVolume = Param(nameof(MaxVolume), 5m)
.SetDisplay("Max Volume", "Maximum volume per entry", "Trading")
.SetGreaterThanZero();
_lotMultiplier = Param(nameof(LotMultiplier), 1.6m)
.SetDisplay("Lot Multiplier", "Multiplier applied to each new entry", "Trading")
.SetGreaterThanZero()
;
_minimumBalance = Param(nameof(MinimumBalance), 0m)
.SetDisplay("Minimum Balance", "Stop trading below this balance", "Risk")
.SetNotNegative();
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for calculations", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_longEntries.Clear();
_shortEntries.Clear();
_pendingBuyEntry = null;
_pendingSellEntry = null;
_lastLongEntryPrice = 0m;
_lastShortEntryPrice = 0m;
_previousMacd = 0m;
_hasPreviousMacd = false;
_pipSize = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fastZlema = new ZeroLagExponentialMovingAverage { Length = FastLength };
_slowZlema = new ZeroLagExponentialMovingAverage { Length = SlowLength };
_signalEma = new ExponentialMovingAverage { Length = SignalLength };
var decimals = Security?.Decimals ?? 0;
var step = Security?.PriceStep ?? 0.0001m;
var factor = decimals == 3 || decimals == 5 ? 10m : 1m;
_pipSize = step * factor;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_fastZlema, _slowZlema, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _fastZlema);
DrawIndicator(area, _slowZlema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow)
{
if (candle.State != CandleStates.Finished)
return;
var balance = Portfolio?.CurrentValue;
if (balance.HasValue && balance.Value < MinimumBalance)
{
Stop();
return;
}
var macd = fast - slow;
_signalEma.Process(new DecimalIndicatorValue(_signalEma, macd, candle.CloseTime) { IsFinal = true });
if (!_fastZlema.IsFormed || !_slowZlema.IsFormed || !_signalEma.IsFormed)
{
_previousMacd = macd;
_hasPreviousMacd = true;
return;
}
if (!_hasPreviousMacd)
{
_previousMacd = macd;
_hasPreviousMacd = true;
return;
}
var direction = 3;
if (macd > _previousMacd && macd != 0m && _previousMacd != 0m)
direction = 2;
else if (macd < _previousMacd && macd != 0m && _previousMacd != 0m)
direction = 1;
if (ReverseCondition)
{
if (direction == 1)
direction = 2;
else if (direction == 2)
direction = 1;
}
_previousMacd = macd;
var openPositions = _longEntries.Count + _shortEntries.Count;
var continueOpening = openPositions <= MaxPositions;
if (direction != 3 && openPositions > MaxPositions)
{
CloseMinimumProfit(candle.ClosePrice);
return;
}
var closedThisBar = ManagePositions(candle);
if (closedThisBar)
return;
var totalProfit = GetTotalProfit(candle.ClosePrice);
if (AccountProtection && openPositions > PositionsForProtection && totalProfit >= SecureProfit)
{
CloseMaximumProfit(candle.ClosePrice);
return;
}
if (!continueOpening)
return;
if (direction == 2)
TryOpenLong(candle, openPositions);
else if (direction == 1)
TryOpenShort(candle, openPositions);
}
private void TryOpenLong(ICandleMessage candle, int openPositions)
{
var interval = GetIntervalDistance(openPositions);
var canOpen = _longEntries.Count == 0 || _lastLongEntryPrice - candle.ClosePrice >= interval;
if (!canOpen)
return;
var stopDistance = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;
var takeDistance = TakeProfitPips > 0 ? TakeProfitPips * _pipSize : 0m;
if (takeDistance > 0m)
{
var tpMultiplier = Pow(TakeProfitCoefficient, openPositions + 1);
takeDistance *= tpMultiplier;
}
var trailingDistance = TrailingStopPips > 0 ? TrailingStopPips * _pipSize : 0m;
var trailingStep = TrailingStepPips > 0 ? TrailingStepPips * _pipSize : 0m;
var lotMultiplier = openPositions == 0 ? 1m : Pow(LotMultiplier, openPositions + 1);
var volume = CalculateEntryVolume(stopDistance, lotMultiplier);
if (volume <= 0m)
return;
var pending = new PendingEntry(Sides.Buy, volume)
{
StopLossDistance = stopDistance,
TakeProfitDistance = takeDistance,
TrailingDistance = trailingDistance,
TrailingStep = trailingStep
};
_pendingBuyEntry = pending;
BuyMarket(volume);
}
private void TryOpenShort(ICandleMessage candle, int openPositions)
{
var interval = GetIntervalDistance(openPositions);
var canOpen = _shortEntries.Count == 0 || candle.ClosePrice - _lastShortEntryPrice >= interval;
if (!canOpen)
return;
var stopDistance = StopLossPips > 0 ? StopLossPips * _pipSize : 0m;
var takeDistance = TakeProfitPips > 0 ? TakeProfitPips * _pipSize : 0m;
if (takeDistance > 0m)
{
var tpMultiplier = Pow(TakeProfitCoefficient, openPositions + 1);
takeDistance *= tpMultiplier;
}
var trailingDistance = TrailingStopPips > 0 ? TrailingStopPips * _pipSize : 0m;
var trailingStep = TrailingStepPips > 0 ? TrailingStepPips * _pipSize : 0m;
var lotMultiplier = openPositions == 0 ? 1m : Pow(LotMultiplier, openPositions + 1);
var volume = CalculateEntryVolume(stopDistance, lotMultiplier);
if (volume <= 0m)
return;
var pending = new PendingEntry(Sides.Sell, volume)
{
StopLossDistance = stopDistance,
TakeProfitDistance = takeDistance,
TrailingDistance = trailingDistance,
TrailingStep = trailingStep
};
_pendingSellEntry = pending;
SellMarket(volume);
}
private bool ManagePositions(ICandleMessage candle)
{
var closed = false;
if (ManageEntries(_longEntries, candle, true))
closed = true;
if (ManageEntries(_shortEntries, candle, false))
closed = true;
return closed;
}
private bool ManageEntries(List<PositionEntry> entries, ICandleMessage candle, bool isLong)
{
var closed = false;
foreach (var entry in entries.ToList())
{
if (entry.PendingCloseVolume > 0m)
continue;
if (isLong)
{
if (entry.StopLoss.HasValue && candle.LowPrice <= entry.StopLoss.Value)
{
SendCloseOrder(entry);
closed = true;
continue;
}
if (entry.TakeProfit.HasValue && candle.HighPrice >= entry.TakeProfit.Value)
{
SendCloseOrder(entry);
closed = true;
continue;
}
if (entry.TrailingDistance > 0m)
{
var profit = candle.ClosePrice - entry.EntryPrice;
if (profit > entry.TrailingDistance + entry.TrailingStep)
{
var newStop = candle.ClosePrice - entry.TrailingDistance;
if (!entry.TrailingStop.HasValue || entry.TrailingStop.Value < newStop)
entry.TrailingStop = newStop;
}
if (entry.TrailingStop.HasValue && candle.LowPrice <= entry.TrailingStop.Value)
{
SendCloseOrder(entry);
closed = true;
}
}
}
else
{
if (entry.StopLoss.HasValue && candle.HighPrice >= entry.StopLoss.Value)
{
SendCloseOrder(entry);
closed = true;
continue;
}
if (entry.TakeProfit.HasValue && candle.LowPrice <= entry.TakeProfit.Value)
{
SendCloseOrder(entry);
closed = true;
continue;
}
if (entry.TrailingDistance > 0m)
{
var profit = entry.EntryPrice - candle.ClosePrice;
if (profit > entry.TrailingDistance + entry.TrailingStep)
{
var newStop = candle.ClosePrice + entry.TrailingDistance;
if (!entry.TrailingStop.HasValue || entry.TrailingStop.Value > newStop)
entry.TrailingStop = newStop;
}
if (entry.TrailingStop.HasValue && candle.HighPrice >= entry.TrailingStop.Value)
{
SendCloseOrder(entry);
closed = true;
}
}
}
}
return closed;
}
private void SendCloseOrder(PositionEntry entry)
{
if (entry.PendingCloseVolume > 0m)
return;
entry.PendingCloseVolume = entry.Volume;
if (entry.Side == Sides.Buy)
SellMarket(entry.Volume);
else
BuyMarket(entry.Volume);
}
private void CloseMaximumProfit(decimal price)
{
PositionEntry best = null;
var bestProfit = decimal.MinValue;
foreach (var entry in _longEntries)
{
var profit = GetEntryProfit(entry, price);
if (profit > bestProfit)
{
bestProfit = profit;
best = entry;
}
}
foreach (var entry in _shortEntries)
{
var profit = GetEntryProfit(entry, price);
if (profit > bestProfit)
{
bestProfit = profit;
best = entry;
}
}
if (best != null)
SendCloseOrder(best);
}
private void CloseMinimumProfit(decimal price)
{
PositionEntry worst = null;
var worstProfit = decimal.MaxValue;
foreach (var entry in _longEntries)
{
var profit = GetEntryProfit(entry, price);
if (profit < worstProfit)
{
worstProfit = profit;
worst = entry;
}
}
foreach (var entry in _shortEntries)
{
var profit = GetEntryProfit(entry, price);
if (profit < worstProfit)
{
worstProfit = profit;
worst = entry;
}
}
if (worst != null)
SendCloseOrder(worst);
}
private decimal GetTotalProfit(decimal price)
{
var total = 0m;
foreach (var entry in _longEntries)
total += GetEntryProfit(entry, price);
foreach (var entry in _shortEntries)
total += GetEntryProfit(entry, price);
return total;
}
private decimal GetEntryProfit(PositionEntry entry, decimal price)
{
var priceStep = Security?.PriceStep ?? 1m;
var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? priceStep;
if (priceStep == 0m)
priceStep = 1m;
var diff = entry.Side == Sides.Buy ? price - entry.EntryPrice : entry.EntryPrice - price;
var steps = diff / priceStep;
return steps * stepPrice * entry.Volume;
}
private decimal CalculateEntryVolume(decimal stopDistance, decimal multiplier)
{
var volume = BaseVolume > 0m ? BaseVolume : CalculateRiskVolume(stopDistance);
if (volume <= 0m)
return 0m;
volume *= multiplier;
var step = Security?.VolumeStep ?? 0m;
if (step > 0m)
volume = Math.Floor(volume / step) * step;
var min = Security?.MinVolume ?? 0m;
if (min > 0m && volume < min)
return 0m;
var max = Security?.MaxVolume;
if (max.HasValue && volume > max.Value)
volume = max.Value;
if (volume > MaxVolume)
return 0m;
return volume;
}
private decimal CalculateRiskVolume(decimal stopDistance)
{
if (stopDistance <= 0m)
return 0m;
var portfolioValue = Portfolio?.CurrentValue;
if (!portfolioValue.HasValue || portfolioValue.Value <= 0m)
return 0m;
var priceStep = Security?.PriceStep ?? 1m;
var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? priceStep;
if (priceStep == 0m || stepPrice == 0m)
return 0m;
var steps = stopDistance / priceStep;
if (steps <= 0m)
return 0m;
var lossPerUnit = steps * stepPrice;
if (lossPerUnit <= 0m)
return 0m;
var riskAmount = portfolioValue.Value * (RiskPercent / 100m);
return riskAmount / lossPerUnit;
}
private decimal GetIntervalDistance(int openPositions)
{
var distance = IntervalPips > 0 ? IntervalPips * _pipSize : 0m;
if (distance <= 0m)
return 0m;
if (openPositions > 0)
{
var multiplier = Pow(IntervalCoefficient, openPositions);
distance *= multiplier;
}
return distance;
}
private static decimal Pow(decimal value, int exponent)
{
var result = 1m;
for (var i = 0; i < exponent; i++)
result *= value;
return result;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade.Order == null)
return;
var volume = trade.Trade.Volume;
var price = trade.Trade.Price;
if (trade.Order.Side == Sides.Buy)
{
if (_pendingBuyEntry != null)
{
ProcessPendingEntry(_pendingBuyEntry, volume, price, _longEntries, true);
if (_pendingBuyEntry.FilledVolume >= _pendingBuyEntry.Volume - 0.0000001m)
{
_lastLongEntryPrice = _pendingBuyEntry.Entry?.EntryPrice ?? _lastLongEntryPrice;
_pendingBuyEntry = null;
}
}
else
{
ProcessClose(_shortEntries, volume, false);
}
}
else if (trade.Order.Side == Sides.Sell)
{
if (_pendingSellEntry != null)
{
ProcessPendingEntry(_pendingSellEntry, volume, price, _shortEntries, false);
if (_pendingSellEntry.FilledVolume >= _pendingSellEntry.Volume - 0.0000001m)
{
_lastShortEntryPrice = _pendingSellEntry.Entry?.EntryPrice ?? _lastShortEntryPrice;
_pendingSellEntry = null;
}
}
else
{
ProcessClose(_longEntries, volume, true);
}
}
}
private void ProcessPendingEntry(PendingEntry pending, decimal volume, decimal price, List<PositionEntry> entries, bool isLong)
{
var entry = pending.Entry;
if (entry == null)
{
entry = new PositionEntry(pending.Side, volume)
{
EntryPrice = price,
TrailingDistance = pending.TrailingDistance,
TrailingStep = pending.TrailingStep
};
entries.Add(entry);
pending.Entry = entry;
}
else
{
var totalVolume = entry.Volume + volume;
entry.EntryPrice = (entry.EntryPrice * entry.Volume + price * volume) / totalVolume;
entry.Volume = totalVolume;
}
pending.FilledVolume += volume;
if (isLong)
{
entry.StopLoss = pending.StopLossDistance > 0m ? entry.EntryPrice - pending.StopLossDistance : null;
entry.TakeProfit = pending.TakeProfitDistance > 0m ? entry.EntryPrice + pending.TakeProfitDistance : null;
}
else
{
entry.StopLoss = pending.StopLossDistance > 0m ? entry.EntryPrice + pending.StopLossDistance : null;
entry.TakeProfit = pending.TakeProfitDistance > 0m ? entry.EntryPrice - pending.TakeProfitDistance : null;
}
entry.TrailingStop = null;
}
private void ProcessClose(List<PositionEntry> entries, decimal volume, bool closingLong)
{
var remaining = volume;
foreach (var entry in entries)
{
if (remaining <= 0m)
break;
if (entry.PendingCloseVolume <= 0m)
continue;
var closeVolume = Math.Min(entry.PendingCloseVolume, remaining);
entry.PendingCloseVolume -= closeVolume;
entry.Volume -= closeVolume;
remaining -= closeVolume;
if (entry.PendingCloseVolume <= 0m)
entry.PendingCloseVolume = 0m;
}
for (var i = entries.Count - 1; i >= 0; i--)
{
var entry = entries[i];
if (entry.Volume <= 0m)
{
entries.RemoveAt(i);
}
}
if (closingLong)
_lastLongEntryPrice = _longEntries.Count > 0 ? _longEntries[^1].EntryPrice : 0m;
else
_lastShortEntryPrice = _shortEntries.Count > 0 ? _shortEntries[^1].EntryPrice : 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, Sides
from StockSharp.Algo.Indicators import ZeroLagExponentialMovingAverage, ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class dealers_trade_zero_lag_macd_strategy(Strategy):
"""
Grid strategy based on zero lag MACD slope with adaptive spacing and money management.
Uses two ZLEMA indicators to compute MACD, smoothed by signal EMA.
Manages a grid of long/short entries with trailing stops, SL/TP, and account protection.
"""
def __init__(self):
super(dealers_trade_zero_lag_macd_strategy, self).__init__()
self._base_volume = self.Param("BaseVolume", 0.1) \
.SetDisplay("Base Volume", "Initial order volume", "Trading")
self._risk_percent = self.Param("RiskPercent", 5.0) \
.SetDisplay("Risk Percent", "Risk per trade when base volume is zero", "Trading")
self._max_positions = self.Param("MaxPositions", 2) \
.SetDisplay("Max Positions", "Maximum simultaneous entries", "Risk")
self._interval_pips = self.Param("IntervalPips", 50) \
.SetDisplay("Interval (pips)", "Base spacing between entries", "Grid")
self._interval_coefficient = self.Param("IntervalCoefficient", 1.2) \
.SetDisplay("Interval Coefficient", "Spacing multiplier for additional entries", "Grid")
self._stop_loss_pips = self.Param("StopLossPips", 0) \
.SetDisplay("Stop Loss (pips)", "Distance to protective stop", "Risk")
self._take_profit_pips = self.Param("TakeProfitPips", 50) \
.SetDisplay("Take Profit (pips)", "Base take profit distance", "Risk")
self._trailing_stop_pips = self.Param("TrailingStopPips", 0) \
.SetDisplay("Trailing Stop (pips)", "Trailing distance", "Risk")
self._trailing_step_pips = self.Param("TrailingStepPips", 5) \
.SetDisplay("Trailing Step (pips)", "Extra move required to tighten trail", "Risk")
self._tp_coefficient = self.Param("TakeProfitCoefficient", 1.2) \
.SetDisplay("TP Coefficient", "Take profit multiplier per entry", "Risk")
self._secure_profit = self.Param("SecureProfit", 300.0) \
.SetDisplay("Secure Profit", "Cumulative profit to trigger protection", "Risk")
self._account_protection = self.Param("AccountProtection", True) \
.SetDisplay("Account Protection", "Enable profit locking", "Risk")
self._positions_for_protection = self.Param("PositionsForProtection", 3) \
.SetDisplay("Positions For Protection", "Entries required for protection", "Risk")
self._reverse_condition = self.Param("ReverseCondition", False) \
.SetDisplay("Reverse Condition", "Invert MACD slope logic", "General")
self._fast_length = self.Param("FastLength", 14) \
.SetDisplay("Fast Length", "Fast ZLEMA length", "Indicators")
self._slow_length = self.Param("SlowLength", 26) \
.SetDisplay("Slow Length", "Slow ZLEMA length", "Indicators")
self._signal_length = self.Param("SignalLength", 9) \
.SetDisplay("Signal Length", "Signal smoothing length", "Indicators")
self._max_volume = self.Param("MaxVolume", 5.0) \
.SetDisplay("Max Volume", "Maximum volume per entry", "Trading")
self._lot_multiplier = self.Param("LotMultiplier", 1.6) \
.SetDisplay("Lot Multiplier", "Multiplier applied to each new entry", "Trading")
self._minimum_balance = self.Param("MinimumBalance", 0.0) \
.SetDisplay("Minimum Balance", "Stop trading below this balance", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Timeframe for calculations", "General")
self._long_entries = []
self._short_entries = []
self._pip_size = 0.0
self._last_long_entry_price = 0.0
self._last_short_entry_price = 0.0
self._previous_macd = 0.0
self._has_previous_macd = False
self._signal_ema = None
@property
def candle_type(self):
return self._candle_type.Value
def OnReseted(self):
super(dealers_trade_zero_lag_macd_strategy, self).OnReseted()
self._long_entries = []
self._short_entries = []
self._last_long_entry_price = 0.0
self._last_short_entry_price = 0.0
self._previous_macd = 0.0
self._has_previous_macd = False
self._pip_size = 0.0
def OnStarted2(self, time):
super(dealers_trade_zero_lag_macd_strategy, self).OnStarted2(time)
fast_zlema = ZeroLagExponentialMovingAverage()
fast_zlema.Length = self._fast_length.Value
slow_zlema = ZeroLagExponentialMovingAverage()
slow_zlema.Length = self._slow_length.Value
self._signal_ema = ExponentialMovingAverage()
self._signal_ema.Length = self._signal_length.Value
decimals = 0
step = 0.0001
if self.Security is not None:
if self.Security.Decimals is not None:
decimals = int(self.Security.Decimals)
if self.Security.PriceStep is not None:
step = float(self.Security.PriceStep)
if step <= 0:
step = 0.0001
factor = 10.0 if (decimals == 3 or decimals == 5) else 1.0
self._pip_size = step * factor
subscription = self.SubscribeCandles(self.candle_type)
subscription.Bind(fast_zlema, slow_zlema, self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, fast_zlema)
self.DrawIndicator(area, slow_zlema)
self.DrawOwnTrades(area)
def _process_candle(self, candle, fast_val, slow_val):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_val)
slow_val = float(slow_val)
macd = fast_val - slow_val
process_float(self._signal_ema, macd, candle.CloseTime, True)
if not self._has_previous_macd:
self._previous_macd = macd
self._has_previous_macd = True
return
direction = 3
if macd > self._previous_macd and macd != 0 and self._previous_macd != 0:
direction = 2
elif macd < self._previous_macd and macd != 0 and self._previous_macd != 0:
direction = 1
if self._reverse_condition.Value:
if direction == 1:
direction = 2
elif direction == 2:
direction = 1
self._previous_macd = macd
open_positions = len(self._long_entries) + len(self._short_entries)
continue_opening = open_positions <= self._max_positions.Value
if direction != 3 and open_positions > self._max_positions.Value:
self._close_minimum_profit(float(candle.ClosePrice))
return
closed_this_bar = self._manage_positions(candle)
if closed_this_bar:
return
total_profit = self._get_total_profit(float(candle.ClosePrice))
if (self._account_protection.Value and open_positions > self._positions_for_protection.Value
and total_profit >= self._secure_profit.Value):
self._close_maximum_profit(float(candle.ClosePrice))
return
if not continue_opening:
return
if direction == 2:
self._try_open_long(candle, open_positions)
elif direction == 1:
self._try_open_short(candle, open_positions)
def _try_open_long(self, candle, open_positions):
interval = self._get_interval_distance(open_positions)
can_open = len(self._long_entries) == 0 or self._last_long_entry_price - float(candle.ClosePrice) >= interval
if not can_open:
return
stop_dist = self._stop_loss_pips.Value * self._pip_size if self._stop_loss_pips.Value > 0 else 0.0
take_dist = self._take_profit_pips.Value * self._pip_size if self._take_profit_pips.Value > 0 else 0.0
if take_dist > 0:
take_dist *= self._pow(self._tp_coefficient.Value, open_positions + 1)
trailing_dist = self._trailing_stop_pips.Value * self._pip_size if self._trailing_stop_pips.Value > 0 else 0.0
trailing_step = self._trailing_step_pips.Value * self._pip_size if self._trailing_step_pips.Value > 0 else 0.0
price = float(candle.ClosePrice)
entry = {
"side": "buy",
"entry_price": price,
"volume": 1.0,
"stop_loss": price - stop_dist if stop_dist > 0 else None,
"take_profit": price + take_dist if take_dist > 0 else None,
"trailing_distance": trailing_dist,
"trailing_step": trailing_step,
"trailing_stop": None,
}
self._long_entries.append(entry)
self._last_long_entry_price = price
self.BuyMarket()
def _try_open_short(self, candle, open_positions):
interval = self._get_interval_distance(open_positions)
can_open = len(self._short_entries) == 0 or float(candle.ClosePrice) - self._last_short_entry_price >= interval
if not can_open:
return
stop_dist = self._stop_loss_pips.Value * self._pip_size if self._stop_loss_pips.Value > 0 else 0.0
take_dist = self._take_profit_pips.Value * self._pip_size if self._take_profit_pips.Value > 0 else 0.0
if take_dist > 0:
take_dist *= self._pow(self._tp_coefficient.Value, open_positions + 1)
trailing_dist = self._trailing_stop_pips.Value * self._pip_size if self._trailing_stop_pips.Value > 0 else 0.0
trailing_step = self._trailing_step_pips.Value * self._pip_size if self._trailing_step_pips.Value > 0 else 0.0
price = float(candle.ClosePrice)
entry = {
"side": "sell",
"entry_price": price,
"volume": 1.0,
"stop_loss": price + stop_dist if stop_dist > 0 else None,
"take_profit": price - take_dist if take_dist > 0 else None,
"trailing_distance": trailing_dist,
"trailing_step": trailing_step,
"trailing_stop": None,
}
self._short_entries.append(entry)
self._last_short_entry_price = price
self.SellMarket()
def _manage_positions(self, candle):
closed = False
if self._manage_entries(self._long_entries, candle, True):
closed = True
if self._manage_entries(self._short_entries, candle, False):
closed = True
return closed
def _manage_entries(self, entries, candle, is_long):
closed = False
to_remove = []
for i, entry in enumerate(entries):
if is_long:
if entry["stop_loss"] is not None and float(candle.LowPrice) <= entry["stop_loss"]:
self.SellMarket()
to_remove.append(i)
closed = True
continue
if entry["take_profit"] is not None and float(candle.HighPrice) >= entry["take_profit"]:
self.SellMarket()
to_remove.append(i)
closed = True
continue
if entry["trailing_distance"] > 0:
profit = float(candle.ClosePrice) - entry["entry_price"]
if profit > entry["trailing_distance"] + entry["trailing_step"]:
new_stop = float(candle.ClosePrice) - entry["trailing_distance"]
if entry["trailing_stop"] is None or entry["trailing_stop"] < new_stop:
entry["trailing_stop"] = new_stop
if entry["trailing_stop"] is not None and float(candle.LowPrice) <= entry["trailing_stop"]:
self.SellMarket()
to_remove.append(i)
closed = True
else:
if entry["stop_loss"] is not None and float(candle.HighPrice) >= entry["stop_loss"]:
self.BuyMarket()
to_remove.append(i)
closed = True
continue
if entry["take_profit"] is not None and float(candle.LowPrice) <= entry["take_profit"]:
self.BuyMarket()
to_remove.append(i)
closed = True
continue
if entry["trailing_distance"] > 0:
profit = entry["entry_price"] - float(candle.ClosePrice)
if profit > entry["trailing_distance"] + entry["trailing_step"]:
new_stop = float(candle.ClosePrice) + entry["trailing_distance"]
if entry["trailing_stop"] is None or entry["trailing_stop"] > new_stop:
entry["trailing_stop"] = new_stop
if entry["trailing_stop"] is not None and float(candle.HighPrice) >= entry["trailing_stop"]:
self.BuyMarket()
to_remove.append(i)
closed = True
for i in reversed(to_remove):
entries.pop(i)
return closed
def _close_maximum_profit(self, price):
best = None
best_profit = -999999999.0
best_list = None
best_idx = -1
for i, entry in enumerate(self._long_entries):
p = price - entry["entry_price"]
if p > best_profit:
best_profit = p
best = entry
best_list = self._long_entries
best_idx = i
for i, entry in enumerate(self._short_entries):
p = entry["entry_price"] - price
if p > best_profit:
best_profit = p
best = entry
best_list = self._short_entries
best_idx = i
if best is not None:
if best["side"] == "buy":
self.SellMarket()
else:
self.BuyMarket()
best_list.pop(best_idx)
def _close_minimum_profit(self, price):
worst = None
worst_profit = 999999999.0
worst_list = None
worst_idx = -1
for i, entry in enumerate(self._long_entries):
p = price - entry["entry_price"]
if p < worst_profit:
worst_profit = p
worst = entry
worst_list = self._long_entries
worst_idx = i
for i, entry in enumerate(self._short_entries):
p = entry["entry_price"] - price
if p < worst_profit:
worst_profit = p
worst = entry
worst_list = self._short_entries
worst_idx = i
if worst is not None:
if worst["side"] == "buy":
self.SellMarket()
else:
self.BuyMarket()
worst_list.pop(worst_idx)
def _get_total_profit(self, price):
total = 0.0
for entry in self._long_entries:
total += price - entry["entry_price"]
for entry in self._short_entries:
total += entry["entry_price"] - price
return total
def _get_interval_distance(self, open_positions):
distance = self._interval_pips.Value * self._pip_size if self._interval_pips.Value > 0 else 0.0
if distance <= 0:
return 0.0
if open_positions > 0:
distance *= self._pow(self._interval_coefficient.Value, open_positions)
return distance
def _pow(self, value, exponent):
result = 1.0
for _ in range(exponent):
result *= value
return result
def CreateClone(self):
return dealers_trade_zero_lag_macd_strategy()