Стратегия Dealers Trade MACD
Перенесённая стратегия Dealers Trade v7.74 MACD – это пирамидальная система, которая открывает серии сделок по направлению наклона основной линии MACD. Она разрабатывалась для таймфреймов H4 и D1, где импульсные развороты менее шумные и дают времени для наращивания позиций.
Как работает стратегия
- Определение сигнала. На каждом закрытом баре выбранного таймфрейма рассчитывается MACD. Рост основной линии воспринимается как бычий сигнал, падение – как медвежий. При необходимости направление можно инвертировать параметром
ReverseCondition. - Управление размером позиции. Первая заявка берёт фиксированный объём
FixedVolume. Если он равен нулю, объём рассчитывается от капитала счёта по долеRiskPercentи расстоянию до стоп-лосса. Каждый следующий вход умножается наVolumeMultiplierв степени текущего количества сделок (1.6, 1.6², 1.6³ …). Новый ордер отправляется только при выполнении двух условий: цена отошла минимум наIntervalPoints * PriceStepот последней покупки и не превышены лимитыMaxPositionsиMaxVolume. - Сопровождение сделок. Для каждой позиции запоминаются собственные уровни стоп-лосса и тейк-профита, рассчитанные в шагах цены (
StopLossPoints,TakeProfitPoints). Если заданTrailingStopPoints, стоп подтягивается вслед за ценой, когда плавающая прибыль превышает суммуTrailingStopPoints + TrailingStepPoints, что повторяет логику MQL-версии. - Защита счёта. Когда число открытых сделок больше
PositionsForProtection, а суммарная нереализованная прибыль достигаетSecureProfit, стратегия фиксирует наиболее прибыльную позицию, прежде чем продолжить пирамиду.
Параметры
| Параметр | Значение по умолчанию | Описание |
|---|---|---|
CandleType |
H4 | Таймфрейм, по которому строятся свечи и рассчитывается MACD. |
FixedVolume |
0.1 | Базовый объём первой сделки. Ноль включает риск-менеджмент по проценту капитала. |
RiskPercent |
5 | Доля капитала, которую можно потерять при одном входе, если FixedVolume = 0. |
StopLossPoints |
90 | Расстояние до стоп-лосса в шагах цены. Ноль отключает жёсткий стоп. |
TakeProfitPoints |
30 | Расстояние до тейк-профита в шагах цены. Ноль отключает цель. |
TrailingStopPoints |
15 | Базовое расстояние трейлинг-стопа в шагах цены. |
TrailingStepPoints |
5 | Минимальное дополнительное движение цены до следующего подтягивания стопа. |
MaxPositions |
5 | Максимальное число одновременно открытых сделок. |
IntervalPoints |
15 | Минимальное расстояние между соседними входами в шагах цены. |
SecureProfit |
50 | Порог прибыли (в валюте котировки) для срабатывания защиты счёта. |
AccountProtection |
true | Включает защиту счёта. |
PositionsForProtection |
3 | Минимальное число сделок для работы защиты. |
ReverseCondition |
false | Инвертировать направление сигнала MACD. |
MacdFastPeriod |
14 | Быстрая EMA в MACD. |
MacdSlowPeriod |
26 | Медленная EMA в MACD. |
MacdSignalPeriod |
1 | Длина сигнальной EMA MACD (в оригинале равна 1). |
MaxVolume |
5 | Максимальный совокупный объём позиции. |
VolumeMultiplier |
1.6 | Множитель объёма для каждой новой сделки. |
Особенности и ограничения
- В MQL-версии робот мог одновременно держать длинные и короткие сделки (хедж). В StockSharp позиции неттингуются, поэтому перед сменой направления текущая противоположная позиция закрывается.
- MACD анализируется только по закрытым свечам. Внутри бара сигнал может появиться чуть позже, зато стратегия устойчивее при тестировании на истории.
- Все значения в "пунктах" умножаются на шаг цены инструмента
PriceStep. Если провайдер не передаёт эту величину, используется запасной шаг 0.0001 – в этом случае скорректируйте параметры под ваш рынок. - При
FixedVolume = 0для расчёта объёма необходим ненулевой стоп-лосс. Иначе объём становится нулевым, и заявка не отправляется.
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()