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
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.
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.
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.
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.#####}");
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class lucky_shift_limit_strategy(Strategy):
"""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."""
def __init__(self):
super(lucky_shift_limit_strategy, self).__init__()
self._shift_points = self.Param("ShiftPoints", 3) \
.SetGreaterThanZero() \
.SetDisplay("Shift points", "Minimum price delta between consecutive candles", "Trading")
self._limit_points = self.Param("LimitPoints", 18) \
.SetGreaterThanZero() \
.SetDisplay("Limit points", "Maximum allowed drawdown in percentage", "Risk management")
self._previous_high = None
self._previous_low = None
self._shift_threshold = 0.0
self._limit_threshold = 0.0
self._entry_price = 0.0
self._thresholds_ready = False
self._hold_bars = 0
@property
def ShiftPoints(self):
return self._shift_points.Value
@property
def LimitPoints(self):
return self._limit_points.Value
def OnReseted(self):
super(lucky_shift_limit_strategy, self).OnReseted()
self._previous_high = None
self._previous_low = None
self._shift_threshold = 0.0
self._limit_threshold = 0.0
self._entry_price = 0.0
self._thresholds_ready = False
self._hold_bars = 0
def OnStarted2(self, time):
super(lucky_shift_limit_strategy, self).OnStarted2(time)
tf = DataType.TimeFrame(TimeSpan.FromMinutes(5))
subscription = self.SubscribeCandles(tf)
subscription.Bind(self._process_candle).Start()
def _ensure_thresholds(self, price):
if self._thresholds_ready:
return
if price <= 0:
return
# ShiftPoints=3 -> 0.9% shift threshold, LimitPoints=18 -> 1.8% limit threshold
self._shift_threshold = float(price) * self.ShiftPoints * 0.003
self._limit_threshold = float(price) * self.LimitPoints * 0.01
self._thresholds_ready = True
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
self._ensure_thresholds(close)
if not self._thresholds_ready:
return
# Count hold bars for position management.
if self.Position != 0:
self._hold_bars += 1
# Entry logic: detect sudden shifts in high/low between consecutive candles.
# Only enter when flat.
if self.Position == 0 and self._previous_high is not None and self._previous_low is not None:
prev_high = self._previous_high
prev_low = self._previous_low
# High jumped up sharply -> sell on expected reversion
if high - prev_high >= self._shift_threshold:
self.SellMarket()
self._entry_price = close
self._hold_bars = 0
# Low dropped sharply -> buy on expected rebound
elif prev_low - low >= self._shift_threshold:
self.BuyMarket()
self._entry_price = close
self._hold_bars = 0
self._previous_high = high
self._previous_low = low
self._try_close_position(close)
def OnOwnTradeReceived(self, trade):
super(lucky_shift_limit_strategy, self).OnOwnTradeReceived(trade)
if self.Position != 0 and self._entry_price == 0:
self._entry_price = float(trade.Trade.Price)
if self.Position == 0:
self._entry_price = 0.0
def _try_close_position(self, current_price):
if self.Position == 0:
return
avg_price = self._entry_price
if avg_price <= 0:
return
# Minimum hold of 5 bars before checking exit.
if self._hold_bars < 5:
return
# Use half of shift threshold as profit target.
profit_target = self._shift_threshold * 0.5
if self.Position > 0:
# Close long on profit or loss cap.
if current_price - avg_price >= profit_target:
self.SellMarket()
self._hold_bars = 0
elif self._limit_threshold > 0 and avg_price - current_price >= self._limit_threshold:
self.SellMarket()
self._hold_bars = 0
elif self.Position < 0:
# Close short on profit or loss cap.
if avg_price - current_price >= profit_target:
self.BuyMarket()
self._hold_bars = 0
elif self._limit_threshold > 0 and current_price - avg_price >= self._limit_threshold:
self.BuyMarket()
self._hold_bars = 0
def CreateClone(self):
return lucky_shift_limit_strategy()