Ver no GitHub

Lucky Shift Limit Strategy

The Lucky Shift Limit strategy is a direct conversion of the MetaTrader 4 expert advisor Lucky_acnl6p6j89zn91fa.mq4. It watches the best bid/ask quotes in real time and reacts to sudden jumps measured in MetaTrader "points" (pips). When the ask price accelerates upward by the configured shift distance the strategy fades the move by selling, while a sharp drop in the bid prompts a contrarian buy. All open trades are constantly monitored and closed either once they become profitable or when the floating loss exceeds a safety threshold identical to the original MQ4 logic.

Data and execution requirements

  • Market data – subscribes to Level 1 quotes only; no candles or depth of market are required.
  • Execution style – entries and exits rely on market orders to mimic the immediate OrderSend calls from MetaTrader.
  • Account mode – works with both hedging and netting accounts. On netting accounts the strategy accumulates exposure in a single position and the exit module flattens it.
  • Volume sizing – default order size comes from Strategy.Volume, but the helper emulates AccountFreeMargin/10000 from MetaTrader when the portfolio value is available.

Parameters

Name Default Description
Shift points 3 Minimum number of MetaTrader points between consecutive asks/bids that triggers a new order. Larger values filter out noise, smaller values react faster.
Limit points 18 Maximum adverse excursion allowed for an open trade. If price moves against the position by this many points the trade is force-closed.

Both parameters are expressed in MetaTrader points and converted internally into absolute price offsets using the instrument tick size. Optimisation boundaries in the UI match the practical ranges from the MQ4 version.

Trading logic

  1. Initialisation
    • Converts the point-based settings into actual price distances using Security.PriceStep.
    • Resets cached bid/ask quotes and starts a Level 1 subscription with high-level Bind processing.
  2. Entry conditions
    • If the ask rises by at least Shift points compared to the previous ask, the strategy sends a market sell order (fading the spike) with a log note explaining the trigger.
    • If the bid falls by at least the same distance compared to the previous bid, it opens a market buy.
    • Signals can fire multiple times in sequence, exactly like the original expert that did not restrict the number of simultaneous positions.
  3. Exit management
    • Every quote tick invokes TryClosePosition(). Long positions are closed immediately when the bid is above the average entry (realised profit) or when the ask is lower than the entry by Limit points (loss cap).
    • Short positions mirror this logic, closing on profitable ask quotes or when the bid exceeds the entry by the configured limit.
    • All exits use market orders to replicate OrderClose and guarantee the position is flattened on the same tick.
  4. Position sizing
    • Calculates the default volume from portfolio equity (equity / 10,000, rounded to one decimal lot) when available, matching the MQ4 helper GetLots().
    • Falls back to the strategy Volume property when equity data is missing.

Implementation notes

  • Uses only high-level StockSharp APIs: SubscribeLevel1().Bind(ProcessLevel1) removes the need for manual quote listeners.
  • No custom collections are stored; previous bid/ask values are kept in simple nullable variables as permitted by the guidelines.
  • The loss cap works with the instrument tick size, so exotic symbols with fractional pip steps automatically map to the correct price delta.
  • Parameter changes during runtime are respected—the strategy recalculates thresholds when Level 1 data arrives.
  • Logging statements document every entry and exit reason, which simplifies backtesting and live diagnostics.

Usage tips

  • Ideal for highly liquid FX pairs or indices where bid/ask shocks occur frequently.
  • Consider pairing the strategy with portfolio-level protections (StartProtection) if additional stop loss or drawdown limits are required.
  • Increase Shift points on noisy feeds to reduce overtrading, or decrease it to capture ultra-short-term moves.
  • The logic is inherently contrarian; if breakout behaviour is desired simply set Shift points high enough or combine it with another filter indicator.
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Candle-based reversion strategy that reacts to sudden price jumps (high/low shifts)
/// and enforces a configurable loss cap. Adapted from a Level1 quote-reversion approach
/// to work with candle data for backtesting.
/// </summary>
public class LuckyShiftLimitStrategy : Strategy
{
	private readonly StrategyParam<int> _shiftPoints;
	private readonly StrategyParam<int> _limitPoints;

	private decimal? _previousHigh;
	private decimal? _previousLow;
	private decimal _shiftThreshold;
	private decimal _limitThreshold;
	private decimal _entryPrice;
	private bool _thresholdsReady;
	private int _holdBars;

	/// <summary>
	/// Minimum price shift (as percentage tenths) required to trigger an entry.
	/// </summary>
	public int ShiftPoints
	{
		get => _shiftPoints.Value;
		set => _shiftPoints.Value = value;
	}

