The Doubler Hedged Trailing Strategy is a direct StockSharp high-level API conversion of the MetaTrader 5 expert advisor Doubler.mq5. The algorithm instantly opens a symmetrical long and short market position whenever no exposure exists, then manages both legs with independent stop-loss, take-profit, and trailing-stop rules. The conversion preserves the hedging behaviour of the original MQL program while adapting risk management to StockSharp primitives (market orders, Level1 subscriptions, and strategy parameters).
Unlike directional strategies, the system keeps both directions active until each leg exits on its own protective logic. Once both legs are flat the strategy recreates the hedge, continuously maintaining paired exposure.
Key Features
Automatic hedging – opens one buy and one sell order with the same volume whenever the strategy has no active positions.
Pip-based risk controls – stop-loss, take-profit, and trailing offsets are configured in pips and internally converted to price steps by inspecting the security price step and decimal precision (3/5 decimal instruments are automatically scaled by a factor of 10).
Independent trailing per leg – each leg tracks the current best bid/ask. When the price moves more than TrailingStopPips + TrailingStepPips in favour, the stop level is advanced by TrailingStopPips while respecting the trailing step condition, exactly mirroring the original EA logic.
Volume validation – order volume is validated against MinVolume, MaxVolume, and VolumeStep, raising an exception when the requested size violates exchange constraints.
Optional diagnostics – the LogTradeDetails flag enables detailed informational messages (entries, exits, trailing adjustments) that help during testing or live monitoring.
Parameters
Parameter
Description
Default
Notes
OrderVolume
Volume of each hedge leg (buy and sell orders).
1
Must respect exchange volume limits; normalised to the closest VolumeStep.
StopLossPips
Stop-loss distance in pips.
150
0 disables the stop-loss.
TakeProfitPips
Take-profit distance in pips.
300
0 disables the take-profit.
TrailingStopPips
Trailing-stop distance in pips.
5
If greater than zero, TrailingStepPips must also be positive.
TrailingStepPips
Minimal additional move before the trailing stop advances.
5
Guard rail that prevents the stop from moving too frequently.
LogTradeDetails
Enables verbose logging of fills and trailing updates.
false
Set to true for debugging runs.
Trading Logic
Entry
Subscribe to Level1 updates (best bid/ask).
When both _longPosition and _shortPosition are null and no entry orders are pending, register two market orders: one buy and one sell with OrderVolume each.
After fills are confirmed the strategy records entry prices, initial stop/take levels, and resets trailing trackers.
Risk Management
Stop-loss – for each leg the initial stop is placed StopLossPips away from the entry price. A stop distance of 0 disables the protective stop entirely.
Take-profit – symmetric take-profit at TakeProfitPips. A value of 0 disables profit targets.
Forced closure – if NormalizeVolume detects an invalid size (too small/large or not matching VolumeStep) the strategy throws an exception to prevent sending an invalid order.
Trailing Stop Behaviour
When the price moves favourably by at least TrailingStopPips + TrailingStepPips, the stop is advanced to currentPrice ± TrailingStopPips.
The trailing step check reproduces the MQL condition: the stop only moves if the new level is at least TrailingStepPips closer to price than the existing stop, or if no stop exists yet.
For long positions the best bid is used as the reference price; for short positions the best ask is used so exit levels reflect realistic execution prices.
Exit
Each leg exits independently whenever its stop-loss, trailing stop, or take-profit condition is met. Exit orders are registered as market orders, and once a leg is flat its internal state is cleared.
After both legs are closed the next Level1 update triggers a brand new hedged pair.
Data Requirements
Level1 (best bid/ask) – required for entry price snapshots, trailing calculations, and exit triggers.
No candle or trade subscription is necessary; the strategy reacts exclusively to Level1 updates.
Notes on the Conversion
Pip distances are converted to absolute price offsets by multiplying with the security PriceStep. Instruments quoted with 3 or 5 decimals automatically receive a ×10 adjustment, matching the pip definition used in the MetaTrader expert.
The strategy relies on StockSharp’s high-level Strategy methods (RegisterOrder, StartProtection, SubscribeLevel1) and avoids low-level connector operations.
Hedging is implemented through internal PositionState objects so that long and short legs are tracked even when the broker/portfolio uses net positions.
The conversion is self-contained and does not modify or require any test harness from the repository.
using System;
using System.Collections.Generic;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Doubler strategy using double EMA confirmation with trailing stop management.
/// Enters long when both fast and medium EMAs are above slow EMA.
/// Enters short when both fast and medium EMAs are below slow EMA.
/// </summary>
public class DoublerStrategy : Strategy
{
private readonly StrategyParam<int> _fastPeriod;
private readonly StrategyParam<int> _medPeriod;
private readonly StrategyParam<int> _slowPeriod;
private readonly StrategyParam<int> _stopLossPoints;
private readonly StrategyParam<int> _takeProfitPoints;
private ExponentialMovingAverage _fast;
private ExponentialMovingAverage _med;
private ExponentialMovingAverage _slow;
private decimal _entryPrice;
private int _cooldown;
/// <summary>
/// Fast EMA period.
/// </summary>
public int FastPeriod
{
get => _fastPeriod.Value;
set => _fastPeriod.Value = value;
}
/// <summary>
/// Medium EMA period.
/// </summary>
public int MedPeriod
{
get => _medPeriod.Value;
set => _medPeriod.Value = value;
}
/// <summary>
/// Slow EMA period.
/// </summary>
public int SlowPeriod
{
get => _slowPeriod.Value;
set => _slowPeriod.Value = value;
}
/// <summary>
/// Stop-loss distance in price steps.
/// </summary>
public int StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Take-profit distance in price steps.
/// </summary>
public int TakeProfitPoints
{
get => _takeProfitPoints.Value;
set => _takeProfitPoints.Value = value;
}
/// <summary>
/// Initializes strategy parameters.
/// </summary>
public DoublerStrategy()
{
_fastPeriod = Param(nameof(FastPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Fast Period", "Fast EMA period", "Indicator");
_medPeriod = Param(nameof(MedPeriod), 50)
.SetGreaterThanZero()
.SetDisplay("Medium Period", "Medium EMA period", "Indicator");
_slowPeriod = Param(nameof(SlowPeriod), 200)
.SetGreaterThanZero()
.SetDisplay("Slow Period", "Slow EMA period", "Indicator");
_stopLossPoints = Param(nameof(StopLossPoints), 150)
.SetNotNegative()
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk");
_takeProfitPoints = Param(nameof(TakeProfitPoints), 300)
.SetNotNegative()
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_fast = null;
_med = null;
_slow = null;
_entryPrice = 0;
_cooldown = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_fast = new ExponentialMovingAverage { Length = FastPeriod };
_med = new ExponentialMovingAverage { Length = MedPeriod };
_slow = new ExponentialMovingAverage { Length = SlowPeriod };
var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
subscription.Bind(_fast, _med, _slow, ProcessCandle);
subscription.Start();
}
private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal medValue, decimal slowValue)
{
if (candle.State != CandleStates.Finished)
return;
if (!_fast.IsFormed || !_med.IsFormed || !_slow.IsFormed)
return;
if (_cooldown > 0)
{
_cooldown--;
return;
}
var close = candle.ClosePrice;
var step = Security?.PriceStep ?? 1m;
// Check SL/TP
if (Position > 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 100;
return;
}
if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
{
SellMarket();
_entryPrice = 0;
_cooldown = 100;
return;
}
}
else if (Position < 0 && _entryPrice > 0)
{
if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 100;
return;
}
if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
{
BuyMarket();
_entryPrice = 0;
_cooldown = 100;
return;
}
}
// Double confirmation: both fast and med above slow for long
if (fastValue > slowValue && medValue > slowValue && Position <= 0)
{
if (Position < 0)
BuyMarket();
BuyMarket();
_entryPrice = close;
_cooldown = 100;
}
// Double confirmation: both fast and med below slow for short
else if (fastValue < slowValue && medValue < slowValue && Position >= 0)
{
if (Position > 0)
SellMarket();
SellMarket();
_entryPrice = close;
_cooldown = 100;
}
}
}
import clr
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 ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class doubler_strategy(Strategy):
def __init__(self):
super(doubler_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 20) \
.SetDisplay("Fast Period", "Fast EMA period", "Indicator")
self._med_period = self.Param("MedPeriod", 50) \
.SetDisplay("Medium Period", "Medium EMA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 200) \
.SetDisplay("Slow Period", "Slow EMA period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 150) \
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 300) \
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk")
self._fast = None
self._med = None
self._slow = None
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def med_period(self):
return self._med_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(doubler_strategy, self).OnReseted()
self._fast = None
self._med = None
self._slow = None
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(doubler_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._med = ExponentialMovingAverage()
self._med.Length = self.med_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._med, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, med_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
med_val = float(med_value)
slow_val = float(slow_value)
if not self._fast.IsFormed or not self._med.IsFormed or not self._slow.IsFormed:
return
if self._cooldown > 0:
self._cooldown -= 1
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
# Check SL/TP
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 100
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 100
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 100
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 100
return
# Double confirmation: both fast and med above slow for long
if fast_val > slow_val and med_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 100
# Double confirmation: both fast and med below slow for short
elif fast_val < slow_val and med_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 100
def CreateClone(self):
return doubler_strategy()