Lucky Shift Limit — это точная конверсия эксперта MetaTrader 4 Lucky_acnl6p6j89zn91fa.mq4. Стратегия отслеживает лучшие цены Bid и Ask в режиме реального времени и реагирует на резкие скачки, измеряемые в метатрейдеровских пунктах. Если Ask ускоряется вверх на заданное расстояние, открывается продажа против движения; если Bid резко падает, выполняется покупка. Каждая позиция контролируется по прибыли и убытку: сделки закрываются либо при появлении положительного результата, либо при достижении допустимого просадочного лимита — точь‑в‑точь как в MQ4.
Требования к данным и исполнению
Рыночные данные — необходима только лента Level 1 (лучшие Bid/Ask); свечи и стакан не используются.
Тип исполнения — все входы и выходы выполняются рыночными заявками, имитируя моментальные OrderSend/OrderClose из MetaTrader.
Режим счёта — подходит для хеджевого и неттингового учёта. В неттинге экспозиция накапливается в одной позиции, а модуль выхода полностью её закрывает.
Размер лота — базовый объём берётся из Strategy.Volume, но при наличии данных по портфелю повторяет формулу AccountFreeMargin/10000, используемую в исходном советнике.
Параметры
Название
Значение по умолчанию
Описание
Shift points
3
Минимальное количество пунктов между соседними котировками, при котором формируется новая сделка. Большое значение фильтрует шум, малое делает стратегию чувствительнее.
Limit points
18
Максимальная допустимая просадка позиции. Если цена уходит против сделки больше чем на указанное число пунктов, позиция принудительно закрывается.
Оба параметра задаются в пунктах MetaTrader и внутри стратегии переводятся в абсолютные ценовые смещения согласно шагу цены инструмента. Диапазоны оптимизации соответствуют практическим значениям оригинального эксперта.
Логика торговли
Инициализация
Переводит параметры в ценовые смещения, используя Security.PriceStep.
Сбрасывает кеш предыдущих котировок и запускает подписку Level 1 через высокоуровневый метод Bind.
Условия входа
Если Ask вырос как минимум на Shift points по сравнению с предыдущим значением, отправляется рыночная продажа (fade-логика) с логированием причины.
Если Bid упал на такую же величину, стратегия покупает рынком.
Сигналы могут срабатывать последовательно, как и в MQ4, где не было ограничения на количество одновременных позиций.
Выход из позиции
На каждом тиковом обновлении вызывается TryClosePosition(). Длинные позиции закрываются, когда Bid становится выше средней цены входа (фиксируется прибыль), либо когда Ask уходит ниже входа на Limit points (ограничение убытка).
Короткие позиции зеркально закрываются по прибыльным значениям Ask или при превышении лимита убытка по Bid.
Все выходы осуществляются рыночными заявками, что гарантирует закрытие позиции на том же тике.
Расчёт объёма
При наличии оценки капитала портфеля объём рассчитывается как equity / 10000, округлённый до десятых лота — полностью повторяя функцию GetLots() из MetaTrader.
Если данные об капитале недоступны, используется значение Strategy.Volume.
Особенности реализации
Используется только высокоуровневый API StockSharp: SubscribeLevel1().Bind(ProcessLevel1) избавляет от ручной обработки котировок.
Внутри не создаются коллекции; предыдущие значения Bid/Ask хранятся в nullable-переменных, что соответствует требованиям из AGENTS.md.
Просадочный лимит учитывает шаг цены инструмента, поэтому корректно работает с пятизнаками и дробными тиками.
При изменении параметров во время работы пороги пересчитываются по новым значениям.
Подробные сообщения в логах помогают анализировать каждую сделку в тестировании и реальной торговле.
Рекомендации по использованию
Наилучшие результаты достигаются на ликвидных валютных парах и индексах с частыми всплесками спреда.
При необходимости расширяйте защиту портфеля (например, через StartProtection), чтобы добавить стоп-лосс или ограничения по просадке.
Увеличивайте Shift points, если поток котировок слишком шумный, или уменьшайте его для сверхкоротких реакций.
Стратегия по своей природе контртрендовая; для работы по пробоям комбинируйте её с фильтрами или повышайте минимальное смещение.
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()