Dealers Trade MACD Strategy
The Dealers Trade MACD strategy is a pyramiding system that was ported from the original MQL5 "Dealers Trade v7.74" expert advisor. It follows the slope of the MACD main line to decide when to accumulate positions in the trend direction. The logic is designed for swing trading on H4 and D1 charts where momentum shifts are less noisy.
How the strategy works
- Signal generation – the strategy subscribes to candles of the selected timeframe and evaluates the MACD main line value on every closed bar. A rising MACD implies long bias and a falling MACD implies short bias. The signal can be inverted with the
ReverseConditionparameter to match accounts that historically traded contrarian entries. - Position sizing – the first order uses either the fixed
FixedVolumesize or, if it is set to0, the system allocates risk dynamically from portfolio equity using theRiskPercentparameter and the configured stop loss distance. Additional entries are multiplied byVolumeMultiplierraised to the current position count (e.g. 1.6, 1.6², 1.6³, …) and are only sent when the price has moved by at leastIntervalPoints * PriceStepfrom the last fill. Orders are skipped once the net exposure would exceedMaxVolumeor the number of entries reachesMaxPositions. - Order management – every position keeps its own stop loss and take profit targets calculated from the entry price and the point-based offsets (
StopLossPoints,TakeProfitPoints). IfTrailingStopPointsis greater than zero the stop is pulled up (or down for shorts) once the profit exceedsTrailingStopPoints + TrailingStepPoints, emulating the original trailing behaviour. - Account protection – when the number of open trades is greater than
PositionsForProtectionand the aggregated unrealised profit crossesSecureProfit, the strategy closes the most profitable leg to lock in gains before adding new exposure. This mirrors the "Account protection" block from the MQL version.
Parameters
| Name | Default | Description |
|---|---|---|
CandleType |
H4 | Timeframe used for MACD calculations and trade decisions. |
FixedVolume |
0.1 | Lot size for the first entry. Set to 0 to enable risk-based sizing. |
RiskPercent |
5 | Percentage of current equity risked when FixedVolume is zero. |
StopLossPoints |
90 | Stop loss distance expressed in price steps. Use 0 to disable hard stops. |
TakeProfitPoints |
30 | Take profit distance in price steps. Use 0 to disable. |
TrailingStopPoints |
15 | Trailing stop distance in price steps. Set to 0 to turn trailing off. |
TrailingStepPoints |
5 | Additional distance that must be gained before the trailing stop moves again. |
MaxPositions |
5 | Maximum number of simultaneously open entries. |
IntervalPoints |
15 | Minimum distance in price steps required between consecutive entries. |
SecureProfit |
50 | Profit threshold (in quote currency) that triggers account protection. |
AccountProtection |
true | Enables closing the best performing trade when the secure profit target is reached. |
PositionsForProtection |
3 | Minimum number of trades that must be open before protection can trigger. |
ReverseCondition |
false | Inverts the MACD slope interpretation. |
MacdFastPeriod |
14 | Fast EMA length for the MACD indicator. |
MacdSlowPeriod |
26 | Slow EMA length for the MACD indicator. |
MacdSignalPeriod |
1 | Signal EMA length for the MACD indicator (set to 1 in the original expert advisor). |
MaxVolume |
5 | Upper cap for the cumulative position size. |
VolumeMultiplier |
1.6 | Multiplier applied to the base size for every new entry. |
Notes and limitations
- The original MQL expert was able to hold long and short hedged positions simultaneously. StockSharp uses netted positions by default, therefore this port closes opposite exposure before adding new trades in the other direction.
- MACD values are evaluated on closed candles only. Intrabar signals may appear later than in the tick-based MQL implementation, but the behaviour is far more stable for historical testing.
- All point-based distances are multiplied by the instrument
PriceStep. If the security does not provide that metadata the strategy falls back to a 0.0001 step, so adjust parameters when trading instruments with different tick sizes. - When
FixedVolumeis zero the strategy requires a non-zero stop loss distance to calculate risk-based sizing. If the stop is disabled the volume defaults to zero and no trade is sent.
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>
/// Dealers Trade MACD strategy converted from MQL5 implementation.
/// </summary>
public class DealersTradeMacdStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<decimal> _fixedVolume;
private readonly StrategyParam<decimal> _riskPercent;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _takeProfitPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<decimal> _trailingStepPoints;
private readonly StrategyParam<int> _maxPositions;
private readonly StrategyParam<decimal> _intervalPoints;
private readonly StrategyParam<decimal> _secureProfit;
private readonly StrategyParam<bool> _accountProtection;
private readonly StrategyParam<int> _positionsForProtection;
private readonly StrategyParam<bool> _reverseCondition;
private readonly StrategyParam<int> _macdFastPeriod;
private readonly StrategyParam<int> _macdSlowPeriod;
private readonly StrategyParam<int> _macdSignalPeriod;
private readonly StrategyParam<decimal> _maxVolume;
private readonly StrategyParam<decimal> _volumeMultiplier;
private MovingAverageConvergenceDivergence _macd = null!;
private decimal? _previousMacd;
private decimal _lastEntryPrice;
private int _cooldown;
private readonly List<PositionState> _longPositions = new();
private readonly List<PositionState> _shortPositions = new();
/// <summary>
/// Initializes a new instance of <see cref="DealersTradeMacdStrategy"/>.
/// </summary>
public DealersTradeMacdStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe", "General");
_fixedVolume = Param(nameof(FixedVolume), 0.1m)
.SetDisplay("Fixed Volume", "Lot size used when above zero", "Risk");
_riskPercent = Param(nameof(RiskPercent), 5m)
.SetDisplay("Risk %", "Risk percent when fixed volume is zero", "Risk");
_stopLossPoints = Param(nameof(StopLossPoints), 90m)
.SetDisplay("Stop Loss pts", "Stop loss distance in price steps", "Risk")
;
_takeProfitPoints = Param(nameof(TakeProfitPoints), 30m)
.SetDisplay("Take Profit pts", "Take profit distance in price steps", "Risk")
;
_trailingStopPoints = Param(nameof(TrailingStopPoints), 15m)
.SetDisplay("Trailing Stop pts", "Trailing stop distance in price steps", "Risk");
_trailingStepPoints = Param(nameof(TrailingStepPoints), 5m)
.SetDisplay("Trailing Step pts", "Additional distance before trailing updates", "Risk");
_maxPositions = Param(nameof(MaxPositions), 2)
.SetDisplay("Max Positions", "Maximum concurrent entries", "Money Management");
_intervalPoints = Param(nameof(IntervalPoints), 50m)
.SetDisplay("Interval pts", "Minimum distance between new entries", "Money Management");
_secureProfit = Param(nameof(SecureProfit), 50m)
.SetDisplay("Secure Profit", "Profit threshold that triggers protection", "Money Management");
_accountProtection = Param(nameof(AccountProtection), true)
.SetDisplay("Account Protection", "Close best trade after reaching secure profit", "Money Management");
_positionsForProtection = Param(nameof(PositionsForProtection), 3)
.SetDisplay("Protect From", "Minimum positions before triggering protection", "Money Management");
_reverseCondition = Param(nameof(ReverseCondition), false)
.SetDisplay("Reverse Signal", "Invert MACD slope direction", "Trading");
_macdFastPeriod = Param(nameof(MacdFastPeriod), 14)
.SetDisplay("MACD Fast", "Fast EMA period", "Indicators");
_macdSlowPeriod = Param(nameof(MacdSlowPeriod), 26)
.SetDisplay("MACD Slow", "Slow EMA period", "Indicators");
_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 1)
.SetDisplay("MACD Signal", "Signal EMA period", "Indicators");
_maxVolume = Param(nameof(MaxVolume), 5m)
.SetDisplay("Max Volume", "Absolute cap for trade volume", "Risk")
.SetGreaterThanZero();
_volumeMultiplier = Param(nameof(VolumeMultiplier), 1.6m)
.SetDisplay("Volume Multiplier", "Multiplier for additional positions", "Money Management")
.SetGreaterThanZero();
}
/// <summary>
/// Candle type used for signal calculations.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Fixed lot size. When zero risk based sizing is used.
/// </summary>
public decimal FixedVolume
{
get => _fixedVolume.Value;
set => _fixedVolume.Value = value;
}
/// <summary>
/// Percent of equity risked when sizing dynamically.
/// </summary>
public decimal RiskPercent
{
get => _riskPercent.Value;
set => _riskPercent.Value = value;
}
/// <summary>
/// Stop loss distance in price steps.
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take profit distance in price steps.
/// </summary>
public decimal TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Trailing stop distance in price steps.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Extra distance required before the trailing stop moves.
/// </summary>
public decimal TrailingStepPoints
{
get => _trailingStepPoints.Value;
set => _trailingStepPoints.Value = value;
}
/// <summary>
/// Maximum number of open entries.
/// </summary>
public int MaxPositions
{
get => _maxPositions.Value;
set => _maxPositions.Value = value;
}
/// <summary>
/// Minimum price distance between sequential entries.
/// </summary>
public decimal IntervalPoints
{
get => _intervalPoints.Value;
set => _intervalPoints.Value = value;
}
/// <summary>
/// Profit target for account protection logic.
/// </summary>
public decimal SecureProfit
{
get => _secureProfit.Value;
set => _secureProfit.Value = value;
}
/// <summary>
/// Enables profit locking when enough trades are open.
/// </summary>
public bool AccountProtection
{
get => _accountProtection.Value;
set => _accountProtection.Value = value;
}
/// <summary>
/// Minimum number of positions before account protection activates.
/// </summary>
public int PositionsForProtection
{
get => _positionsForProtection.Value;
set => _positionsForProtection.Value = value;
}
/// <summary>
/// Inverts the MACD slope direction.
/// </summary>
public bool ReverseCondition
{
get => _reverseCondition.Value;
set => _reverseCondition.Value = value;
}
/// <summary>
/// MACD fast EMA period.
/// </summary>
public int MacdFastPeriod
{
get => _macdFastPeriod.Value;
set => _macdFastPeriod.Value = value;
}
/// <summary>
/// MACD slow EMA period.
/// </summary>
public int MacdSlowPeriod
{
get => _macdSlowPeriod.Value;
set => _macdSlowPeriod.Value = value;
}
/// <summary>
/// MACD signal EMA period.
/// </summary>
public int MacdSignalPeriod
{
get => _macdSignalPeriod.Value;
set => _macdSignalPeriod.Value = value;
}
/// <summary>
/// Maximum allowed total volume.
/// </summary>
public decimal MaxVolume
{
get => _maxVolume.Value;
set => _maxVolume.Value = value;
}
/// <summary>
/// Multiplier applied to the base volume for each additional entry.
/// </summary>
public decimal VolumeMultiplier
{
get => _volumeMultiplier.Value;
set => _volumeMultiplier.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_macd?.Reset();
_previousMacd = null;
_lastEntryPrice = 0m;
_cooldown = 0;
_longPositions.Clear();
_shortPositions.Clear();
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_macd = new MovingAverageConvergenceDivergence(
new ExponentialMovingAverage { Length = MacdSlowPeriod },
new ExponentialMovingAverage { Length = MacdFastPeriod }
);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_macd, ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal macdValue)
{
if (candle.State != CandleStates.Finished)
return;
HandleTrailingAndExits(candle);
if (!IsFormedAndOnlineAndAllowTrading())
{
_previousMacd = macdValue;
return;
}
if (_cooldown > 0)
{
_cooldown--;
_previousMacd = macdValue;
return;
}
var openPositions = _longPositions.Count + _shortPositions.Count;
var continueOpening = openPositions < MaxPositions;
var direction = 0;
if (_previousMacd is null)
{
_previousMacd = macdValue;
return;
}
if (macdValue > _previousMacd)
direction = 1;
else if (macdValue < _previousMacd)
direction = -1;
if (ReverseCondition)
direction = -direction;
if (AccountProtection && openPositions > PositionsForProtection)
{
var totalProfit = CalculateTotalProfit(candle.ClosePrice);
if (totalProfit >= SecureProfit)
{
CloseMostProfitablePosition(candle.ClosePrice);
_previousMacd = macdValue;
return;
}
}
if (continueOpening && direction > 0 && _shortPositions.Count == 0)
TryOpenLong(candle);
else if (continueOpening && direction < 0 && _longPositions.Count == 0)
TryOpenShort(candle);
_previousMacd = macdValue;
}
private void HandleTrailingAndExits(ICandleMessage candle)
{
var step = GetPriceStep();
var trailingDistance = TrailingStopPoints * step;
var trailingActivation = (TrailingStopPoints + TrailingStepPoints) * step;
// Collect exits first, then execute to avoid collection modification during enumeration
var longExits = new List<PositionState>();
var longSnapshot = _longPositions.ToList();
foreach (var state in longSnapshot)
{
if (state.TakeProfitPrice > 0 && candle.HighPrice >= state.TakeProfitPrice)
{
longExits.Add(state);
continue;
}
if (state.StopPrice > 0 && candle.LowPrice <= state.StopPrice)
{
longExits.Add(state);
continue;
}
if (TrailingStopPoints > 0 && candle.ClosePrice - state.EntryPrice > trailingActivation)
{
var candidateStop = candle.ClosePrice - trailingDistance;
if (state.StopPrice == 0m || state.StopPrice < candle.ClosePrice - trailingActivation)
state.StopPrice = candidateStop;
}
}
foreach (var state in longExits)
{
Volume = state.Volume;
SellMarket();
_longPositions.Remove(state);
_lastEntryPrice = 0m;
}
var shortExits = new List<PositionState>();
var shortSnapshot = _shortPositions.ToList();
foreach (var state in shortSnapshot)
{
if (state.TakeProfitPrice > 0 && candle.LowPrice <= state.TakeProfitPrice)
{
shortExits.Add(state);
continue;
}
if (state.StopPrice > 0 && candle.HighPrice >= state.StopPrice)
{
shortExits.Add(state);
continue;
}
if (TrailingStopPoints > 0 && state.EntryPrice - candle.ClosePrice > trailingActivation)
{
var candidateStop = candle.ClosePrice + trailingDistance;
if (state.StopPrice == 0m || state.StopPrice > candle.ClosePrice + trailingActivation)
state.StopPrice = candidateStop;
}
}
foreach (var state in shortExits)
{
Volume = state.Volume;
BuyMarket();
_shortPositions.Remove(state);
_lastEntryPrice = 0m;
}
}
private void TryOpenLong(ICandleMessage candle)
{
var step = GetPriceStep();
var interval = IntervalPoints * step;
if (_lastEntryPrice != 0m && Math.Abs(_lastEntryPrice - candle.ClosePrice) < interval)
return;
var baseVolume = FixedVolume > 0 ? FixedVolume : CalculateRiskVolume(step);
if (baseVolume <= 0)
return;
var openPositions = _longPositions.Count + _shortPositions.Count;
var lotCoefficient = openPositions == 0 ? 1m : Pow(VolumeMultiplier, openPositions + 1);
var volume = NormalizeVolume(baseVolume * lotCoefficient);
if (volume <= 0 || volume > MaxVolume)
return;
var stopDistance = StopLossPoints * step;
var takeDistance = TakeProfitPoints * step;
Volume = volume;
BuyMarket();
_longPositions.Add(new PositionState
{
EntryPrice = candle.ClosePrice,
Volume = volume,
StopPrice = stopDistance > 0 ? candle.ClosePrice - stopDistance : 0m,
TakeProfitPrice = takeDistance > 0 ? candle.ClosePrice + takeDistance : 0m
});
_lastEntryPrice = candle.ClosePrice;
_cooldown = 3;
}
private void TryOpenShort(ICandleMessage candle)
{
var step = GetPriceStep();
var interval = IntervalPoints * step;
if (_lastEntryPrice != 0m && Math.Abs(_lastEntryPrice - candle.ClosePrice) < interval)
return;
var baseVolume = FixedVolume > 0 ? FixedVolume : CalculateRiskVolume(step);
if (baseVolume <= 0)
return;
var openPositions = _longPositions.Count + _shortPositions.Count;
var lotCoefficient = openPositions == 0 ? 1m : Pow(VolumeMultiplier, openPositions + 1);
var volume = NormalizeVolume(baseVolume * lotCoefficient);
if (volume <= 0 || volume > MaxVolume)
return;
var stopDistance = StopLossPoints * step;
var takeDistance = TakeProfitPoints * step;
Volume = volume;
SellMarket();
_shortPositions.Add(new PositionState
{
EntryPrice = candle.ClosePrice,
Volume = volume,
StopPrice = stopDistance > 0 ? candle.ClosePrice + stopDistance : 0m,
TakeProfitPrice = takeDistance > 0 ? candle.ClosePrice - takeDistance : 0m
});
_lastEntryPrice = candle.ClosePrice;
_cooldown = 3;
}
private decimal CalculateRiskVolume(decimal priceStep)
{
if (StopLossPoints <= 0)
return 0m;
var stopDistance = StopLossPoints * priceStep;
if (stopDistance <= 0)
return 0m;
if (Portfolio is null)
return 0m;
var equity = Portfolio.CurrentValue ?? 0m;
if (equity <= 0)
return 0m;
var riskAmount = equity * (RiskPercent / 100m);
return riskAmount / stopDistance;
}
private decimal CalculateTotalProfit(decimal currentPrice)
{
decimal profit = 0m;
foreach (var pos in _longPositions)
profit += (currentPrice - pos.EntryPrice) * pos.Volume;
foreach (var pos in _shortPositions)
profit += (pos.EntryPrice - currentPrice) * pos.Volume;
return profit;
}
private void CloseMostProfitablePosition(decimal currentPrice)
{
PositionState best = null;
var bestIsLong = false;
decimal bestProfit = 0m;
foreach (var pos in _longPositions)
{
var profit = (currentPrice - pos.EntryPrice) * pos.Volume;
if (profit > bestProfit)
{
bestProfit = profit;
best = pos;
bestIsLong = true;
}
}
foreach (var pos in _shortPositions)
{
var profit = (pos.EntryPrice - currentPrice) * pos.Volume;
if (profit > bestProfit)
{
bestProfit = profit;
best = pos;
bestIsLong = false;
}
}
if (best is null || bestProfit <= 0m)
return;
if (bestIsLong)
{
SellMarket();
_longPositions.Remove(best);
}
else
{
BuyMarket();
_shortPositions.Remove(best);
}
_lastEntryPrice = 0m;
}
private decimal NormalizeVolume(decimal volume)
{
if (volume <= 0)
return 0m;
var step = Security?.VolumeStep ?? 0m;
if (step > 0)
{
var steps = Math.Floor(volume / step);
volume = steps * step;
}
return volume;
}
private decimal GetPriceStep()
{
var step = Security?.PriceStep ?? 0m;
if (step > 0)
return step;
var decimals = Security?.Decimals ?? 0;
if (decimals > 0)
return (decimal)Math.Pow(10, -decimals);
return 0.0001m;
}
private static decimal Pow(decimal value, int power)
{
if (power <= 0)
return 1m;
return (decimal)Math.Pow((double)value, power);
}
private sealed class PositionState
{
public decimal EntryPrice { get; set; }
public decimal Volume { get; set; }
public decimal StopPrice { get; set; }
public decimal TakeProfitPrice { get; set; }
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import (
MovingAverageConvergenceDivergence, ExponentialMovingAverage
)
from StockSharp.Algo.Strategies import Strategy
class dealers_trade_macd_strategy(Strategy):
def __init__(self):
super(dealers_trade_macd_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._fixed_volume = self.Param("FixedVolume", 0.1)
self._risk_percent = self.Param("RiskPercent", 5.0)
self._stop_loss_points = self.Param("StopLossPoints", 90.0)
self._take_profit_points = self.Param("TakeProfitPoints", 30.0)
self._trailing_stop_points = self.Param("TrailingStopPoints", 15.0)
self._trailing_step_points = self.Param("TrailingStepPoints", 5.0)
self._max_positions = self.Param("MaxPositions", 2)
self._interval_points = self.Param("IntervalPoints", 50.0)
self._secure_profit = self.Param("SecureProfit", 50.0)
self._account_protection = self.Param("AccountProtection", True)
self._positions_for_protection = self.Param("PositionsForProtection", 3)
self._reverse_condition = self.Param("ReverseCondition", False)
self._macd_fast_period = self.Param("MacdFastPeriod", 14)
self._macd_slow_period = self.Param("MacdSlowPeriod", 26)
self._macd_signal_period = self.Param("MacdSignalPeriod", 1)
self._max_volume = self.Param("MaxVolume", 5.0)
self._volume_multiplier = self.Param("VolumeMultiplier", 1.6)
self._macd = None
self._previous_macd = None
self._last_entry_price = 0.0
self._cooldown = 0
self._long_positions = []
self._short_positions = []
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def FixedVolume(self):
return self._fixed_volume.Value
@property
def RiskPercent(self):
return self._risk_percent.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TakeProfitPoints(self):
return self._take_profit_points.Value
@property
def TrailingStopPoints(self):
return self._trailing_stop_points.Value
@property
def TrailingStepPoints(self):
return self._trailing_step_points.Value
@property
def MaxPositions(self):
return self._max_positions.Value
@property
def IntervalPoints(self):
return self._interval_points.Value
@property
def SecureProfit(self):
return self._secure_profit.Value
@property
def AccountProtection(self):
return self._account_protection.Value
@property
def PositionsForProtection(self):
return self._positions_for_protection.Value
@property
def ReverseCondition(self):
return self._reverse_condition.Value
@property
def MacdFastPeriod(self):
return self._macd_fast_period.Value
@property
def MacdSlowPeriod(self):
return self._macd_slow_period.Value
@property
def MacdSignalPeriod(self):
return self._macd_signal_period.Value
@property
def MaxVolume(self):
return self._max_volume.Value
@property
def VolumeMultiplier(self):
return self._volume_multiplier.Value
def OnStarted2(self, time):
super(dealers_trade_macd_strategy, self).OnStarted2(time)
slow_ema = ExponentialMovingAverage()
slow_ema.Length = self.MacdSlowPeriod
fast_ema = ExponentialMovingAverage()
fast_ema.Length = self.MacdFastPeriod
self._macd = MovingAverageConvergenceDivergence(slow_ema, fast_ema)
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._macd, self._process_candle).Start()
def _process_candle(self, candle, macd_value):
if candle.State != CandleStates.Finished:
return
self._handle_trailing_and_exits(candle)
if not self.IsFormedAndOnlineAndAllowTrading():
self._previous_macd = macd_value
return
if self._cooldown > 0:
self._cooldown -= 1
self._previous_macd = macd_value
return
open_positions = len(self._long_positions) + len(self._short_positions)
continue_opening = open_positions < self.MaxPositions
direction = 0
if self._previous_macd is None:
self._previous_macd = macd_value
return
if macd_value > self._previous_macd:
direction = 1
elif macd_value < self._previous_macd:
direction = -1
if self.ReverseCondition:
direction = -direction
if self.AccountProtection and open_positions > self.PositionsForProtection:
total_profit = self._calc_total_profit(float(candle.ClosePrice))
if total_profit >= self.SecureProfit:
self._close_most_profitable(float(candle.ClosePrice))
self._previous_macd = macd_value
return
if continue_opening and direction > 0 and len(self._short_positions) == 0:
self._try_open_long(candle)
elif continue_opening and direction < 0 and len(self._long_positions) == 0:
self._try_open_short(candle)
self._previous_macd = macd_value
def _handle_trailing_and_exits(self, candle):
step = self._get_price_step()
trail_dist = self.TrailingStopPoints * step
trail_act = (self.TrailingStopPoints + self.TrailingStepPoints) * step
long_exits = []
for s in list(self._long_positions):
if s["take"] > 0 and float(candle.HighPrice) >= s["take"]:
long_exits.append(s); continue
if s["stop"] > 0 and float(candle.LowPrice) <= s["stop"]:
long_exits.append(s); continue
if self.TrailingStopPoints > 0 and float(candle.ClosePrice) - s["entry"] > trail_act:
cs = float(candle.ClosePrice) - trail_dist
if s["stop"] == 0 or s["stop"] < float(candle.ClosePrice) - trail_act:
s["stop"] = cs
for s in long_exits:
self.Volume = s["volume"]
self.SellMarket()
self._long_positions.remove(s)
self._last_entry_price = 0.0
short_exits = []
for s in list(self._short_positions):
if s["take"] > 0 and float(candle.LowPrice) <= s["take"]:
short_exits.append(s); continue
if s["stop"] > 0 and float(candle.HighPrice) >= s["stop"]:
short_exits.append(s); continue
if self.TrailingStopPoints > 0 and s["entry"] - float(candle.ClosePrice) > trail_act:
cs = float(candle.ClosePrice) + trail_dist
if s["stop"] == 0 or s["stop"] > float(candle.ClosePrice) + trail_act:
s["stop"] = cs
for s in short_exits:
self.Volume = s["volume"]
self.BuyMarket()
self._short_positions.remove(s)
self._last_entry_price = 0.0
def _try_open_long(self, candle):
step = self._get_price_step()
interval = self.IntervalPoints * step
if self._last_entry_price != 0 and abs(self._last_entry_price - float(candle.ClosePrice)) < interval:
return
base_vol = self.FixedVolume if self.FixedVolume > 0 else self._calc_risk_vol(step)
if base_vol <= 0:
return
n = len(self._long_positions) + len(self._short_positions)
coeff = 1.0 if n == 0 else math.pow(self.VolumeMultiplier, n + 1)
vol = self._norm_vol(base_vol * coeff)
if vol <= 0 or vol > self.MaxVolume:
return
sd = self.StopLossPoints * step
td = self.TakeProfitPoints * step
self.Volume = vol
self.BuyMarket()
self._long_positions.append({
"entry": float(candle.ClosePrice), "volume": vol,
"stop": float(candle.ClosePrice) - sd if sd > 0 else 0,
"take": float(candle.ClosePrice) + td if td > 0 else 0
})
self._last_entry_price = float(candle.ClosePrice)
self._cooldown = 3
def _try_open_short(self, candle):
step = self._get_price_step()
interval = self.IntervalPoints * step
if self._last_entry_price != 0 and abs(self._last_entry_price - float(candle.ClosePrice)) < interval:
return
base_vol = self.FixedVolume if self.FixedVolume > 0 else self._calc_risk_vol(step)
if base_vol <= 0:
return
n = len(self._long_positions) + len(self._short_positions)
coeff = 1.0 if n == 0 else math.pow(self.VolumeMultiplier, n + 1)
vol = self._norm_vol(base_vol * coeff)
if vol <= 0 or vol > self.MaxVolume:
return
sd = self.StopLossPoints * step
td = self.TakeProfitPoints * step
self.Volume = vol
self.SellMarket()
self._short_positions.append({
"entry": float(candle.ClosePrice), "volume": vol,
"stop": float(candle.ClosePrice) + sd if sd > 0 else 0,
"take": float(candle.ClosePrice) - td if td > 0 else 0
})
self._last_entry_price = float(candle.ClosePrice)
self._cooldown = 3
def _calc_risk_vol(self, price_step):
if self.StopLossPoints <= 0:
return 0
sd = self.StopLossPoints * price_step
if sd <= 0 or self.Portfolio is None:
return 0
eq = float(self.Portfolio.CurrentValue) if self.Portfolio.CurrentValue is not None else 0
if eq <= 0:
return 0
return eq * (self.RiskPercent / 100.0) / sd
def _calc_total_profit(self, price):
p = 0.0
for s in self._long_positions:
p += (price - s["entry"]) * s["volume"]
for s in self._short_positions:
p += (s["entry"] - price) * s["volume"]
return p
def _close_most_profitable(self, price):
best = None
best_long = False
best_pnl = 0.0
for s in self._long_positions:
pnl = (price - s["entry"]) * s["volume"]
if pnl > best_pnl:
best_pnl = pnl; best = s; best_long = True
for s in self._short_positions:
pnl = (s["entry"] - price) * s["volume"]
if pnl > best_pnl:
best_pnl = pnl; best = s; best_long = False
if best is None or best_pnl <= 0:
return
if best_long:
self.SellMarket()
self._long_positions.remove(best)
else:
self.BuyMarket()
self._short_positions.remove(best)
self._last_entry_price = 0.0
def _norm_vol(self, vol):
if vol <= 0:
return 0
sec = self.Security
step = float(sec.VolumeStep) if sec is not None and sec.VolumeStep is not None else 0
if step > 0:
vol = math.floor(vol / step) * step
return vol
def _get_price_step(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 0
if step > 0:
return step
d = sec.Decimals if sec is not None and sec.Decimals is not None else 0
if d > 0:
return math.pow(10, -d)
return 0.0001
def OnReseted(self):
super(dealers_trade_macd_strategy, self).OnReseted()
self._previous_macd = None
self._last_entry_price = 0.0
self._cooldown = 0
self._long_positions = []
self._short_positions = []
def CreateClone(self):
return dealers_trade_macd_strategy()