Стратегия Trend Me Leave Me
Обзор
Trend Me Leave Me — это точная конверсия классического советника MQL5 Юрия Решетова. Стратегия терпеливо ждёт спокойные периоды рынка, входит в направлении Parabolic SAR и после прибыльной сделки меняет сторону. Если позиция закрывается по стопу, алгоритм снова попытает ту же сторону, полностью повторяя исходную логику "trend me, leave me". Реализация на C# использует высокоуровневый API StockSharp и предоставляет доступ ко всем числовым параметрам оригинала.
Ключевые идеи
Фильтр спокойного рынка
- Индикатор Average Directional Index (ADX) с периодом
AdxPeriodизмеряет силу тренда. - Новые входы разрешены только тогда, когда скользящее среднее ADX опускается ниже
AdxQuietLevel, что соответствует поиску низковолатильных откатов в исходном советнике.
Тайминг по Parabolic SAR
- Точки Parabolic SAR задают направление. Лонг возможен, когда закрытие свечи выше точки SAR, шорт — когда закрытие ниже.
- Параметры
SarStepиSarMaxповторяют настройки ускорения из MQL и при необходимости могут оптимизироваться.
Планировщик направления
- Внутренний флаг
TradeDirectionsзаменяет переменнуюcmd. На старте он находится в состоянии buy. - После выхода по тейк-профиту флаг переключается на противоположную сторону и стратегия ищет разворотную сделку.
- После стоп-лосса (или срабатывания безубыточности) флаг сохраняет прежнюю сторону, чтобы повторить попытку.
Управление позицией
StopLossPipsиTakeProfitPipsзадают фиксированные расстояния от средней цены входа. Значение0отключает соответствующую защиту.BreakevenPipsпереносит стоп в точку входа после движения в прибыль на заданное количество пунктов. Возврат цены к входу закрывает сделку около нуля и оставляет флаг направления без изменений.- Проверка стопов и целей выполняется по завершённой свече с учётом High/Low, что максимально приближает поведение к тиковому исполнению исходного эксперта.
Управление объёмом
- Объём заявок определяется базовым свойством
Strategy.Volume. В примере не используется объектCMoneyFixedRiskиз MQL — при необходимости можно изменитьVolumeили расширить стратегию для более сложного манименеджмента.
Параметры
| Параметр | Описание | Значение по умолчанию |
|---|---|---|
StopLossPips |
Расстояние до защитного стопа в пунктах. | 50 |
TakeProfitPips |
Расстояние до тейк-профита в пунктах. | 180 |
BreakevenPips |
Перевод стопа в безубыток после указанного движения. | 5 |
AdxPeriod |
Период сглаживания ADX. | 14 |
AdxQuietLevel |
Максимальное значение ADX для входа. | 20 |
SarStep |
Шаг ускорения Parabolic SAR. | 0.02 |
SarMax |
Максимальное ускорение Parabolic SAR. | 0.2 |
CandleType |
Таймфрейм расчёта. | Свечи 1 час |
Особенности реализации
- Размер пункта вычисляется как шаг цены, умноженный на 10 для инструментов с 3 или 5 знаками после запятой — как и в MQL.
- Индикаторы подключены через высокоуровневый API StockSharp, сделки выполняются рыночными заявками
BuyMarket/SellMarket. - Python-версия намеренно отсутствует, папка
PY/не создавалась согласно задаче. - Перед запуском стратегии выберите инструмент, установите
Volumeи настройте параметры под волатильность конкретного рынка.
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>
/// Trend Me Leave Me strategy converted from the original MQL5 version.
/// Waits for calm markets, trades with Parabolic SAR direction and flips after profitable exits.
/// </summary>
public class TrendMeLeaveMeStrategy : Strategy
{
private enum TradeDirections
{
None,
Buy,
Sell
}
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _breakevenPips;
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _adxQuietLevel;
private readonly StrategyParam<decimal> _sarStep;
private readonly StrategyParam<decimal> _sarMax;
private readonly StrategyParam<DataType> _candleType;
private AverageDirectionalIndex _adx = null!;
private ParabolicSar _sar = null!;
private TradeDirections _nextDirection = TradeDirections.Buy;
private bool _breakevenActivated;
private decimal _pipSize;
private int _positionDirection;
private bool _exitOrderPending;
private decimal _entryPrice;
/// <summary>
/// Stop loss distance expressed in pips.
/// </summary>
public int StopLossPips
{
get => _stopLossPips.Value;
set => _stopLossPips.Value = value;
}
/// <summary>
/// Take profit distance expressed in pips.
/// </summary>
public int TakeProfitPips
{
get => _takeProfitPips.Value;
set => _takeProfitPips.Value = value;
}
/// <summary>
/// Breakeven trigger distance expressed in pips.
/// </summary>
public int BreakevenPips
{
get => _breakevenPips.Value;
set => _breakevenPips.Value = value;
}
/// <summary>
/// ADX averaging period.
/// </summary>
public int AdxPeriod
{
get => _adxPeriod.Value;
set
{
_adxPeriod.Value = value;
if (_adx != null)
_adx.Length = value;
}
}
/// <summary>
/// ADX level that defines when the market is calm enough to enter.
/// </summary>
public decimal AdxQuietLevel
{
get => _adxQuietLevel.Value;
set => _adxQuietLevel.Value = value;
}
/// <summary>
/// Parabolic SAR acceleration step.
/// </summary>
public decimal SarStep
{
get => _sarStep.Value;
set
{
_sarStep.Value = value;
if (_sar != null)
_sar.AccelerationStep = value;
}
}
/// <summary>
/// Maximum Parabolic SAR acceleration factor.
/// </summary>
public decimal SarMax
{
get => _sarMax.Value;
set
{
_sarMax.Value = value;
if (_sar != null)
_sar.AccelerationMax = value;
}
}
/// <summary>
/// Candle type used by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="TrendMeLeaveMeStrategy"/> class.
/// </summary>
public TrendMeLeaveMeStrategy()
{
_stopLossPips = Param(nameof(StopLossPips), 50)
.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk");
_takeProfitPips = Param(nameof(TakeProfitPips), 180)
.SetDisplay("Take Profit (pips)", "Take profit distance", "Risk");
_breakevenPips = Param(nameof(BreakevenPips), 5)
.SetDisplay("Breakeven (pips)", "Distance before moving stop to entry", "Risk");
_adxPeriod = Param(nameof(AdxPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ADX Period", "Smoothing period for ADX", "Indicators");
_adxQuietLevel = Param(nameof(AdxQuietLevel), 20m)
.SetGreaterThanZero()
.SetDisplay("ADX Quiet Level", "Maximum ADX value to allow entries", "Indicators");
_sarStep = Param(nameof(SarStep), 0.02m)
.SetGreaterThanZero()
.SetDisplay("SAR Step", "Acceleration step for Parabolic SAR", "Indicators");
_sarMax = Param(nameof(SarMax), 0.2m)
.SetGreaterThanZero()
.SetDisplay("SAR Max", "Maximum acceleration for Parabolic SAR", "Indicators");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Primary timeframe", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_nextDirection = TradeDirections.Buy;
_breakevenActivated = false;
_pipSize = 0m;
_positionDirection = 0;
_exitOrderPending = false;
_entryPrice = 0m;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Pre-calculate pip size respecting fractional pricing conventions.
_pipSize = CalculatePipSize();
// Prepare indicators used for filtering and timing.
_adx = new AverageDirectionalIndex
{
Length = AdxPeriod
};
_sar = new ParabolicSar
{
AccelerationStep = SarStep,
AccelerationMax = SarMax
};
// Subscribe to candle stream and process indicators manually.
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandleManual)
.Start();
// Draw everything on a chart if UI is attached.
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _sar);
DrawIndicator(area, _adx);
DrawOwnTrades(area);
}
}
private void ProcessCandleManual(ICandleMessage candle)
{
// Process only completed candles to stay close to bar-close logic from the EA.
if (candle.State != CandleStates.Finished)
return;
// Process indicators manually to avoid BindEx crash.
var adxValue = _adx.Process(candle);
var sarValue = _sar.Process(candle);
if (!_adx.IsFormed || !_sar.IsFormed)
return;
if (!adxValue.IsFinal || !sarValue.IsFinal)
return;
if (_pipSize <= 0m)
_pipSize = CalculatePipSize();
// Make sure we do not send new commands until exit orders are filled.
if (_exitOrderPending)
{
if (Position == 0)
{
_exitOrderPending = false;
_positionDirection = 0;
_breakevenActivated = false;
}
else
{
return;
}
}
if (Position != 0)
{
var currentDirection = Position > 0 ? 1 : -1;
if (_positionDirection != currentDirection)
{
_positionDirection = currentDirection;
_breakevenActivated = false;
}
// Manage protective logic for the active trade.
ManageOpenPosition(candle);
if (_exitOrderPending || Position != 0)
return;
}
else
{
_positionDirection = 0;
_breakevenActivated = false;
}
if (adxValue is not AverageDirectionalIndexValue adxData)
return;
if (adxData.MovingAverage is not decimal adx)
return;
var sar = sarValue.ToDecimal();
var close = candle.ClosePrice;
var quietMarket = adx < AdxQuietLevel;
// Follow original cmd logic: buy after losses or initialization, sell after profits.
if ((_nextDirection == TradeDirections.Buy || _nextDirection == TradeDirections.None) && quietMarket && close > sar)
{
_breakevenActivated = false;
BuyMarket(Volume + Math.Abs(Position));
_positionDirection = 1;
}
else if (_nextDirection == TradeDirections.Sell && quietMarket && close < sar)
{
_breakevenActivated = false;
SellMarket(Volume + Math.Abs(Position));
_positionDirection = -1;
}
}
private void ManageOpenPosition(ICandleMessage candle)
{
var entryPrice = _entryPrice;
if (entryPrice <= 0m)
return;
var direction = _positionDirection;
var pip = _pipSize <= 0m ? 1m : _pipSize;
if (direction > 0)
{
var stopPrice = StopLossPips > 0 ? entryPrice - StopLossPips * pip : decimal.MinValue;
var takePrice = TakeProfitPips > 0 ? entryPrice + TakeProfitPips * pip : decimal.MaxValue;
// Activate the breakeven flag once price moves far enough in favor.
if (!_breakevenActivated && BreakevenPips > 0)
{
var trigger = entryPrice + BreakevenPips * pip;
if (candle.HighPrice >= trigger)
_breakevenActivated = true;
}
var stopTriggered = (StopLossPips > 0 && candle.LowPrice <= stopPrice) || (_breakevenActivated && candle.LowPrice <= entryPrice);
var takeTriggered = TakeProfitPips > 0 && candle.HighPrice >= takePrice;
// Exit long positions on either stop or target, mirroring the EA logic.
if (stopTriggered || takeTriggered)
{
SellMarket(Position);
_exitOrderPending = true;
UpdateNextDirection(takeTriggered && !stopTriggered, direction);
}
}
else if (direction < 0)
{
var stopPrice = StopLossPips > 0 ? entryPrice + StopLossPips * pip : decimal.MaxValue;
var takePrice = TakeProfitPips > 0 ? entryPrice - TakeProfitPips * pip : decimal.MinValue;
// Activate the breakeven flag once the short trade gains enough.
if (!_breakevenActivated && BreakevenPips > 0)
{
var trigger = entryPrice - BreakevenPips * pip;
if (candle.LowPrice <= trigger)
_breakevenActivated = true;
}
var stopTriggered = (StopLossPips > 0 && candle.HighPrice >= stopPrice) || (_breakevenActivated && candle.HighPrice >= entryPrice);
var takeTriggered = TakeProfitPips > 0 && candle.LowPrice <= takePrice;
// Exit short trades and adjust the direction scheduler.
if (stopTriggered || takeTriggered)
{
BuyMarket(Math.Abs(Position));
_exitOrderPending = true;
UpdateNextDirection(takeTriggered && !stopTriggered, direction);
}
}
}
private void UpdateNextDirection(bool wasProfit, int direction)
{
if (direction > 0)
_nextDirection = wasProfit ? TradeDirections.Sell : TradeDirections.Buy;
else if (direction < 0)
_nextDirection = wasProfit ? TradeDirections.Buy : TradeDirections.Sell;
}
private decimal CalculatePipSize()
{
var security = Security;
if (security == null)
return 1m;
var step = security.PriceStep ?? 1m;
if (step <= 0m)
return 1m;
var decimals = GetDecimalPlaces(step);
if (decimals == 3 || decimals == 5)
return step * 10m;
return step;
}
/// <inheritdoc />
protected override void OnOwnTradeReceived(MyTrade trade)
{
base.OnOwnTradeReceived(trade);
if (trade?.Trade == null) return;
if (Position != 0 && _entryPrice == 0m)
_entryPrice = trade.Trade.Price;
if (Position == 0)
_entryPrice = 0m;
}
private static int GetDecimalPlaces(decimal value)
{
var bits = decimal.GetBits(value);
var scale = (bits[3] >> 16) & 0x7F;
return scale;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import AverageDirectionalIndex, ParabolicSar, CandleIndicatorValue
from StockSharp.Algo.Strategies import Strategy
class trend_me_leave_me_strategy(Strategy):
DIR_NONE = 0
DIR_BUY = 1
DIR_SELL = 2
def __init__(self):
super(trend_me_leave_me_strategy, self).__init__()
self._stop_loss_pips = self.Param("StopLossPips", 50)
self._take_profit_pips = self.Param("TakeProfitPips", 180)
self._breakeven_pips = self.Param("BreakevenPips", 5)
self._adx_period = self.Param("AdxPeriod", 14)
self._adx_quiet_level = self.Param("AdxQuietLevel", 20.0)
self._sar_step = self.Param("SarStep", 0.02)
self._sar_max = self.Param("SarMax", 0.2)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._adx = None
self._sar = None
self._next_direction = self.DIR_BUY
self._breakeven_activated = False
self._pip_size = 0.0
self._position_direction = 0
self._exit_order_pending = False
self._entry_price = 0.0
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@property
def BreakevenPips(self):
return self._breakeven_pips.Value
@property
def AdxPeriod(self):
return self._adx_period.Value
@property
def AdxQuietLevel(self):
return self._adx_quiet_level.Value
@property
def SarStep(self):
return self._sar_step.Value
@property
def SarMax(self):
return self._sar_max.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(trend_me_leave_me_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self._adx = AverageDirectionalIndex()
self._adx.Length = self.AdxPeriod
self._sar = ParabolicSar()
self._sar.AccelerationStep = self.SarStep
self._sar.AccelerationMax = self.SarMax
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, self._sar)
self.DrawIndicator(area, self._adx)
self.DrawOwnTrades(area)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
civ_adx = CandleIndicatorValue(self._adx, candle)
civ_adx.IsFinal = True
adx_value = self._adx.Process(civ_adx)
civ_sar = CandleIndicatorValue(self._sar, candle)
civ_sar.IsFinal = True
sar_value = self._sar.Process(civ_sar)
if not self._adx.IsFormed or not self._sar.IsFormed:
return
if not adx_value.IsFinal or not sar_value.IsFinal:
return
if self._pip_size <= 0:
self._pip_size = self._calculate_pip_size()
pos = float(self.Position)
if self._exit_order_pending:
if pos == 0:
self._exit_order_pending = False
self._position_direction = 0
self._breakeven_activated = False
else:
return
if pos != 0:
current_dir = 1 if pos > 0 else -1
if self._position_direction != current_dir:
self._position_direction = current_dir
self._breakeven_activated = False
self._manage_open_position(candle)
if self._exit_order_pending or float(self.Position) != 0:
return
else:
self._position_direction = 0
self._breakeven_activated = False
try:
ma = adx_value.MovingAverage
if ma is None:
return
adx_ma = float(ma)
except:
try:
adx_ma = float(adx_value.Value)
except:
return
sar = float(sar_value.Value)
close = float(candle.ClosePrice)
quiet_market = adx_ma < float(self.AdxQuietLevel)
if (self._next_direction == self.DIR_BUY or self._next_direction == self.DIR_NONE) and quiet_market and close > sar:
self._breakeven_activated = False
self.BuyMarket(float(self.Volume) + abs(float(self.Position)))
self._position_direction = 1
self._entry_price = close
elif self._next_direction == self.DIR_SELL and quiet_market and close < sar:
self._breakeven_activated = False
self.SellMarket(float(self.Volume) + abs(float(self.Position)))
self._position_direction = -1
self._entry_price = close
def _manage_open_position(self, candle):
entry = self._entry_price
if entry <= 0:
return
direction = self._position_direction
pip = self._pip_size if self._pip_size > 0 else 1.0
if direction > 0:
stop_price = entry - self.StopLossPips * pip if self.StopLossPips > 0 else float('-inf')
take_price = entry + self.TakeProfitPips * pip if self.TakeProfitPips > 0 else float('inf')
if not self._breakeven_activated and self.BreakevenPips > 0:
trigger = entry + self.BreakevenPips * pip
if float(candle.HighPrice) >= trigger:
self._breakeven_activated = True
stop_triggered = (self.StopLossPips > 0 and float(candle.LowPrice) <= stop_price) or (self._breakeven_activated and float(candle.LowPrice) <= entry)
take_triggered = self.TakeProfitPips > 0 and float(candle.HighPrice) >= take_price
if stop_triggered or take_triggered:
self.SellMarket(float(self.Position))
self._exit_order_pending = True
self._update_next_direction(take_triggered and not stop_triggered, direction)
elif direction < 0:
stop_price = entry + self.StopLossPips * pip if self.StopLossPips > 0 else float('inf')
take_price = entry - self.TakeProfitPips * pip if self.TakeProfitPips > 0 else float('-inf')
if not self._breakeven_activated and self.BreakevenPips > 0:
trigger = entry - self.BreakevenPips * pip
if float(candle.LowPrice) <= trigger:
self._breakeven_activated = True
stop_triggered = (self.StopLossPips > 0 and float(candle.HighPrice) >= stop_price) or (self._breakeven_activated and float(candle.HighPrice) >= entry)
take_triggered = self.TakeProfitPips > 0 and float(candle.LowPrice) <= take_price
if stop_triggered or take_triggered:
self.BuyMarket(abs(float(self.Position)))
self._exit_order_pending = True
self._update_next_direction(take_triggered and not stop_triggered, direction)
def _update_next_direction(self, was_profit, direction):
if direction > 0:
self._next_direction = self.DIR_SELL if was_profit else self.DIR_BUY
elif direction < 0:
self._next_direction = self.DIR_BUY if was_profit else self.DIR_SELL
def _calculate_pip_size(self):
sec = self.Security
if sec is None:
return 1.0
step = float(sec.PriceStep) if sec.PriceStep is not None else 1.0
if step <= 0:
return 1.0
decimals = self._get_decimal_places(step)
if decimals == 3 or decimals == 5:
return step * 10.0
return step
def _get_decimal_places(self, value):
s = str(value)
if '.' in s:
return len(s.split('.')[1].rstrip('0'))
return 0
def OnOwnTradeReceived(self, trade):
super(trend_me_leave_me_strategy, self).OnOwnTradeReceived(trade)
if trade is None or trade.Trade is None:
return
pos = float(self.Position)
if pos != 0 and self._entry_price == 0:
self._entry_price = float(trade.Trade.Price)
if pos == 0:
self._entry_price = 0.0
def OnReseted(self):
super(trend_me_leave_me_strategy, self).OnReseted()
self._next_direction = self.DIR_BUY
self._breakeven_activated = False
self._pip_size = 0.0
self._position_direction = 0
self._exit_order_pending = False
self._entry_price = 0.0
def CreateClone(self):
return trend_me_leave_me_strategy()