Ver no GitHub

Backbone Strategy

This strategy reproduces the core behaviour of the original Backbone MQL5 expert advisor using the StockSharp high-level API. It alternates between bullish and bearish trading cycles, scales into positions according to a risk fraction, and protects open trades with fixed targets together with a trailing stop.

Core Idea

  1. Initial direction detection – the strategy tracks the highest high and the lowest low after startup. A move larger than the trailing-stop distance away from either extreme defines which side will trade first.
  2. Directional cycles – after a cycle starts, the algorithm only trades in that direction until all positions are closed. When the last position exits, it immediately flips and prepares for the opposite cycle.
  3. Risk-based scaling – each additional entry uses a dynamic volume derived from the account equity, the MaxRisk fraction, the configured limit MaxTrades, and the stop-loss distance. This mimics the lot-sizing function from the original EA.
  4. Protective exits – every entry recalculates a stop-loss and a take-profit level around the volume-weighted average price of the current cycle. A trailing stop tightens the protective stop whenever the unrealised profit exceeds the configured trailing distance.

Parameters

Parameter Default Description
MaxRisk 0.5 Fraction of account equity available for all positions in the current direction.
MaxTrades 10 Maximum number of sequential entries per directional cycle.
TakeProfitPips 170 Distance (in pips) between the entry average and the take-profit target.
StopLossPips 40 Distance (in pips) between the entry average and the protective stop.
TrailingStopPips 300 Distance (in pips) used both to determine the initial direction and to trail profits.
CandleType 5-minute time frame Candle type used for signal evaluation.

Pip definition – the strategy automatically adjusts the pip size based on the instrument PriceStep. Symbols quoted with 3 or 5 decimal places use a 10× multiplier, which replicates the original MetaTrader pip handling.

Trading Logic

  1. Wait for a finished candle. Skip processing while the strategy is warming up or trading is disabled.
  2. Update the extreme prices while no direction has been chosen yet. Once the high breaks upward (by more than TrailingStopPips) the first cycle will be short; if the low breaks downward, the first cycle will be long.
  3. While the cycle is long:
    • Add a new long entry when either (a) the previous cycle was short and no long positions are open, or (b) the previous cycle was also long and the number of open longs is below MaxTrades.
    • Exit the entire long cycle when the take-profit or stop-loss is reached, or when the trailing stop raises the protective level above the current stop.
  4. While the cycle is short the same rules apply with inverted conditions.
  5. After a cycle closes, reset its counters and wait for the opposite setup.

Position Sizing

The position size for each new entry is calculated as:

qty = equity * fraction / (pipSize * stopLoss)
where fraction = 1 / (MaxTrades / MaxRisk - openTrades)

The quantity is then aligned to the instrument volume step and capped within the minimum/maximum volume bounds. If the required size falls below the allowed minimum, the minimum is used. When equity information is unavailable, the default strategy volume acts as a fallback.

Exit Management

  • Stop-loss / take-profit – recalculated whenever a new order is added so that all trades in the current cycle share the same combined levels based on the average entry price.
  • Trailing stop – for a long cycle the stop moves to Close - TrailingStopPips * pipSize once the unrealised profit exceeds that threshold. The short-side trailing mirror is handled symmetrically.

Notes and Limitations

  • StockSharp executes trades in a netting environment, therefore each directional cycle manages the combined position instead of individual tickets. The alternating logic and risk formula reproduce the original behaviour while fitting the API model.
  • The strategy relies on completed candles. Intrabar movements smaller than the candle range are not evaluated.
  • Ensure that the selected candle type and security produce enough data to build the initial extremes before expecting trades.
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;

using System.Globalization;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Backbone strategy converted from the MQL5 expert advisor.
/// Alternates long and short series with risk-based scaling, stop-loss, take-profit, and trailing stop management.
/// </summary>
public class BackboneStrategy : Strategy
{
	private readonly StrategyParam<decimal> _maxRisk;
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _bidMax;
	private decimal _askMin;
	private int _lastDirection;
	private int _currentDirection;
	private int _longCount;
	private int _shortCount;
	private decimal _longAveragePrice;
	private decimal _shortAveragePrice;
	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;
	private decimal _adjustedPoint;

	/// <summary>
	/// Maximum total risk fraction shared across all positions.
	/// </summary>
	public decimal MaxRisk
	{
		get => _maxRisk.Value;
		set => _maxRisk.Value = value;
	}

