Dealers Trade ZeroLag MACD 策略
概述
该策略将 MetaTrader 平台上的 "Dealers Trade v 7.91 ZeroLag MACD" 专家顾问迁移到 StockSharp 高级 API。策略通过观察 ZeroLag MACD 线的斜率来判断市场方向,并在趋势方向上构建具有自适应间距的网格仓位,同时执行严格的风险控制。默认的分析周期为四小时 K 线,但可自定义任何支持的蜡烛类型。
运行逻辑
- 信号判定:使用两条零延迟指数移动平均(ZLEMA)生成 MACD 线。当 MACD 相比前一根 K 线向上时视为看多,向下则视为看空;
ReverseCondition可以反向解释信号。 - 网格加仓:在检测到的方向上逐步加仓。相邻建仓点之间的距离以点数(
IntervalPips)表示,并按IntervalCoefficient逐步放大。每次加仓的手数按LotMultiplier乘法放大,复制原策略的马丁加仓方式。 - 仓位规模:
BaseVolume大于零时直接作为初始手数;当其为零时,根据RiskPercent、止损距离以及品种的价格步长自动推算手数,并确保不超过交易所限制及MaxVolume。 - 仓位管理:每笔仓位都可以设置止损、止盈以及跟踪止损(全部以点数表示)。后续仓位的止盈距离会按
TakeProfitCoefficient扩大,以便更好地管理分层盈利。 - 账户保护:当持仓数量超过
PositionsForProtection且累计浮动利润达到SecureProfit时,策略会平掉盈利最高的仓位以锁定收益;若仓位数超过MaxPositions,则会先行关闭浮动亏损最大的仓位。
仓位跟踪
- 策略只在蜡烛收盘后处理信号与风控逻辑,并使用收盘价、最高价、最低价进行判断。
- 每笔仓位都记录自己的成交价格、数量以及当前跟踪止损位置,最新成交价用于控制下一笔加仓的最小距离。
- 当账户余额低于
MinimumBalance时策略自动停止,避免在资金不足时继续运行。
参数说明
BaseVolume:初始下单数量(为 0 时启用按风险计算)。RiskPercent:按风险计算手数时使用的账户风险百分比。MaxPositions:允许同时持有的最大仓位数量。IntervalPips/IntervalCoefficient:网格初始间距及其放大系数。StopLossPips、TakeProfitPips:止损和止盈距离(点)。TrailingStopPips、TrailingStepPips:跟踪止损的距离与触发阈值。TakeProfitCoefficient:后续仓位止盈距离的放大倍数。SecureProfit、AccountProtection、PositionsForProtection:账户保护相关参数。ReverseCondition:是否反转 MACD 斜率的判断方向。FastLength、SlowLength、SignalLength:零延迟指数移动平均的周期。MaxVolume:单笔仓位的最大数量限制。LotMultiplier:每次加仓数量的放大倍数。MinimumBalance:继续交易所需的最低账户余额。CandleType:使用的蜡烛类型。
使用建议
- 启动前需先绑定证券和投资组合,并确认交易所参数正确。
- 检查品种的价格步长与步长价值,确保点数与实际价格变动之间的换算准确。
- 默认参数与原 MQL 策略一致,可通过 StockSharp 的优化工具进一步优化。
- 本任务仅提供 C# 版本,未包含 Python 实现。
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()