	/// <summary>
	/// Maximum adverse excursion (as percentage) tolerated before force-closing losing trades.
	/// </summary>
	public int LimitPoints
	{
		get => _limitPoints.Value;
		set => _limitPoints.Value = value;
	}

	/// <summary>
	/// Initializes the strategy parameters taken from the original MQ4 expert.
	/// </summary>
	public LuckyShiftLimitStrategy()
	{
		_shiftPoints = Param(nameof(ShiftPoints), 3)
			.SetGreaterThanZero()
			.SetDisplay("Shift points", "Minimum price delta between consecutive candles", "Trading")

			.SetOptimize(1, 20, 1);

		_limitPoints = Param(nameof(LimitPoints), 18)
			.SetGreaterThanZero()
			.SetDisplay("Limit points", "Maximum allowed drawdown in percentage", "Risk management")

			.SetOptimize(5, 80, 5);
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_previousHigh = null;
		_previousLow = null;
		_shiftThreshold = 0m;
		_limitThreshold = 0m;
		_entryPrice = 0m;
		_thresholdsReady = false;
		_holdBars = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		var tf = TimeSpan.FromMinutes(5).TimeFrame();

		SubscribeCandles(tf)
			.Bind(ProcessCandle)
			.Start();
	}

	private void EnsureThresholds(decimal price)
	{
		if (_thresholdsReady)
			return;

		if (price <= 0m)
			return;

		// ShiftPoints=3 -> 0.9% shift threshold, LimitPoints=18 -> 1.8% limit threshold
		_shiftThreshold = price * ShiftPoints * 0.003m;
		_limitThreshold = price * LimitPoints * 0.01m;
		_thresholdsReady = true;
	}

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

		var high = candle.HighPrice;
		var low = candle.LowPrice;
		var close = candle.ClosePrice;

		EnsureThresholds(close);

		if (!_thresholdsReady)
			return;

		// Count hold bars for position management.
		if (Position != 0)
			_holdBars++;

		// Entry logic: detect sudden shifts in high/low between consecutive candles.
		// Only enter when flat.
		if (Position == 0 && _previousHigh is decimal prevHigh && _previousLow is decimal prevLow)
		{
			// High jumped up sharply -> sell on expected reversion
			if (high - prevHigh >= _shiftThreshold)
			{
				SellMarket();
				_entryPrice = close;
				_holdBars = 0;
				LogInfo($"Sell triggered: high shift {high - prevHigh:0.##} >= {_shiftThreshold:0.##}. Price={close:0.#####}");
			}
			// Low dropped sharply -> buy on expected rebound
			else if (prevLow - low >= _shiftThreshold)
			{
				BuyMarket();
				_entryPrice = close;
				_holdBars = 0;
				LogInfo($"Buy triggered: low shift {prevLow - low:0.##} >= {_shiftThreshold:0.##}. Price={close:0.#####}");
			}
		}

		_previousHigh = high;
		_previousLow = low;

		TryClosePosition(close);
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (Position != 0m && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

	private void TryClosePosition(decimal currentPrice)
	{
		if (Position == 0)
			return;

		var avgPrice = _entryPrice;

		if (avgPrice <= 0m)
			return;

		// Minimum hold of 5 bars before checking exit.
		if (_holdBars < 5)
			return;

		// Use half of shift threshold as profit target.
		var profitTarget = _shiftThreshold * 0.5m;

		if (Position > 0)
		{
			// Close long on profit or loss cap.
			if (currentPrice - avgPrice >= profitTarget)
			{
				SellMarket();
				_holdBars = 0;
				LogInfo($"Closed long in profit. Price={currentPrice:0.#####}");
			}
			else if (_limitThreshold > 0m && avgPrice - currentPrice >= _limitThreshold)
			{
				SellMarket();
				_holdBars = 0;
				LogInfo($"Closed long on loss cap. Price={currentPrice:0.#####}");
			}
		}
		else if (Position < 0)
		{
			// Close short on profit or loss cap.
			if (avgPrice - currentPrice >= profitTarget)
			{
				BuyMarket();
				_holdBars = 0;
				LogInfo($"Closed short in profit. Price={currentPrice:0.#####}");
			}
			else if (_limitThreshold > 0m && currentPrice - avgPrice >= _limitThreshold)
			{
				BuyMarket();
				_holdBars = 0;
				LogInfo($"Closed short on loss cap. Price={currentPrice:0.#####}");
			}
		}
	}
}