Auf GitHub ansehen

V1N1 Lonny Breakout-Strategie

Überblick

Die V1N1 Lonny Breakout-Strategie repliziert den MetaTrader Expert Advisor "V1N1 LONNY". Sie zielt auf Ausbrüche ab, die rund um die London- und New-York-Sitzungen entstehen, indem eine Eröffnungsrange aufgebaut und auf einen entschiedenen Schlusskurs außerhalb dieser Range gewartet wird. Die Strategie stützt sich auf einen exponentiellen gleitenden Durchschnitt, um den vorherrschenden Trend zu erfassen, und auf einen Stochastik-Oszillator, um überkaufte oder überverkaufte Bedingungen vor dem Markteintritt herauszufiltern.

Ein konfigurierbares Risikomodell ermöglicht die Positionsgrößenbestimmung durch festes Volumen oder als Prozentsatz des Kontokapitals. Die Implementierung beinhaltet auch optionale Spread-Filterung, Trailing-Stops und ein balkenbasiertes Timeout, das den Trade schließt, wenn der Schwung nach einer vordefinierten Anzahl von Kerzen nachlässt.

Handelslogik

  1. Sitzungsausrichtung – Handel ist nur zwischen den konfigurierten Start- und Endzeiten erlaubt. Der Zeitplan kann gemäß Sommerzeit-Regelungen für London oder New York verschoben werden.
  2. Eröffnungsrange – Unmittelbar vor Sitzungsbeginn zeichnet die Strategie die Hochs und Tiefs einer festen Anzahl von Kerzen auf. Diese Range liefert die Ausbruchsniveaus, die während des Handelsfensters verwendet werden.
  3. Trendbestätigung – Die Steigung des exponentiellen gleitenden Durchschnitts (EMA) muss mit der Handelsrichtung übereinstimmen. Ein bullischer Ausbruch erfordert einen steigenden EMA, während ein bärischer Ausbruch einen fallenden EMA erfordert.
  4. Momentum-Filter – Der Stochastik-Oszillator muss innerhalb einer konfigurierbaren Zone um den Mittelpunkt bleiben, um zu vermeiden, dass eingestiegen wird, wenn der Markt bereits überkauft oder überverkauft ist.
  5. Ausbruchsvalidierung – Die vorherige Kerze muss mindestens um den minimalen Ausbruchsabstand über dem Range-Hoch oder -Tief schließen, aber nicht weiter als der maximale Abstand.
  6. Risikokontrollen – Jede Position definiert einen Stop-Loss von der Range-Grenze und ein Take-Profit-Ziel basierend auf einem Faktor dieses Stop-Abstands. Ein Trailing-Stop kann den Ausstieg enger setzen, während der Trade fortschreitet, und Positionen können nach einer bestimmten Anzahl von Kerzen zwangsweise geschlossen werden.

Parameter

Name Beschreibung
StartTrade Sitzungsstartzeit.
EndTrade Sitzungsendzeit.
SwitchDst Sommerzeit-Behandlung: Europa (kein Versatz), USA (relativer Versatz zwischen London und New York) oder deaktiviert.
RiskModes Positionsgrößenmodus (Prozentsatz des Kapitals oder festes Volumen).
PositionRisk Risikoprozentsatz oder festes Volumen, je nach Modus.
TradeRange Anzahl der Kerzen zum Aufbau der Eröffnungsrange.
MinRangePoints / MaxRangePoints Minimale und maximale Größe der Eröffnungsrange in Preispunkten.
MinBreakRange / MaxBreakRange Minimaler und maximaler akzeptabler Ausbruchsabstand über oder unter der Range in Preispunkten.
StopLossPoints Stop-Loss-Abstand gemessen von der gegenüberliegenden Seite der Range in Preispunkten.
TpFactor Take-Profit-Multiplikator angewendet auf den Stop-Loss-Abstand.
TrailStopPoints Optionaler Trailing-Stop-Abstand in Preispunkten. Auf null setzen, um das Trailing zu deaktivieren.
TrendPeriod Periode für den EMA-Steigungsfilter.
OverPeriod Periode für den Stochastik-Oszillator.
OverLevels Abstand von 50, der zur Definition des akzeptablen Stochastik-Bereichs verwendet wird.
BarsToClose Maximale Anzahl von Kerzen, die die Position offen halten. Null deaktiviert das Timeout.
MaxSpreadPoints Maximaler erlaubter Spread in Preispunkten.
SlippagePoints Referenz-Slippage in Preispunkten (zur Kompatibilität mit dem ursprünglichen Expert Advisor beibehalten).
CandleType Von der Strategie verarbeiteter Kerzentyp und Zeitrahmen.

Verwendungshinweise

  • Die Strategie ist für Instrumente mit einem festen Preisschritt konzipiert. Punktbasierte Eingaben werden mit dem PriceStep des Instruments multipliziert, um Preisabstände zu erhalten.
  • Orderbuchdaten werden zur Schätzung des aktuellen Spreads verwendet. Wenn keine besten Bid/Ask-Kurse verfügbar sind, wird die Spread-Filterung übersprungen.
  • Trailing- und Timeout-Ausstiege werden auf geschlossenen Kerzen ausgewertet, entsprechend der ursprünglichen MQL-Logik.
  • Die Positionsgrößenbestimmung erfordert eine Portfoliobewertung (Portfolio.CurrentValue), wenn RiskModes auf Prozentsatz eingestellt ist. Wenn der Wert nicht verfügbar ist, greift die Strategie auf die konfigurierte Losgröße zurück.

Dateien

  • CS/V1n1LonnyBreakoutStrategy.cs – Strategie-Implementierung in C# für StockSharp.
  • README.md – Diese Beschreibung auf Englisch.
  • README_zh.md – 中文简介。
  • README_ru.md – Russische Beschreibung.
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;
	}
}