View on GitHub

Arrows and Curves Strategy

Overview

This strategy is a C# port of the MetaTrader 5 "Arrows and Curves" expert advisor (EA). It replicates the original indicator-based logic inside StockSharp using the high-level API. The system trades a single symbol and reacts to the custom channel signals generated by the Arrows and Curves indicator. Only one position can be active at a time, and every new signal either opens a fresh trade or closes an existing one.

Strategy Logic

  • Candles from the configurable timeframe are streamed via SubscribeCandles. The processing routine only works with finished candles to mirror the EA behaviour on new bar openings.
  • The Arrows and Curves channel is rebuilt inside the strategy: the algorithm scans the highest high and lowest low for the SSP lookback window, shifted by the Relay offset just like the MT5 indicator. From those values two envelopes are derived (Channel % for the outer band and Channel Stop % for the inner band).
  • The indicator state variables (uptrend and uptrend2) are updated exactly in the same order as in the original MQL code. Whenever the previous candle produces a Sell arrow the strategy prepares a long entry, and whenever it produces a Buy arrow the strategy prepares a short entry. This mirrors the EA behaviour where the signals are read with an index of 1 on the next bar.
  • When no position is open, the stored signal from the previous bar is used to open a market order in the opposite direction of the arrow (Sell arrow → Buy trade, Buy arrow → Sell trade).
  • When a position already exists and an opposite signal appears, the current position is closed but a reverse position is not opened immediately, matching the MT5 source where closing happens first and entries are evaluated again on the next bar.

Risk Management

  • Stop loss and take profit distances are defined in pips and converted to absolute price offsets using the instrument PriceStep. For instruments quoted with 3 or 5 decimal places the conversion multiplies the step by ten, reproducing the pip adjustments of the EA.
  • Trailing stop functionality mirrors the EA: once the floating profit exceeds Trailing Stop + Trailing Step, the protective stop is trailed by the configured distance while respecting the minimum step.
  • Protective levels are checked on every completed candle by using the candle high/low to approximate intrabar triggers.
  • Position sizing can be fixed via the Volume parameter. When Volume is set to zero, the strategy derives a dynamic quantity by risking Risk % of the portfolio value against the configured stop-loss distance.

Parameters

  • Volume: fixed order size. Set to zero to enable risk-based sizing.
  • Risk %: percentage of portfolio value to risk when volume is zero.
  • Stop Loss (pips): distance of the protective stop in pips.
  • Take Profit (pips): distance of the profit target in pips.
  • Trailing Stop (pips): trailing stop distance in pips; set to zero to disable.
  • Trailing Step (pips): minimum additional move required before the trailing stop is shifted again.
  • SSP: number of candles used to calculate the channel range.
  • Channel %: outer envelope percentage, identical to the MT5 setting.
  • Channel Stop %: inner envelope percentage used to flip the secondary state.
  • Relay: shift applied to the channel calculation.
  • Candle Type: timeframe or candle type that feeds the indicator.

Implementation Notes

  • The strategy stores only the minimal amount of historical highs, lows and closes required by the indicator (SSP + Relay + 5 bars).
  • All comments and helper methods are written in English to match repository guidelines.
  • Unlike MT5, stop-loss and take-profit orders are simulated on candle data, so intrabar fills may differ from the original EA. Everything else follows the same decision rules, making the port faithful to the source script.
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>
/// Port of the MT5 Arrows and Curves expert advisor using StockSharp high level API.
/// </summary>
public class ArrowsAndCurvesStrategy : Strategy
{
	private readonly StrategyParam<decimal> _volume;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _trailingStopPips;
	private readonly StrategyParam<int> _trailingStepPips;
	private readonly StrategyParam<int> _sspPeriod;
	private readonly StrategyParam<int> _channelPercent;
	private readonly StrategyParam<int> _channelStopPercent;
	private readonly StrategyParam<int> _relayShift;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _highSeries = new();
	private readonly List<decimal> _lowSeries = new();
	private readonly List<decimal> _closeSeries = new();

