GitHub で見る

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

  1. 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.
  2. 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.
  3. 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.
  4. 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;
	}
}