View on GitHub

Open Close Strategy (ID 3996)

Overview

This strategy replicates the MetaTrader 4 expert open_close.mq4. It works on a single instrument and compares the open and close of the latest candle against the previous one. When no position is active, it fades strong one-bar moves (gap-and-reversal patterns). While in a trade, it closes the position either when the pattern reverses or when a floating-loss protection threshold is breached.

Trading Logic

Entry rules

  • Trades only when the previous candle has been processed (the original Volume[0] == 1 guard).
  • Long entry: the current candle opens above the previous open and closes below the previous close. The strategy buys the configured volume at market.
  • Short entry: the current candle opens below the previous open and closes above the previous close. The strategy sells short at market.

Only one position can be active at any time. New signals are ignored until the open position is closed.

Exit rules

  1. Risk protection: floating PnL is measured from the average entry price. If the unrealized loss exceeds MaximumRisk × Portfolio.CurrentValue, the strategy immediately closes the position. The original MQL version used AccountMargin, which is approximated here with the best available portfolio valuation.
  2. Pattern reversal:
    • Long positions close when the next candle continues downward (open < previous open and close < previous close).
    • Short positions close when the next candle continues upward (open > previous open and close > previous close).

Position Sizing

  • The default order size is derived from MaximumRisk. The strategy multiplies the available account value by MaximumRisk and divides the result by 1000, mimicking the MetaTrader calculation of AccountFreeMargin * MaximumRisk / 1000.
  • If the account information is not available, the fallback InitialVolume parameter is used.
  • After more than one consecutive losing trade, the lot size is reduced by volume × losses / DecreaseFactor, reproducing the MetaTrader loop over the history of closed trades.
  • A minimum tradable volume of 0.1 lots is enforced before aligning the quantity to the instrument volume step and exchange limits.

Parameters

Name Type Default Description
InitialVolume decimal 0.1 Fallback lot size used when equity information is not available.
MaximumRisk decimal 0.3 Fraction of account value that controls both position sizing and the maximum tolerated floating loss.
DecreaseFactor decimal 100 Reduction factor applied after more than one consecutive losing trade.
CandleType DataType 15m time-frame Candle series used to evaluate the pattern.

Implementation Notes

  • The strategy subscribes to the selected candle series and processes only finished candles, matching the Volume[0] > 1 condition in the original expert.
  • Floating PnL is estimated from the strategy’s current position and the latest close price because StockSharp does not expose MetaTrader’s AccountProfit and AccountMargin metrics.
  • Consecutive losses are tracked through filled trades, allowing DecreaseFactor to behave like the original loop over the trade history.
  • Volume alignment respects Security.VolumeStep, MinVolume, and MaxVolume to stay compatible with exchange requirements.
  • Charts are populated with candles and own trades when a chart area is available for visual debugging.

Usage Tips

  • Choose a candle interval that matches the one used in MetaTrader when calibrating the original expert.
  • Adjust MaximumRisk and DecreaseFactor to tune the aggressiveness of the lot-sizing rule.
  • Because the strategy is contrarian, it performs best on instruments that exhibit frequent single-bar overextensions and snap-back moves.
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>
/// Contrarian pattern strategy converted from the MetaTrader expert "open_close".
/// Evaluates relationships between consecutive candle opens and closes.
/// Buys when a bearish candle opens above the previous open (fading the move),
/// and sells when a bullish candle opens below the previous open.
/// </summary>
public class OpenCloseStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _ema;

	private bool _hasPreviousCandle;
	private decimal _previousOpen;
	private decimal _previousClose;

	public OpenCloseStrategy()
	{
		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop loss in absolute points", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take profit in absolute points", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Time-frame used to evaluate the open/close pattern.", "Data");

		Volume = 1;
	}

	/// <summary>
	/// Stop loss distance in absolute points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance in absolute points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Candle series used to evaluate the pattern.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_hasPreviousCandle = false;
		_previousOpen = 0m;
		_previousClose = 0m;
		_ema = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		_ema = new ExponentialMovingAverage { Length = 20 };

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

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

		var tp = TakeProfitPoints > 0 ? new Unit(TakeProfitPoints, UnitTypes.Absolute) : null;
		var sl = StopLossPoints > 0 ? new Unit(StopLossPoints, UnitTypes.Absolute) : null;
		if (tp != null || sl != null)
			StartProtection(tp, sl);

		base.OnStarted2(time);
	}

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

		var open = candle.OpenPrice;
		var close = candle.ClosePrice;

		if (!_hasPreviousCandle)
		{
			_previousOpen = open;
			_previousClose = close;
			_hasPreviousCandle = true;
			return;
		}

		// Exit logic
		if (Position > 0)
		{
			// Close long on bearish continuation
			if (open < _previousOpen && close < _previousClose)
				SellMarket(Position);
		}
		else if (Position < 0)
		{
			// Close short on bullish continuation
			if (open > _previousOpen && close > _previousClose)
				BuyMarket(Math.Abs(Position));
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_previousOpen = open;
			_previousClose = close;
			return;
		}

		// Entry logic
		if (Position == 0)
		{
			// Buy: fade a bearish candle that opened above the previous open
			if (open > _previousOpen && close < _previousClose)
			{
				BuyMarket(Volume);
			}
			// Sell: fade a bullish candle that opened below the previous open
			else if (open < _previousOpen && close > _previousClose)
			{
				SellMarket(Volume);
			}
		}

		_previousOpen = open;
		_previousClose = close;
	}
}