	private bool _uptrend;
	private bool _uptrend2;
	private bool _previousSellArrow;
	private bool _previousBuyArrow;

	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	public decimal VolumeValue { get => _volume.Value; set => _volume.Value = value; }
	public decimal RiskPercent { get => _riskPercent.Value; set => _riskPercent.Value = value; }
	public int StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
	public int TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
	public int TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
	public int TrailingStepPips { get => _trailingStepPips.Value; set => _trailingStepPips.Value = value; }
	public int SspPeriod { get => _sspPeriod.Value; set => _sspPeriod.Value = value; }
	public int ChannelPercent { get => _channelPercent.Value; set => _channelPercent.Value = value; }
	public int ChannelStopPercent { get => _channelStopPercent.Value; set => _channelStopPercent.Value = value; }
	public int RelayShift { get => _relayShift.Value; set => _relayShift.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public ArrowsAndCurvesStrategy()
	{
		_volume = Param(nameof(VolumeValue), 1m)
		.SetNotNegative()
		.SetDisplay("Volume", "Order volume", "Trading");

		_riskPercent = Param(nameof(RiskPercent), 5m)
		.SetNotNegative()
		.SetDisplay("Risk %", "Risk percent for dynamic sizing when volume is zero", "Trading");

		_stopLossPips = Param(nameof(StopLossPips), 50)
		.SetNotNegative()
		.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
		.SetNotNegative()
		.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 0)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (pips)", "Trailing stop distance", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5)
		.SetNotNegative()
		.SetDisplay("Trailing Step (pips)", "Minimum movement before trailing updates", "Risk");

		_sspPeriod = Param(nameof(SspPeriod), 20)
		.SetGreaterThanZero()
		.SetDisplay("SSP", "Lookback period of the custom channel", "Indicator");

		_channelPercent = Param(nameof(ChannelPercent), 0)
		.SetNotNegative()
		.SetDisplay("Channel %", "Outer channel percentage", "Indicator");

		_channelStopPercent = Param(nameof(ChannelStopPercent), 30)
		.SetNotNegative()
		.SetDisplay("Channel Stop %", "Inner channel percentage", "Indicator");

		_relayShift = Param(nameof(RelayShift), 10)
		.SetNotNegative()
		.SetDisplay("Relay", "Shift used by the indicator", "Indicator");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Candle Type", "Candles used for processing", "General");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();
		_highSeries.Clear();
		_lowSeries.Clear();
		_closeSeries.Clear();
		_uptrend = false;
		_uptrend2 = false;
		_previousSellArrow = false;
		_previousBuyArrow = false;
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

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

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
		return;

		AddCandle(candle);

		var shouldOpenBuy = _previousSellArrow;
		var shouldOpenSell = _previousBuyArrow;

		if (Position == 0)
		{
			if (shouldOpenBuy)
			OpenLong(candle);
			else if (shouldOpenSell)
			OpenShort(candle);
		}
		else
		{
			if (Position > 0 && shouldOpenSell)
			{
				CloseAndReset();
			}
			else if (Position < 0 && shouldOpenBuy)
			{
				CloseAndReset();
			}

			UpdateTrailing(candle);
			CheckRiskExits(candle);
		}

		if (!TryComputeSignals(out var buySignal, out var sellSignal))
		{
			_previousBuyArrow = false;
			_previousSellArrow = false;
			return;
		}

		_previousBuyArrow = buySignal;
		_previousSellArrow = sellSignal;
	}

	private void OpenLong(ICandleMessage candle)
	{
		var volume = GetOrderVolume(candle.ClosePrice);
		if (volume <= 0m)
		return;

		BuyMarket(volume);

		_entryPrice = candle.ClosePrice;
		_stopPrice = StopLossPips > 0 ? candle.ClosePrice - ConvertPips(StopLossPips) : null;
		_takePrice = TakeProfitPips > 0 ? candle.ClosePrice + ConvertPips(TakeProfitPips) : null;
	}

	private void OpenShort(ICandleMessage candle)
	{
		var volume = GetOrderVolume(candle.ClosePrice);
		if (volume <= 0m)
		return;

		SellMarket(volume);

		_entryPrice = candle.ClosePrice;
		_stopPrice = StopLossPips > 0 ? candle.ClosePrice + ConvertPips(StopLossPips) : null;
		_takePrice = TakeProfitPips > 0 ? candle.ClosePrice - ConvertPips(TakeProfitPips) : null;
	}

	private void UpdateTrailing(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0 || _entryPrice == null)
		return;

		var distance = ConvertPips(TrailingStopPips);
		if (distance <= 0m)
		return;

		var step = ConvertPips(TrailingStepPips);

