View on GitHub

Conditional Position Opener Strategy

Overview

The Conditional Position Opener Strategy reproduces the behaviour of the original MetaTrader script "Open a buy position if there's no open position". The logic is intentionally simple: when manual switches enable the long or short side the strategy sends a market order only if there is no open exposure in that direction. This prevents duplicate entries and keeps the position aligned with the selected side.

The StockSharp port keeps the implementation broker-neutral by relying on the framework's high-level candle subscription and built-in protection helper. Stop-loss and take-profit distances are provided in pip units (price steps) so they can be adapted to any instrument.

Strategy Logic

  1. Subscribe to the configured candle series to act as a timing heartbeat.
  2. On each finished candle check the current net position.
  3. If the long switch is enabled and the position is flat or short, send a buy market order.
  4. If the short switch is enabled and the position is flat or long, send a sell market order.
  5. Protective orders are managed automatically through StartProtection, which converts the pip distances into actual price offsets.

Because StockSharp uses net positions, enabling both sides at the same time will first open the long trade and then, if still flat after fills, the short trade. This mirrors the intent of the MQL code that avoided multiple orders per direction.

Parameters

Name Default Description
Volume 1 Order size for each market entry.
StopLossPips 100 Stop-loss distance expressed in price steps. Set to zero to disable.
TakeProfitPips 200 Take-profit distance expressed in price steps. Set to zero to disable.
EnableBuy false When true the strategy may open long positions if no long exposure exists.
EnableSell false When true the strategy may open short positions if no short exposure exists.
CandleType 1 minute timeframe Candle series that drives the periodic evaluation.

Notes

  • Distances are converted to actual price increments using the instrument's PriceStep. If the exchange does not report it, the raw pip value is used as an absolute offset.
  • StartProtection automatically attaches stop-loss and take-profit orders after every fill, so no manual order management is required.
  • The strategy focuses on manual-style triggering and is intended as a template for discretionary execution via parameters.
using System;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Simplified from "Conditional Position Opener" MetaTrader expert.
/// Uses Momentum indicator to conditionally open long or short positions.
/// Opens long when momentum is positive, short when negative.
/// </summary>
public class ConditionalPositionOpenerStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _momentumPeriod;

	private Momentum _momentum;
	private decimal? _prevMomentum;

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

	public int MomentumPeriod
	{
		get => _momentumPeriod.Value;
		set => _momentumPeriod.Value = value;
	}

	public ConditionalPositionOpenerStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for signal generation", "General");

		_momentumPeriod = Param(nameof(MomentumPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("Momentum Period", "Momentum indicator period", "Indicators");
	}

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

		_prevMomentum = null;
		_momentum = new Momentum { Length = MomentumPeriod };

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

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

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

		if (!_momentum.IsFormed)
		{
			_prevMomentum = momentumValue;
			return;
		}

		if (_prevMomentum is null)
		{
			_prevMomentum = momentumValue;
			return;
		}

		var volume = Volume;
		if (volume <= 0)
			volume = 1;

		// Cross above 101 (positive momentum)
		var crossUp = _prevMomentum.Value <= 101m && momentumValue > 101m;
		// Cross below 99 (negative momentum)
		var crossDown = _prevMomentum.Value >= 99m && momentumValue < 99m;

		if (crossUp)
		{
			if (Position <= 0)
				BuyMarket(Position < 0 ? Math.Abs(Position) + volume : volume);
		}
		else if (crossDown)
		{
			if (Position >= 0)
				SellMarket(Position > 0 ? Math.Abs(Position) + volume : volume);
		}

		_prevMomentum = momentumValue;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		_momentum = null;
		_prevMomentum = null;

		base.OnReseted();
	}
}