View on GitHub

2DLimits

Overview

2DLimits is a direct port of the MetaTrader 4 expert advisor 2DLimits_EA_v2. The strategy evaluates the last two completed daily candles and only participates when they form a stair-step pattern (higher highs/lows or lower highs/lows). When the pattern is valid, the strategy submits stop orders at the previous day's extreme and protects the position with a midpoint stop-loss and a target equal to the prior daily range.

The implementation relies on StockSharp's high-level candle subscriptions together with level-1 quotes. Daily candles supply the breakout levels while the best bid/ask snapshots ensure that long setups are only armed when price trades below the midpoint and short setups only when price trades above it.

Strategy logic

Daily structure filter

  • The strategy keeps a two-day rolling window of completed daily candles (configurable through the candle type parameter).
  • A bullish setup requires the most recent day to register both a higher high and a higher low compared to the previous day.
  • A bearish setup requires the most recent day to post both a lower high and a lower low than the earlier day.
  • The midpoint of the latest day is calculated as (high + low) / 2, and the candle range is stored for the profit target.

Entry rules

  • Only one batch of pending orders is active at a time; all orders are cancelled and recalculated when a new daily candle closes.
  • Long entries are prepared when:
    • The bullish structure filter is satisfied.
    • The latest ask price is below the midpoint of the previous day (mirrors the original EA's Ask < middleY check).
    • A buy-stop order is placed exactly at the previous day's high.
  • Short entries are prepared when:
    • The bearish structure filter is satisfied.
    • The latest bid price is above the midpoint of the previous day (mirrors Bid > middleY).
    • A sell-stop order is placed at the previous day's low.
  • If both structure checks fail, no orders are left working for the upcoming session.

Exit rules

  • When a stop order triggers, the opposing entry order is cancelled immediately so the strategy never holds simultaneous long and short exposures.
  • After a long breakout fills, two protective orders are registered:
    • A stop order at the midpoint of the reference day acts as the stop-loss.
    • A take-profit order at previous high + previous range matches the MetaTrader take-profit distance.
  • After a short breakout fills, symmetric protection is applied:
    • A stop order at the midpoint (buy-stop) covers the stop-loss.
    • A take-profit order at previous low - previous range mirrors the original target.
  • Protective orders are re-armed whenever the filled position size changes and are removed once the position returns to flat.

Order lifecycle and safety checks

  • Pending orders are refreshed only after the next daily candle completes, enforcing a single setup per trading day.
  • The strategy skips signal generation whenever it already holds a position, preventing overlaps between setups.
  • The most recent bid/ask snapshot is retained from SubscribeLevel1(); if unavailable, the last trade price is used as a fallback to avoid submitting blind orders.
  • Rounding is performed with the instrument's price step so all orders align with the exchange tick size.

Parameters

Name Description
Volume Order volume for the stop entries. Must be greater than zero.
CandleType Candle type that provides the reference range (defaults to daily candles).

Additional notes

  • The strategy manages every order directly through the high-level API; there is no reliance on custom collections or indicator buffers.
  • Only the C# implementation is provided in this package. No Python version is created for this conversion.
  • Tests are untouched as requested.
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

public class TwoDLimitsStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public TwoDLimitsStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}