Straddle Trail Strategy
Overview
The Straddle Trail Strategy replicates the behaviour of the original MetaTrader 5 "Straddle&Trail" expert advisor. The strategy places a pair of stop orders (a straddle) around the current price ahead of scheduled news events or immediately on demand. Once a position is triggered the algorithm manages break-even transitions, trailing stops and optional shutdown commands that cancel pending orders or close open positions.
This implementation is built on top of the StockSharp high level API. Order placement, position management and risk controls are implemented without using low-level message processing.
Trading Logic
Straddle placement
- Two stop orders (buy stop above and sell stop below) are created once the scheduled event window is reached or instantly if
PlaceStraddleImmediately is enabled.
- Order prices are offset from the current bid/ask by
DistanceFromPrice (expressed in pips). The offset is converted into price units using the instrument price step.
- The strategy prevents re-creating the straddle multiple times on the same day unless the orders are adjusted or explicitly cancelled.
Pre-event order management
- When
AdjustPendingOrders is enabled the stop orders are cancelled and re-placed every new minute so they stay aligned with the current price.
- Adjustments stop
StopAdjustMinutes before the event to avoid chasing the price when volatility rises.
- If
RemoveOppositeOrder is enabled the remaining stop order is automatically cancelled once one side of the straddle triggers and opens a position.
Risk management
- Initial stop-loss and take-profit levels are calculated from
StopLossPips and TakeProfitPips and are tracked internally.
- When the open profit reaches
BreakevenTriggerPips the stop level is moved to the entry price plus BreakevenLockPips (or the symmetric value for short trades).
- If
TrailPips is greater than zero a trailing stop follows the price. Trailing can start immediately or only after the break-even condition depending on TrailAfterBreakeven.
- Profit taking and stop exits are executed with market orders for reliability.
Manual shutdown
- Setting
ShutdownNow to true triggers an immediate cleanup according to the ShutdownMode option. Possible actions include closing long/short positions and cancelling pending long/short orders.
Parameters
| Parameter |
Description |
ShutdownNow |
Triggers the shutdown procedure on the next candle update. Automatically resets to false after execution. |
ShutdownMode |
Defines what should be cancelled or closed (All, LongPositions, ShortPositions, PendingLong, PendingShort). |
DistanceFromPrice |
Distance between the current price and each stop order, measured in pips. |
StopLossPips |
Initial stop-loss distance for triggered positions. Set to 0 to disable. |
TakeProfitPips |
Initial take-profit distance. Set to 0 to disable. |
TrailPips |
Trailing stop distance. Set to 0 to disable trailing. |
TrailAfterBreakeven |
When true, trailing starts only after the break-even condition is satisfied. |
BreakevenLockPips |
Profit locked when the break-even trigger activates. |
BreakevenTriggerPips |
Profit threshold that activates the break-even logic. |
EventHour / EventMinute |
Scheduled event time (broker/server time). Set both to 0 to disable the event scheduler. |
PreEventEntryMinutes |
Minutes before the event when the straddle should be placed. Ignored when the event is disabled or when immediate placement is enabled. |
StopAdjustMinutes |
Number of minutes before the event when auto-adjustment of pending orders stops. |
RemoveOppositeOrder |
Cancels the unfilled stop order when the first leg of the straddle triggers. |
AdjustPendingOrders |
Enables automatic re-centring of pending orders while waiting for the event. |
PlaceStraddleImmediately |
Places the straddle right after the strategy starts, bypassing the event schedule. |
CandleType |
Candle subscription used for time tracking. Defaults to 1-minute candles. |
Volume – the StockSharp Volume property controls order size. It is set to 1 by default and can be modified before starting the strategy.
Data Subscriptions
The strategy subscribes to:
- The configured candle series (default 1-minute) to run the scheduler, trailing logic and shutdown checks.
- The order book to keep track of the latest bid/ask prices for precise stop order alignment.
Notes and Limitations
- Stop-loss and take-profit management is executed via market orders rather than by modifying broker-side protective orders. This mirrors the original behaviour while keeping the implementation simple.
- The strategy uses the instrument
PriceStep to approximate pip size. For exotic instruments adjust parameters accordingly.
- The shutdown command is evaluated only when new candle data arrive. For immediate action reduce the candle timeframe.
- Python implementation is intentionally omitted as requested.
Conversion Notes
- The break-even and trailing logic is ported line-by-line from the MQL version. The StockSharp version maintains the same numeric relationships but operates on decimal prices and uses market exits.
- Manual trade handling (magic number
0 in MQL) is not reproduced because StockSharp strategies manage their own positions. All protective logic applies to strategy-generated trades only.
- The
CalcMagic function is unnecessary in StockSharp and was therefore removed. Strategy state is tracked internally by the framework.
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>
/// Strategy that simulates a straddle approach: defines upper/lower breakout levels
/// from a consolidation range (ATR-based) and enters on breakouts with trailing stop.
/// </summary>
public class StraddleTrailStrategy : Strategy
{
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<decimal> _stopLossMult;
private readonly StrategyParam<decimal> _takeProfitMult;
private readonly StrategyParam<decimal> _trailMult;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private decimal _entryPrice;
private decimal? _stopLevel;
private decimal? _takeLevel;
private int _barsSinceEntry;
private int _cooldownCounter;
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
public decimal StopLossMult { get => _stopLossMult.Value; set => _stopLossMult.Value = value; }
public decimal TakeProfitMult { get => _takeProfitMult.Value; set => _takeProfitMult.Value = value; }
public decimal TrailMult { get => _trailMult.Value; set => _trailMult.Value = value; }
public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public StraddleTrailStrategy()
{
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "ATR calculation length", "ATR");
_atrMultiplier = Param(nameof(AtrMultiplier), 2.5m)
.SetGreaterThanZero()
.SetDisplay("ATR Multiplier", "Breakout distance multiplier", "ATR");
_stopLossMult = Param(nameof(StopLossMult), 1.0m)
.SetGreaterThanZero()
.SetDisplay("SL Multiplier", "Stop loss as ATR multiple", "Risk");
_takeProfitMult = Param(nameof(TakeProfitMult), 3.0m)
.SetGreaterThanZero()
.SetDisplay("TP Multiplier", "Take profit as ATR multiple", "Risk");
_trailMult = Param(nameof(TrailMult), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Trail Multiplier", "Trailing distance as ATR multiple", "Risk");
_cooldownBars = Param(nameof(CooldownBars), 6)
.SetGreaterThanZero()
.SetDisplay("Cooldown", "Bars to wait after exit", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle subscription", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0;
_stopLevel = null;
_takeLevel = null;
_barsSinceEntry = 0;
_cooldownCounter = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var atr = new AverageTrueRange { Length = AtrPeriod };
var sma = new SimpleMovingAverage { Length = 20 };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, sma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atr, decimal sma)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
// Manage existing position
if (Position != 0)
{
_barsSinceEntry++;
if (Position > 0)
{
// Trail stop up
var newTrail = close - TrailMult * atr;
if (_stopLevel == null || newTrail > _stopLevel)
_stopLevel = newTrail;
// Check stop or take
if (close <= _stopLevel || (_takeLevel != null && close >= _takeLevel))
{
SellMarket(Math.Abs(Position));
ResetPosition();
return;
}
}
else
{
// Trail stop down
var newTrail = close + TrailMult * atr;
if (_stopLevel == null || newTrail < _stopLevel)
_stopLevel = newTrail;
// Check stop or take
if (close >= _stopLevel || (_takeLevel != null && close <= _takeLevel))
{
BuyMarket(Math.Abs(Position));
ResetPosition();
return;
}
}
return;
}
// Cooldown after exit
if (_cooldownCounter > 0)
{
_cooldownCounter--;
return;
}
// Entry: breakout above/below SMA + ATR distance
var upperLevel = sma + AtrMultiplier * atr;
var lowerLevel = sma - AtrMultiplier * atr;
if (close > upperLevel)
{
BuyMarket();
_entryPrice = close;
_stopLevel = close - StopLossMult * atr;
_takeLevel = close + TakeProfitMult * atr;
_barsSinceEntry = 0;
}
else if (close < lowerLevel)
{
SellMarket();
_entryPrice = close;
_stopLevel = close + StopLossMult * atr;
_takeLevel = close - TakeProfitMult * atr;
_barsSinceEntry = 0;
}
}
private void ResetPosition()
{
_entryPrice = 0;
_stopLevel = null;
_takeLevel = null;
_barsSinceEntry = 0;
_cooldownCounter = CooldownBars;
}
}
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.Strategies import Strategy
from StockSharp.Algo.Indicators import AverageTrueRange, SimpleMovingAverage
class straddle_trail_strategy(Strategy):
def __init__(self):
super(straddle_trail_strategy, self).__init__()
self._atr_period = self.Param("AtrPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "ATR calculation length", "ATR")
self._atr_multiplier = self.Param("AtrMultiplier", 2.5) \
.SetGreaterThanZero() \
.SetDisplay("ATR Multiplier", "Breakout distance multiplier", "ATR")
self._stop_loss_mult = self.Param("StopLossMult", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("SL Multiplier", "Stop loss as ATR multiple", "Risk")
self._take_profit_mult = self.Param("TakeProfitMult", 3.0) \
.SetGreaterThanZero() \
.SetDisplay("TP Multiplier", "Take profit as ATR multiple", "Risk")
self._trail_mult = self.Param("TrailMult", 1.5) \
.SetGreaterThanZero() \
.SetDisplay("Trail Multiplier", "Trailing distance as ATR multiple", "Risk")
self._cooldown_bars = self.Param("CooldownBars", 6) \
.SetGreaterThanZero() \
.SetDisplay("Cooldown", "Bars to wait after exit", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Candle subscription", "General")
self._entry_price = 0.0
self._stop_level = None
self._take_level = None
self._bars_since_entry = 0
self._cooldown_counter = 0
@property
def AtrPeriod(self):
return self._atr_period.Value
@AtrPeriod.setter
def AtrPeriod(self, value):
self._atr_period.Value = value
@property
def AtrMultiplier(self):
return self._atr_multiplier.Value
@AtrMultiplier.setter
def AtrMultiplier(self, value):
self._atr_multiplier.Value = value
@property
def StopLossMult(self):
return self._stop_loss_mult.Value
@StopLossMult.setter
def StopLossMult(self, value):
self._stop_loss_mult.Value = value
@property
def TakeProfitMult(self):
return self._take_profit_mult.Value
@TakeProfitMult.setter
def TakeProfitMult(self, value):
self._take_profit_mult.Value = value
@property
def TrailMult(self):
return self._trail_mult.Value
@TrailMult.setter
def TrailMult(self, value):
self._trail_mult.Value = value
@property
def CooldownBars(self):
return self._cooldown_bars.Value
@CooldownBars.setter
def CooldownBars(self, value):
self._cooldown_bars.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(straddle_trail_strategy, self).OnStarted2(time)
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
sma = SimpleMovingAverage()
sma.Length = 20
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.Bind(atr, sma, self.process_candle) \
.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def process_candle(self, candle, atr_val, sma_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
atr = float(atr_val)
sma = float(sma_val)
if self.Position != 0:
self._bars_since_entry += 1
if self.Position > 0:
new_trail = close - float(self.TrailMult) * atr
if self._stop_level is None or new_trail > self._stop_level:
self._stop_level = new_trail
if close <= self._stop_level or (self._take_level is not None and close >= self._take_level):
self.SellMarket(abs(self.Position))
self._reset_position()
return
else:
new_trail = close + float(self.TrailMult) * atr
if self._stop_level is None or new_trail < self._stop_level:
self._stop_level = new_trail
if close >= self._stop_level or (self._take_level is not None and close <= self._take_level):
self.BuyMarket(abs(self.Position))
self._reset_position()
return
return
if self._cooldown_counter > 0:
self._cooldown_counter -= 1
return
upper_level = sma + float(self.AtrMultiplier) * atr
lower_level = sma - float(self.AtrMultiplier) * atr
if close > upper_level:
self.BuyMarket()
self._entry_price = close
self._stop_level = close - float(self.StopLossMult) * atr
self._take_level = close + float(self.TakeProfitMult) * atr
self._bars_since_entry = 0
elif close < lower_level:
self.SellMarket()
self._entry_price = close
self._stop_level = close + float(self.StopLossMult) * atr
self._take_level = close - float(self.TakeProfitMult) * atr
self._bars_since_entry = 0
def _reset_position(self):
self._entry_price = 0.0
self._stop_level = None
self._take_level = None
self._bars_since_entry = 0
self._cooldown_counter = self.CooldownBars
def OnReseted(self):
super(straddle_trail_strategy, self).OnReseted()
self._entry_price = 0.0
self._stop_level = None
self._take_level = None
self._bars_since_entry = 0
self._cooldown_counter = 0
def CreateClone(self):
return straddle_trail_strategy()