Dealers Trade ZeroLag MACD Strategy
Overview
This strategy ports the MetaTrader expert advisor "Dealers Trade v 7.91 ZeroLag MACD" to the StockSharp high level API. It tracks the slope of a zero lag MACD to decide whether the market is in an accumulation phase for longs or shorts and builds a grid of positions with adaptive spacing and risk management. The default timeframe is four-hour candles as recommended by the original author, but any candle type supported by StockSharp can be selected.
Trading logic
- Signal detection. Two zero lag exponential moving averages (fast and slow) generate a MACD line. When the MACD rises compared to the previous bar the strategy treats the market as bullish; when it falls it treats the market as bearish. The signal can be inverted via the
ReverseConditionparameter. - Position grid. The algorithm scales into the detected direction. Distances between entries are measured in pips and multiplied after each fill by
IntervalCoefficient. The lot size is multiplied byLotMultiplieron every additional entry, mimicking the martingale scheme from the MQL version. - Volume control. If
BaseVolumeis greater than zero it is used as the initial order quantity. Otherwise the engine derives the size fromRiskPercent, stop distance and the instrument step parameters. Each calculated volume is checked against the instrument limits and capped byMaxVolume. - Order management. Every entry can be equipped with a stop loss, take profit and trailing stop (all in pips). The take profit distance is multiplied by
TakeProfitCoefficientfor successive entries to widen targets. - Account protection. When the total number of open positions exceeds
PositionsForProtectionand their combined profit reachesSecureProfit, the strategy closes the trade with the largest profit to lock in gains. If the total number of positions exceedsMaxPositionsit closes the worst performing trade before accepting new entries.
Position handling
- Stops, trailing logic and targets are evaluated on finished candles using close, high and low prices.
- All open positions are tracked with their own volume, entry price and trailing state. The last fill price is reused to enforce the minimum spacing for future entries.
- When the account balance falls below
MinimumBalancethe strategy stops itself to avoid overtrading on small accounts.
Parameters
| Parameter | Description |
|---|---|
BaseVolume |
Initial order size. Set to zero to enable risk-based sizing via RiskPercent. |
RiskPercent |
Percentage of portfolio equity to risk when position size is derived from the stop distance. |
MaxPositions |
Maximum number of simultaneously open entries. |
IntervalPips |
Initial spacing between grid entries in pips. |
IntervalCoefficient |
Multiplier applied to the spacing after each additional entry. |
StopLossPips |
Stop loss distance in pips. Set to zero to disable. |
TakeProfitPips |
Base take profit distance in pips. Multiplied by TakeProfitCoefficient per entry. |
TrailingStopPips / TrailingStepPips |
Trailing stop distance and required advance before the trail is adjusted. |
TakeProfitCoefficient |
Multiplier for widening take profit distances on later entries. |
SecureProfit |
Profit threshold that triggers account protection once enough positions are open. |
AccountProtection |
Enables automatic profit locking by closing the best trade. |
PositionsForProtection |
Minimum number of open positions required before account protection becomes active. |
ReverseCondition |
Inverts the MACD slope interpretation. |
FastLength, SlowLength, SignalLength |
Periods of the zero lag exponential moving averages. |
MaxVolume |
Cap for the volume of a single entry. |
LotMultiplier |
Multiplicative factor for scaling position size with each grid entry. |
MinimumBalance |
Minimal account balance required to continue trading. |
CandleType |
Candle data type used for calculations. |
Usage notes
- Connect the strategy to a portfolio and security before starting it.
- Review the instrument step and price settings to ensure pip conversions are correct.
- The default parameters replicate the original expert advisor behaviour but can be optimised through StockSharp optimisers.
- Python translation is not included for this strategy.
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()