View on GitHub

OpenPendingorderAfterPositionGetStopLoss

Overview

The OpenPendingorderAfterPositionGetStopLoss strategy ports the MetaTrader 5 expert advisor of the same name into the StockSharp high-level API. It continuously evaluates the slope of the Stochastic %K line on the selected timeframe. When %K turns down it places a sell stop order below the market, and when %K turns up it places a buy stop order above the market. Each filled entry immediately receives a protective stop-loss and take-profit order. If a stop-loss closes the position, the strategy automatically reinstalls the corresponding pending order so that the grid of breakout trades is restored without waiting for the next candle.

Trading rules

  • Subscribe to finished candles of the configured timeframe and compute a classic Stochastic oscillator (KPeriod, DPeriod, Slowing).
  • Compare the current %K value with the value two bars ago:
    • %K(current) < %K(two bars ago) → submit a sell stop below the best bid.
    • %K(current) > %K(two bars ago) → submit a buy stop above the best ask.
  • Pending orders are offset from the market by the current spread plus the user-defined MinStopDistancePoints buffer, matching the original MQL logic.
  • Once a pending order fills, the strategy sends a protective stop-loss (stop order) and an optional take-profit (limit order).
  • When the protective stop-loss fires, the corresponding pending order is recreated immediately using the latest market prices.
  • Protective orders are cancelled automatically when the position is closed by the take-profit or when the strategy stops.

Parameters

Name Description
OrderVolume Trade volume in lots for each pending order.
StopLossPoints Stop-loss distance in symbol points. Set to 0 to disable.
TakeProfitPoints Take-profit distance in symbol points. Set to 0 to disable.
MinStopDistancePoints Minimal price buffer (in points) added to the spread before placing a pending order.
MaxPositions Maximum number of simultaneous positions per direction (netting accounts effectively use 0 or 1).
KPeriod Number of bars used for the %K calculation.
DPeriod Length of the smoothing %D line.
Slowing Additional smoothing factor applied to %K before comparison.
PendingExpiry Optional lifetime of pending stop orders. Expired orders are cancelled on the next candle.
CandleType Timeframe used for candle subscription and indicator calculations.

Implementation notes

  • All order management relies on high-level helpers such as BuyStop, SellStop, SellLimit, and BuyLimit as required by AGENTS.md.
  • Indicator values are consumed directly inside the SubscribeCandles().BindEx(...) callback, avoiding any GetValue calls.
  • The strategy monitors MyTrade events to install and remove protective orders, emulating the OnTradeTransaction logic from the original Expert Advisor.
  • Comments inside the code are written in English and indentation is done with tabs, conforming to the repository guidelines.
namespace StockSharp.Samples.Strategies;

using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// Trades based on the slope of the Stochastic %K line.
/// Buys when %K is rising, sells when %K is falling.
/// Uses take-profit and stop-loss protection.
/// </summary>
public class OpenPendingorderAfterPositionGetStopLossStrategy : Strategy
{
	private readonly StrategyParam<int> _kPeriod;
	private readonly StrategyParam<int> _dPeriod;
	private readonly StrategyParam<int> _slowing;
	private readonly StrategyParam<decimal> _stopLossPct;
	private readonly StrategyParam<decimal> _takeProfitPct;
	private readonly StrategyParam<int> _signalCooldownCandles;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _lastK;
	private decimal? _prevK;
	private decimal _entryPrice;
	private int _candlesSinceTrade;

	public int KPeriod
	{
		get => _kPeriod.Value;
		set => _kPeriod.Value = value;
	}

	public int DPeriod
	{
		get => _dPeriod.Value;
		set => _dPeriod.Value = value;
	}

	public int Slowing
	{
		get => _slowing.Value;
		set => _slowing.Value = value;
	}

	public decimal StopLossPct
	{
		get => _stopLossPct.Value;
		set => _stopLossPct.Value = value;
	}

	public decimal TakeProfitPct
	{
		get => _takeProfitPct.Value;
		set => _takeProfitPct.Value = value;
	}

	public int SignalCooldownCandles
	{
		get => _signalCooldownCandles.Value;
		set => _signalCooldownCandles.Value = value;
	}

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

	public OpenPendingorderAfterPositionGetStopLossStrategy()
	{
		_kPeriod = Param(nameof(KPeriod), 22)
			.SetDisplay("%K Period", "Number of bars for %K", "Indicators");

		_dPeriod = Param(nameof(DPeriod), 7)
			.SetDisplay("%D Period", "Smoothing period for %K", "Indicators");

		_slowing = Param(nameof(Slowing), 2)
			.SetDisplay("Slowing", "Additional smoothing factor", "Indicators");

		_stopLossPct = Param(nameof(StopLossPct), 2m)
			.SetDisplay("Stop Loss %", "Stop-loss as percentage of entry price", "Risk");

		_takeProfitPct = Param(nameof(TakeProfitPct), 3m)
			.SetDisplay("Take Profit %", "Take-profit as percentage of entry price", "Risk");

		_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 4)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait between entries", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for indicator", "General");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_lastK = null;
		_prevK = null;
		_entryPrice = 0;
		_candlesSinceTrade = SignalCooldownCandles;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_lastK = null;
		_prevK = null;
		_entryPrice = 0;
		_candlesSinceTrade = SignalCooldownCandles;

		var rsi = new RelativeStrengthIndex { Length = KPeriod };

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

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, rsi);
			DrawOwnTrades(area);
		}
	}

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

		var close = candle.ClosePrice;

		if (_candlesSinceTrade < SignalCooldownCandles)
			_candlesSinceTrade++;

		// Check stop-loss / take-profit on existing position
		if (Position != 0 && _entryPrice > 0)
		{
			if (Position > 0)
			{
				var pnlPct = (close - _entryPrice) / _entryPrice * 100m;
				if (pnlPct <= -StopLossPct || pnlPct >= TakeProfitPct)
				{
					SellMarket();
					_entryPrice = 0;
					_prevK = currentK;
					_lastK = currentK;
					return;
				}
			}
			else if (Position < 0)
			{
				var pnlPct = (_entryPrice - close) / _entryPrice * 100m;
				if (pnlPct <= -StopLossPct || pnlPct >= TakeProfitPct)
				{
					BuyMarket();
					_entryPrice = 0;
					_prevK = currentK;
					_lastK = currentK;
					return;
				}
			}
		}

		// Need at least 2 values to determine signal transition.
		if (_lastK is not decimal prevK)
		{
			_lastK = currentK;
			return;
		}

		_prevK = _lastK;
		_lastK = currentK;

		var crossedUp = prevK <= 45m && currentK > 45m;
		var crossedDown = prevK >= 55m && currentK < 55m;

		if (crossedUp && Position <= 0 && _candlesSinceTrade >= SignalCooldownCandles)
		{
			BuyMarket();
			_entryPrice = close;
			_candlesSinceTrade = 0;
		}
		else if (crossedDown && Position >= 0 && _candlesSinceTrade >= SignalCooldownCandles)
		{
			SellMarket();
			_entryPrice = close;
			_candlesSinceTrade = 0;
		}
	}
}