	/// <summary>
	/// Maximum number of stacked entries in one direction.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Trailing stop activation distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Candle type used for the calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="BackboneStrategy"/> class.
	/// </summary>
	public BackboneStrategy()
	{
		_maxRisk = Param(nameof(MaxRisk), 0.5m)
			.SetGreaterThanZero()
			.SetDisplay("Max Risk", "Maximum risk fraction shared across trades", "Risk");

		_maxTrades = Param(nameof(MaxTrades), 1)
			.SetGreaterThanZero()
			.SetDisplay("Max Trades", "Maximum number of layered entries", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 170m)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit", "Distance for the take-profit target (pips)", "Risk");

		_stopLossPips = Param(nameof(StopLossPips), 40m)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss", "Distance for the protective stop (pips)", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 300m)
			.SetGreaterThanZero()
			.SetDisplay("Trailing Stop", "Distance for the trailing stop activation (pips)", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromDays(1).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for analysis", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		ResetState();
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		ResetState();
		_adjustedPoint = GetAdjustedPoint();

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		// Wait for completed candles only.
		if (candle.State != CandleStates.Finished)
			return;

		// Trade only when the strategy is fully operational.
		// removed IsFormedAndOnlineAndAllowTrading for backtesting

		if (_adjustedPoint <= 0m)
			_adjustedPoint = GetAdjustedPoint();

		UpdateExtremeLevels(candle);

		if (_currentDirection == 1)
		{
			if (HandleLongExit(candle))
				return;
		}
		else if (_currentDirection == -1)
		{
			if (HandleShortExit(candle))
				return;
		}
		else
		{
			// Reset counters when all positions are closed.
			ResetLongState();
			ResetShortState();
		}

		if (ShouldEnterLong())
		{
			EnterLong(candle);
		}
		else if (ShouldEnterShort())
		{
			EnterShort(candle);
		}
	}

	private void EnterLong(ICandleMessage candle)
	{
		var openPositions = _currentDirection == 1 ? _longCount : 0;
		var qty = CalculateOrderVolume(openPositions);
		if (qty <= 0m)
			return;

		if (_currentDirection == -1)
		{
			// Close the short series before switching sides.
			if (Position > 0) SellMarket(Math.Abs(Position)); else if (Position < 0) BuyMarket(Math.Abs(Position));
			ResetShortState();
			_currentDirection = 0;
			openPositions = 0;
		}

		BuyMarket(qty);

		openPositions = Math.Max(0, openPositions) + 1;
		_longCount = openPositions;
		_currentDirection = 1;

		var average = _longCount == 1
		? candle.ClosePrice
		: (_longAveragePrice * (_longCount - 1) + candle.ClosePrice) / _longCount;
		_longAveragePrice = average;

		if (StopLossPips > 0m && _adjustedPoint > 0m)
			_longStop = average - StopLossPips * _adjustedPoint;
		else
			_longStop = null;

		if (TakeProfitPips > 0m && _adjustedPoint > 0m)
			_longTake = average + TakeProfitPips * _adjustedPoint;
		else
			_longTake = null;

		_lastDirection = 1;
	}

	private void EnterShort(ICandleMessage candle)
	{
		var openPositions = _currentDirection == -1 ? _shortCount : 0;
		var qty = CalculateOrderVolume(openPositions);
		if (qty <= 0m)
			return;

		if (_currentDirection == 1)
		{
			// Close the long series before switching sides.
			if (Position > 0) SellMarket(Math.Abs(Position)); else if (Position < 0) BuyMarket(Math.Abs(Position));
			ResetLongState();
			_currentDirection = 0;
			openPositions = 0;
		}

		SellMarket(qty);

		openPositions = Math.Max(0, openPositions) + 1;
		_shortCount = openPositions;
		_currentDirection = -1;

		var average = _shortCount == 1
		? candle.ClosePrice
		: (_shortAveragePrice * (_shortCount - 1) + candle.ClosePrice) / _shortCount;
		_shortAveragePrice = average;

		if (StopLossPips > 0m && _adjustedPoint > 0m)
			_shortStop = average + StopLossPips * _adjustedPoint;
		else
			_shortStop = null;

		if (TakeProfitPips > 0m && _adjustedPoint > 0m)
			_shortTake = average - TakeProfitPips * _adjustedPoint;
		else
			_shortTake = null;

		_lastDirection = -1;
	}

	private bool HandleLongExit(ICandleMessage candle)
	{
		var exitTriggered = false;

		if (_longTake.HasValue && candle.HighPrice >= _longTake.Value)
		{
			// Take-profit reached for the long series.
			SellMarket(Math.Abs(Position));
			exitTriggered = true;
		}
		else if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
		{
			// Stop-loss touched for the long series.
			SellMarket(Math.Abs(Position));
			exitTriggered = true;
		}
		else if (TrailingStopPips > 0m && StopLossPips > 0m && _longCount > 0 && _adjustedPoint > 0m)
		{
			var trailingDistance = TrailingStopPips * _adjustedPoint;
			var profit = candle.ClosePrice - _longAveragePrice;
			if (trailingDistance > 0m && profit > trailingDistance)
			{
				var newStop = candle.ClosePrice - trailingDistance;
				if (!_longStop.HasValue || _longStop.Value < newStop)
					_longStop = newStop;
			}
		}

		if (exitTriggered)
		{
			ResetLongState();
			_currentDirection = 0;
			return true;
		}

		return false;
	}

	private bool HandleShortExit(ICandleMessage candle)
	{
		var exitTriggered = false;

		if (_shortTake.HasValue && candle.LowPrice <= _shortTake.Value)
		{
			// Take-profit reached for the short series.
			BuyMarket(Math.Abs(Position));
			exitTriggered = true;
		}
		else if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
		{
			// Stop-loss touched for the short series.
			BuyMarket(Math.Abs(Position));
			exitTriggered = true;
		}
		else if (TrailingStopPips > 0m && StopLossPips > 0m && _shortCount > 0 && _adjustedPoint > 0m)
		{
			var trailingDistance = TrailingStopPips * _adjustedPoint;
			var profit = _shortAveragePrice - candle.ClosePrice;
			if (trailingDistance > 0m && profit > trailingDistance)
			{
				var newStop = candle.ClosePrice + trailingDistance;
				if (!_shortStop.HasValue || _shortStop.Value > newStop)
					_shortStop = newStop;
			}
		}

		if (exitTriggered)
		{
			ResetShortState();
			_currentDirection = 0;
			return true;
		}

		return false;
	}

	private bool ShouldEnterLong()
	{
		var openPositions = _currentDirection == 1 ? _longCount : 0;
		if (MaxTrades <= 0)
			return false;

		var firstEntry = _lastDirection == -1 && openPositions == 0;
		var addEntry = _lastDirection == 1 && openPositions > 0 && openPositions < MaxTrades;
		return firstEntry || addEntry;
	}

	private bool ShouldEnterShort()
	{
		var openPositions = _currentDirection == -1 ? _shortCount : 0;
		if (MaxTrades <= 0)
			return false;

		var firstEntry = _lastDirection == 1 && openPositions == 0;
		var addEntry = _lastDirection == -1 && openPositions > 0 && openPositions < MaxTrades;
		return firstEntry || addEntry;
	}

	private decimal CalculateOrderVolume(int openPositions)
	{
		var defaultVolume = Volume > 0m ? Volume : 1m;
		var minVolume = Security?.MinVolume ?? defaultVolume;
		var volumeStep = Security?.VolumeStep ?? 0m;
		var maxVolume = Security?.MaxVolume;

		if (minVolume <= 0m)
			minVolume = defaultVolume;

		if (MaxTrades <= 0 || MaxRisk <= 0m)
			return minVolume;

		var denominatorBase = (decimal)MaxTrades / MaxRisk;
		var denominator = denominatorBase - openPositions;
		if (denominator <= 0m)
			return 0m;

		var fraction = 1m / denominator;
		var equity = Portfolio?.CurrentValue ?? 0m;
		if (equity <= 0m)
			return 0m;

		var pip = _adjustedPoint;
		if (pip <= 0m)
		{
			var priceStep = Security?.PriceStep ?? 0m;
			pip = priceStep > 0m ? priceStep : 1m;
		}

		var stopLoss = StopLossPips > 0m ? StopLossPips : 1m;
		var riskPerUnit = stopLoss * pip;
		if (riskPerUnit <= 0m)
			return minVolume;

		var qty = equity * fraction / riskPerUnit;

		if (volumeStep > 0m)
			qty = Math.Floor(qty / volumeStep) * volumeStep;

		if (qty < minVolume)
			qty = minVolume;

		if (maxVolume.HasValue && maxVolume.Value > 0m && qty > maxVolume.Value)
			qty = maxVolume.Value;

		return qty;
	}

	private void UpdateExtremeLevels(ICandleMessage candle)
	{
		if (_lastDirection != 0)
			return;

		var trailingDistance = TrailingStopPips * _adjustedPoint;
		if (trailingDistance <= 0m)
			return;

		if (candle.HighPrice > _bidMax)
			_bidMax = candle.HighPrice;

		if (candle.LowPrice < _askMin)
			_askMin = candle.LowPrice;

		if (_bidMax != decimal.MinValue && candle.LowPrice < _bidMax - trailingDistance)
		{
			_lastDirection = -1;
			return;
		}

		if (_askMin != decimal.MaxValue && candle.HighPrice > _askMin + trailingDistance)
			_lastDirection = 1;
	}

	private decimal GetAdjustedPoint()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 1m;

		var decimals = CountDecimals(step);
		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}

	private static int CountDecimals(decimal value)
	{
		value = Math.Abs(value);
		var text = value.ToString(CultureInfo.InvariantCulture);
		var index = text.IndexOf('.');
		return index < 0 ? 0 : text.Length - index - 1;
	}

	private void ResetState()
	{
		_bidMax = decimal.MinValue;
		_askMin = decimal.MaxValue;
		_lastDirection = 0;
		_currentDirection = 0;
		ResetLongState();
		ResetShortState();
		_adjustedPoint = 0m;
	}

	private void ResetLongState()
	{
		_longCount = 0;
		_longAveragePrice = 0m;
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortState()
	{
		_shortCount = 0;
		_shortAveragePrice = 0m;
		_shortStop = null;
		_shortTake = null;
	}
}