Стратегия V1N1 Lonny Breakout повторяет логику эксперта MetaTrader "V1N1 LONNY", ориентированного на прорывы во время перекрытия лондонской и нью-йоркской сессий. Перед началом торговли формируется ценовой диапазон из нескольких свечей, после чего стратегия ожидает закрытия свечи за пределами диапазона. Дополнительные фильтры: экспоненциальная скользящая средняя для оценки тренда и стохастик для контроля перекупленности/перепроданности.
Настройки риска позволяют рассчитывать объём позиции как фиксированное значение либо как процент от капитала. Также реализованы фильтр по спреду, плавающий стоп-лосс и закрытие сделки по истечении заданного числа свечей при отсутствии развития импульса.
Логика торговли
Торговые часы. Сделки разрешены только между заданными временем начала и окончания. Время может корректироваться с учётом перехода на летнее время в Лондоне или Нью-Йорке.
Диапазон открытия. Перед стартом сессии запоминаются максимумы и минимумы фиксированного количества свечей. Этот диапазон служит базой для определения пробоев.
Подтверждение тренда. EMA должна наклоняться в сторону предполагаемой сделки: для покупок — рост, для продаж — снижение.
Фильтр импульса. Значение стохастика должно находиться в заданном диапазоне вокруг 50, чтобы избежать входа при крайних значениях индикатора.
Валидация пробоя. Предыдущая свеча должна закрыться за пределами диапазона на расстояние не меньше минимального и не больше максимального порога.
Управление риском. Стоп-лосс рассчитывается от противоположной границы диапазона, тейк-профит — как произведение дистанции стопа на коэффициент. Доступны трейлинг-стоп и принудительное закрытие по количеству свечей.
Параметры
StartTrade — время начала торговли.
EndTrade — время завершения торговли.
SwitchDst — режим учёта летнего времени (Европа/США/без сдвига).
RiskModes — способ расчёта объёма (процент капитала или фиксированный объём).
PositionRisk — процент риска либо фиксированный объём в зависимости от режима.
TradeRange — число свечей для построения диапазона.
MinRangePoints / MaxRangePoints — допустимый размер диапазона в пунктах.
MinBreakRange / MaxBreakRange — минимальная и максимальная величина пробоя в пунктах.
StopLossPoints — расстояние стоп-лосса в пунктах.
TpFactor — коэффициент для расчёта тейк-профита от стоп-лосса.
TrailStopPoints — размер трейлинг-стопа (0 — отключено).
TrendPeriod — период EMA.
OverPeriod — период стохастика.
OverLevels — допустимое отклонение стохастика от уровня 50.
BarsToClose — максимальное количество свечей в позиции (0 — без ограничения).
MaxSpreadPoints — максимальный допустимый спред.
SlippagePoints — ориентир по проскальзыванию (для совместимости с оригиналом).
CandleType — используемый тип и таймфрейм свечей.
Рекомендации по использованию
Все параметры, заданные в пунктах, преобразуются в ценовые дистанции через PriceStep инструмента.
Для контроля спреда стратегия подписывается на стакан заявок. При отсутствии котировок фильтр не применяется.
Трейлинг-стоп и закрытие по количеству свечей проверяются только на закрытых барах, что соответствует оригинальной реализации в MQL.
При выборе расчёта по проценту капитала требуется значение Portfolio.CurrentValue. Если оно недоступно, используется минимальный доступный объём.
Файлы
CS/V1n1LonnyBreakoutStrategy.cs — реализация стратегии на C# для StockSharp.
README.md — описание на английском языке.
README_zh.md — описание на китайском языке.
README_ru.md — описание на русском языке.
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;
/// <summary>
/// Breakout strategy that mirrors the original "V1N1 LONNY" MQL expert advisor.
/// The strategy forms an opening range from early candles and
/// enters when a candle closes outside that range while trend and momentum filters agree.
/// </summary>
public class V1n1LonnyBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _trendPeriod;
private readonly StrategyParam<int> _rsiPeriod;
private readonly StrategyParam<int> _rangeBars;
private readonly StrategyParam<DataType> _candleType;
private ExponentialMovingAverage _ema;
private RelativeStrengthIndex _rsi;
private decimal _prevEma;
private decimal _prevPrevEma;
private bool _hasPrevEma;
private bool _hasPrevPrevEma;
private readonly List<decimal> _highs = new();
private readonly List<decimal> _lows = new();
private bool _rangeReady;
private decimal _rangeHigh;
private decimal _rangeLow;
private bool _breakoutUpSeen;
private bool _breakoutDownSeen;
/// <summary>
/// EMA period for the trend filter.
/// </summary>
public int TrendPeriod
{
get => _trendPeriod.Value;
set => _trendPeriod.Value = value;
}
/// <summary>
/// RSI period for momentum filter.
/// </summary>
public int RsiPeriod
{
get => _rsiPeriod.Value;
set => _rsiPeriod.Value = value;
}
/// <summary>
/// Number of initial bars to build the opening range.
/// </summary>
public int RangeBars
{
get => _rangeBars.Value;
set => _rangeBars.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="V1n1LonnyBreakoutStrategy"/> class.
/// </summary>
public V1n1LonnyBreakoutStrategy()
{
_trendPeriod = Param(nameof(TrendPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Trend EMA", "EMA period for trend filter", "Indicators");
_rsiPeriod = Param(nameof(RsiPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("RSI Period", "RSI period for momentum filter", "Indicators");
_rangeBars = Param(nameof(RangeBars), 5)
.SetGreaterThanZero()
.SetDisplay("Range Bars", "Bars used to build the opening range", "Breakout");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle type and timeframe", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_ema = null;
_rsi = null;
_prevEma = 0m;
_prevPrevEma = 0m;
_hasPrevEma = false;
_hasPrevPrevEma = false;
_highs.Clear();
_lows.Clear();
_rangeReady = false;
_rangeHigh = 0m;
_rangeLow = 0m;
_breakoutUpSeen = false;
_breakoutDownSeen = false;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ema = new ExponentialMovingAverage { Length = TrendPeriod };
_rsi = new RelativeStrengthIndex { Length = RsiPeriod };
Indicators.Add(_ema);
Indicators.Add(_rsi);
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_ema, ProcessCandle)
.Start();
StartProtection(
takeProfit: new Unit(2, UnitTypes.Percent),
stopLoss: new Unit(1, UnitTypes.Percent));
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _ema);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal emaValue)
{
if (candle.State != CandleStates.Finished)
return;
var rsiResult = _rsi.Process(new DecimalIndicatorValue(_rsi, candle.ClosePrice, candle.OpenTime) { IsFinal = true });
if (!_rsi.IsFormed || !_ema.IsFormed)
{
ShiftEma(emaValue);
return;
}
var rsiValue = rsiResult.ToDecimal();
// Build the opening range from the first N bars
if (!_rangeReady)
{
_highs.Add(candle.HighPrice);
_lows.Add(candle.LowPrice);
if (_highs.Count >= RangeBars)
{
_rangeHigh = decimal.MinValue;
_rangeLow = decimal.MaxValue;
for (var i = 0; i < _highs.Count; i++)
{
if (_highs[i] > _rangeHigh) _rangeHigh = _highs[i];
if (_lows[i] < _rangeLow) _rangeLow = _lows[i];
}
_rangeReady = true;
}
ShiftEma(emaValue);
return;
}
if (Position != 0 || !_hasPrevEma || !_hasPrevPrevEma)
{
ShiftEma(emaValue);
return;
}
// Trend rising: EMA going up
var trendUp = _prevEma > _prevPrevEma;
var trendDown = _prevEma < _prevPrevEma;
// Long: close above range high + trend up + RSI not overbought
if (!_breakoutUpSeen && trendUp && rsiValue < 70 && candle.ClosePrice > _rangeHigh)
{
BuyMarket();
_breakoutUpSeen = true;
}
// Short: close below range low + trend down + RSI not oversold
else if (!_breakoutDownSeen && trendDown && rsiValue > 30 && candle.ClosePrice < _rangeLow)
{
SellMarket();
_breakoutDownSeen = true;
}
ShiftEma(emaValue);
}
private void ShiftEma(decimal emaValue)
{
if (_hasPrevEma)
{
_prevPrevEma = _prevEma;
_hasPrevPrevEma = true;
}
_prevEma = emaValue;
_hasPrevEma = true;
}
}
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, Decimal
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Indicators import ExponentialMovingAverage, RelativeStrengthIndex
from StockSharp.Algo.Strategies import Strategy
from indicator_extensions import *
class v1n1_lonny_breakout_strategy(Strategy):
def __init__(self):
super(v1n1_lonny_breakout_strategy, self).__init__()
self._trend_period = self.Param("TrendPeriod", 20)
self._rsi_period = self.Param("RsiPeriod", 14)
self._range_bars = self.Param("RangeBars", 5)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._prev_ema = 0.0
self._prev_prev_ema = 0.0
self._has_prev_ema = False
self._has_prev_prev_ema = False
self._highs = []
self._lows = []
self._range_ready = False
self._range_high = 0.0
self._range_low = 0.0
self._breakout_up_seen = False
self._breakout_down_seen = False
@property
def TrendPeriod(self):
return self._trend_period.Value
@TrendPeriod.setter
def TrendPeriod(self, value):
self._trend_period.Value = value
@property
def RsiPeriod(self):
return self._rsi_period.Value
@RsiPeriod.setter
def RsiPeriod(self, value):
self._rsi_period.Value = value
@property
def RangeBars(self):
return self._range_bars.Value
@RangeBars.setter
def RangeBars(self, value):
self._range_bars.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(v1n1_lonny_breakout_strategy, self).OnStarted2(time)
self._prev_ema = 0.0
self._prev_prev_ema = 0.0
self._has_prev_ema = False
self._has_prev_prev_ema = False
self._highs = []
self._lows = []
self._range_ready = False
self._range_high = 0.0
self._range_low = 0.0
self._breakout_up_seen = False
self._breakout_down_seen = False
ema = ExponentialMovingAverage()
ema.Length = self.TrendPeriod
self._ema = ema
self._rsi = RelativeStrengthIndex()
self._rsi.Length = self.RsiPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(ema, self.ProcessCandle).Start()
self.StartProtection(
takeProfit=Unit(2, UnitTypes.Percent),
stopLoss=Unit(1, UnitTypes.Percent))
def ProcessCandle(self, candle, ema_value):
if candle.State != CandleStates.Finished:
return
ema_val = float(ema_value)
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
rsi_result = process_float(self._rsi, candle.ClosePrice, candle.OpenTime, True)
if not self._rsi.IsFormed or not self._ema.IsFormed:
self._shift_ema(ema_val)
return
rsi_val = float(rsi_result)
if not self._range_ready:
self._highs.append(high)
self._lows.append(low)
if len(self._highs) >= int(self.RangeBars):
self._range_high = max(self._highs)
self._range_low = min(self._lows)
self._range_ready = True
self._shift_ema(ema_val)
return
if self.Position != 0 or not self._has_prev_ema or not self._has_prev_prev_ema:
self._shift_ema(ema_val)
return
trend_up = self._prev_ema > self._prev_prev_ema
trend_down = self._prev_ema < self._prev_prev_ema
if not self._breakout_up_seen and trend_up and rsi_val < 70.0 and close > self._range_high:
self.BuyMarket()
self._breakout_up_seen = True
elif not self._breakout_down_seen and trend_down and rsi_val > 30.0 and close < self._range_low:
self.SellMarket()
self._breakout_down_seen = True
self._shift_ema(ema_val)
def _shift_ema(self, ema_val):
if self._has_prev_ema:
self._prev_prev_ema = self._prev_ema
self._has_prev_prev_ema = True
self._prev_ema = ema_val
self._has_prev_ema = True
def OnReseted(self):
super(v1n1_lonny_breakout_strategy, self).OnReseted()
self._prev_ema = 0.0
self._prev_prev_ema = 0.0
self._has_prev_ema = False
self._has_prev_prev_ema = False
self._highs = []
self._lows = []
self._range_ready = False
self._range_high = 0.0
self._range_low = 0.0
self._breakout_up_seen = False
self._breakout_down_seen = False
def CreateClone(self):
return v1n1_lonny_breakout_strategy()