		if (Position > 0)
		{
			var gain = candle.ClosePrice - _entryPrice.Value;
			if (gain > distance + step)
			{
				var newStop = candle.ClosePrice - distance;
				if (!_stopPrice.HasValue || _stopPrice.Value < newStop - step)
				_stopPrice = newStop;
			}
		}
		else if (Position < 0)
		{
			var gain = _entryPrice.Value - candle.ClosePrice;
			if (gain > distance + step)
			{
				var newStop = candle.ClosePrice + distance;
				if (!_stopPrice.HasValue || _stopPrice.Value > newStop + step)
				_stopPrice = newStop;
			}
		}
	}

	private void CheckRiskExits(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var stopHit = _stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value;
			var takeHit = _takePrice.HasValue && candle.HighPrice >= _takePrice.Value;

			if (stopHit || takeHit)
			CloseAndReset();
		}
		else if (Position < 0)
		{
			var stopHit = _stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value;
			var takeHit = _takePrice.HasValue && candle.LowPrice <= _takePrice.Value;

			if (stopHit || takeHit)
			CloseAndReset();
		}
	}

	private void CloseAndReset()
	{
		if (Position > 0)
			SellMarket(Position);
		else if (Position < 0)
			BuyMarket(-Position);
		ResetPositionState();
	}

	private void ResetPositionState()
	{
		_entryPrice = null;
		_stopPrice = null;
		_takePrice = null;
	}

	private void AddCandle(ICandleMessage candle)
	{
		_highSeries.Add(candle.HighPrice);
		_lowSeries.Add(candle.LowPrice);
		_closeSeries.Add(candle.ClosePrice);

		var maxCount = RelayShift + SspPeriod + 5;
		TrimSeries(_highSeries, maxCount);
		TrimSeries(_lowSeries, maxCount);
		TrimSeries(_closeSeries, maxCount);
	}

	private static void TrimSeries(List<decimal> series, int maxCount)
	{
		var excess = series.Count - maxCount;
		if (excess > 0)
		series.RemoveRange(0, excess);
	}

	private bool TryComputeSignals(out bool buySignal, out bool sellSignal)
	{
		buySignal = false;
		sellSignal = false;

		if (_closeSeries.Count <= 1)
		return false;

		var start = RelayShift + 1;
		var end = start + SspPeriod;

		if (end > _highSeries.Count || end > _lowSeries.Count)
		return false;

		var close = GetSeriesValue(_closeSeries, 1);

		decimal high = decimal.MinValue;
		decimal low = decimal.MaxValue;

		for (var i = start; i < end; i++)
		{
			var h = GetSeriesValue(_highSeries, i);
			var l = GetSeriesValue(_lowSeries, i);

			if (h > high)
			high = h;

			if (l < low)
			low = l;
		}

		var range = high - low;
		var smax = high - (low - high) * ChannelPercent / 100m;
		var smin = low + range * ChannelPercent / 100m;
		var innerPercent = ChannelPercent + ChannelStopPercent;
		var smax2 = high - range * innerPercent / 100m;
		var smin2 = low + range * innerPercent / 100m;

		var uptrend = _uptrend;
		var uptrend2 = _uptrend2;
		var old = uptrend;
		var old2 = uptrend2;

		if (close < smin && close < smax && uptrend2)
		uptrend = false;

		if (close > smax && close > smin && !uptrend2)
		uptrend = true;

		if ((close > smax2 || close > smin2) && !uptrend)
		uptrend2 = false;

		if ((close < smin2 || close < smax2) && uptrend)
		uptrend2 = true;

		if (close < smin && close < smax && !uptrend2)
		{
			sellSignal = true;
			uptrend2 = true;
		}

		if (close > smax && close > smin && uptrend2)
		{
			buySignal = true;
			uptrend2 = false;
		}

		if (uptrend != old && !uptrend)
		sellSignal = true;

		if (uptrend != old && uptrend)
		buySignal = true;

		_uptrend = uptrend;
		_uptrend2 = uptrend2;

		return true;
	}

	private static decimal GetSeriesValue(List<decimal> series, int index)
	{
		var targetIndex = series.Count - 1 - index;
		return targetIndex >= 0 ? series[targetIndex] : 0m;
	}

	private decimal GetOrderVolume(decimal price)
	{
		if (VolumeValue > 0m)
		return VolumeValue;

		var portfolio = Portfolio;
		var portfolioValue = portfolio?.CurrentValue ?? portfolio?.BeginValue ?? 0m;
		if (portfolioValue <= 0m || RiskPercent <= 0m)
		return 1m;

		var riskAmount = portfolioValue * RiskPercent / 100m;
		var stopOffset = StopLossPips > 0 ? ConvertPips(StopLossPips) : price * 0.01m;

		if (stopOffset <= 0m)
		return 1m;

		var volume = riskAmount / stopOffset;
		return volume > 0m ? volume : 1m;
	}

	private decimal ConvertPips(int pips)
	{
		if (pips <= 0)
		return 0m;

		var pipSize = GetPipSize();
		return pipSize <= 0m ? 0m : pipSize * pips;
	}

	private decimal GetPipSize()
	{
		var security = Security;
		if (security == null)
		return 0m;

		var step = security.PriceStep ?? 0m;
		if (step <= 0m)
		return 0m;

		var scale = GetDecimalScale(step);
		var factor = scale is 3 or 5 ? 10m : 1m;
		return step * factor;
	}

	private static int GetDecimalScale(decimal value)
	{
		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0xFF;
	}
}