Ver en GitHub

One Price Stop-Loss / Take-Profit Strategy

This utility strategy replicates the MetaTrader script "One Price SL TP" inside StockSharp. Instead of opening trades, the algorithm watches the current position on the configured instrument and makes sure that both protective orders are aligned with a single target price specified by the user.

Whenever the parameter ZenPrice is above zero, the strategy compares it with the live bid/ask quotes:

  • For a long position: if ZenPrice is higher than the ask, a take-profit limit order is placed at that price; if ZenPrice is lower than the bid, a stop-loss stop order is registered instead.
  • For a short position: if ZenPrice is lower than the bid, it becomes the take-profit limit order; if ZenPrice is higher than the ask, it becomes the stop-loss stop order.

When the price falls between bid and ask nothing is sent, so the previous protective order remains untouched. As soon as the position is closed or the parameter is reset to zero, all protective orders are cancelled automatically.

How it works

  1. Subscribes to Level1 data to receive up-to-date bid/ask quotes that are required for the direction checks.
  2. Keeps track of the current strategy position volume and direction. Positions are assumed to be created manually or by other strategies.
  3. On each quote, position or personal trade update, recalculates which side of the market the ZenPrice belongs to and builds the corresponding protective order type.
  4. Normalises the requested price using the instrument price step and rounds the order volume to exchange limits before sending anything to the trading connector.
  5. Uses ReRegisterOrder to modify already active protective orders instead of cancelling them, matching the behaviour of MetaTrader's in-place modification.

Parameter

  • ZenPrice – absolute price that should be used either as a stop-loss or take-profit level. Set the value to 0 to disable the automation. Default: 0.

Practical notes

  • The strategy never submits entry orders. It is safe to start it alongside discretionary trading terminals or other automated strategies.
  • Protective orders are issued only after the first Level1 snapshot delivers both bid and ask quotes. Until then the script waits, just like the original MQL version relied on the terminal quotes.
  • When only one side of the market satisfies the condition (for example, ZenPrice is above ask but not below bid), the other protective order is cancelled to avoid stale prices.
  • All comments inside the code are in English, while this documentation is provided in multiple languages in accordance with the project guidelines.

Differences from the MetaTrader script

  • The original script modifies the stop-loss and take-profit fields of an existing position ticket. StockSharp exposes protective orders as explicit stop and limit orders, therefore the conversion operates on exchange-visible orders instead.
  • MetaTrader automatically snaps the price to the broker precision. In this port the same behaviour is reproduced via NormalizePrice, which leverages the symbol's price step and decimal settings.
  • Position volume is rounded to exchange lot limits before sending the protective orders, ensuring compatibility with venues that require specific lot steps.
namespace StockSharp.Samples.Strategies;

using System;
using Ecng.Common;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// One Price SL TP strategy: EMA + candle body size filter.
/// Buys when bullish candle with large body closes above EMA, sells on opposite.
/// </summary>
public class OnePriceSlTpStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _emaPeriod;
	private bool _wasBullishSignal;
	private bool _hasPrevSignal;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int EmaPeriod { get => _emaPeriod.Value; set => _emaPeriod.Value = value; }

	public OnePriceSlTpStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Candle timeframe", "General");
		_emaPeriod = Param(nameof(EmaPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("EMA Period", "EMA trend filter period", "Indicators");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_wasBullishSignal = false;
		_hasPrevSignal = false;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_wasBullishSignal = false;
		_hasPrevSignal = false;
		var ema = new ExponentialMovingAverage { Length = EmaPeriod };
		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ema, ProcessCandle).Start();
	}

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

		var body = Math.Abs(candle.ClosePrice - candle.OpenPrice);
		var range = candle.HighPrice - candle.LowPrice;
		if (range <= 0) return;

		// Strong candle: body > 60% of range
		if (body < range * 0.7m) return;

		var bullishSignal = candle.ClosePrice > candle.OpenPrice && candle.ClosePrice > emaValue;
		var bearishSignal = candle.ClosePrice < candle.OpenPrice && candle.ClosePrice < emaValue;
		var crossedUp = bullishSignal && (!_hasPrevSignal || !_wasBullishSignal);
		var crossedDown = bearishSignal && (!_hasPrevSignal || _wasBullishSignal);

		if (crossedUp && Position <= 0)
			BuyMarket();
		else if (crossedDown && Position >= 0)
			SellMarket();

		if (bullishSignal || bearishSignal)
		{
			_wasBullishSignal = bullishSignal;
			_hasPrevSignal = true;
		}
	}
}