Auf GitHub ansehen

AIS1 EURUSD Ausbruch-Strategie

Diese Strategie reproduziert den ursprünglichen AIS1 "A System: EURUSD Daily Metrics" Expert Advisor unter Verwendung von StockSharp's High-Level-API. Sie handelt EURUSD-Ausbrüche, indem sie die aktuelle Kursbewegung mit der Range des Vortages vergleicht und Trades mit adaptiver Positionsgröße plus einem Vier-Stunden-Trailing-Stop verwaltet.

Strategieüberblick

  • Markt: EURUSD Spot/CFD/Forex-Instrumente.
  • Primärer Zeitrahmen: Tageskerzen liefern das Referenzhoch, -tief und den Schlusskurs.
  • Sekundärer Zeitrahmen: 4-Stunden-Kerzen steuern Trailing-Stop-Updates und Einstiegsprüfungen.
  • Richtung: Long- und Short-Trades sind erlaubt.
  • Stil: Ausbruchsfortsetzung mit volatilitätsskalierten Zielen und Stops.

Handelslogik

  1. Die vorherige abgeschlossene Tageskerze verfolgen. Mittelpunkt, Range und abgeleitete Stop-/Take-Abstände mit konfigurierbaren Multiplikatoren (StopFactor, TakeFactor) berechnen.
  2. Jede abgeschlossene 4-Stunden-Kerze auswerten:
    • Long-Einstieg: Vorheriger Tagesschluss liegt über dem Mittelpunkt und das 4-Stunden-Hoch bricht über das vorherige Tageshoch.
    • Short-Einstieg: Vorheriger Tagesschluss liegt unter dem Mittelpunkt und das 4-Stunden-Tief bricht unter das vorherige Tagestief.
  3. Die Positionsgröße wird aus dem aktuellen Portfolio-Eigenkapital und dem konfigurierten Risikoanteil (OrderReserve) ermittelt. Das Volumen wird auf Instrument-Handelsschritte gerundet.
  4. Für offene Positionen wendet die Strategie drei Ebenen der Ausgangssteuerung an:
    • Fester Stop-Loss auf der gegenüberliegenden Seite der Tagesrange, skaliert mit StopFactor.
    • Fester Take-Profit bei einer Distanz von TakeFactor × Tagesrange.
    • Dynamischer Trailing-Stop unter Verwendung der vorherigen 4-Stunden-Range multipliziert mit TrailFactor. Der Trailing-Stop aktiviert sich erst, nachdem der Trade in Gewinn geht.
  5. Eine Fünf-Sekunden-Abkühlung nach jedem Trade oder Ausstieg spiegelt das ursprüngliche EA-Verhalten wider und verhindert schnelle Modifikationen.

Risikomanagement

  • OrderReserve definiert den Anteil des aktuellen Eigenkapitals, der beim nächsten Trade riskiert werden kann. Wenn die berechnete Größe unter dem Instrument-Minimum liegt, wird der Trade übersprungen.
  • AccountReserve verfolgt das Spitzen-Eigenkapital und stoppt das Öffnen oder Verwalten von Trades, sobald der Eigenkapital-Drawdown AccountReserve - OrderReserve überschreitet (16% mit Standard-Eingaben).
  • Trailing-Ausstiege und feste Ziele stellen sicher, dass Positionen geschlossen werden, auch wenn neue Trades durch den Drawdown-Guard blockiert sind.

Parameter

Parameter Beschreibung
AccountReserve Anteil des Eigenkapitals, der vom Handel ausgeschlossen ist, zur Berechnung des erlaubten Drawdowns vor der Handelspause.
OrderReserve Eigenkapitalanteil, der pro Trade riskiert wird. Bestimmt den maximalen Verlust unter Verwendung des Stop-Abstands.
TakeFactor Multiplikator, der auf die vorherige Tagesrange angewendet wird, um den Take-Profit-Abstand festzulegen.
StopFactor Multiplikator, der auf die vorherige Tagesrange angewendet wird, um den Stop-Loss-Abstand festzulegen.
TrailFactor Multiplikator, der auf die vorherige 4-Stunden-Range angewendet wird, um den Trailing-Stop zu bewegen, sobald die Position profitabel ist.
EntryCandleType Kerzentyp (standardmäßig täglich) für Ausbruchslevels.
TrailCandleType Kerzentyp (standardmäßig 4 Stunden) für Intraday-Auswertung und Trailing.

Hinweise zur Konvertierung

  • Die StockSharp-Version löst Einstiege und Trailing-Updates bei abgeschlossenen 4-Stunden-Kerzen aus. Der ursprüngliche MQL Expert Advisor reagierte auf jeden Tick; die Verwendung von Kerzen hält die Logik robust innerhalb der High-Level-API.
  • Stop-Loss, Take-Profit und Trailing-Ausstiege werden mit Market Orders ausgeführt, wenn die jeweiligen Preisniveaus innerhalb der verarbeiteten Kerze berührt werden.
  • Margin-Prüfungen aus der MQL-Version werden durch eigenkapitalbasiertes Sizing ersetzt, um plattformneutral zu bleiben und gleichzeitig die ursprünglichen Risikobeschränkungen zu respektieren.
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>
/// Daily breakout strategy converted from the AIS1 expert advisor.
/// Tracks previous day levels, applies a risk based position size and manages trailing exits.
/// </summary>
public class Ais1EurUsdBreakoutStrategy : Strategy
{
	private readonly StrategyParam<decimal> _accountReserve;
	private readonly StrategyParam<decimal> _orderReserve;
	private readonly StrategyParam<decimal> _takeFactor;
	private readonly StrategyParam<decimal> _stopFactor;
	private readonly StrategyParam<decimal> _trailFactor;
	private readonly StrategyParam<DataType> _entryCandleType;
	private readonly StrategyParam<DataType> _trailCandleType;

	private decimal _prevDayHigh;
	private decimal _prevDayLow;
	private decimal _prevDayClose;
	private decimal _prevTrailRange;
	private bool _hasPrevDay;
	private bool _hasPrevTrail;
	private decimal _entryPrice;
	private decimal _longStop;
	private decimal _longTake;
	private decimal _shortStop;
	private decimal _shortTake;
	private decimal _longTrail;
	private decimal _shortTrail;
	private decimal _maxEquity;
	private DateTimeOffset _nextActionTime;

	private static readonly TimeSpan Cooldown = TimeSpan.FromSeconds(5);

	public decimal AccountReserve
	{
		get => _accountReserve.Value;
		set => _accountReserve.Value = value;
	}

	public decimal OrderReserve
	{
		get => _orderReserve.Value;
		set => _orderReserve.Value = value;
	}

	public decimal TakeFactor
	{
		get => _takeFactor.Value;
		set => _takeFactor.Value = value;
	}

	public decimal StopFactor
	{
		get => _stopFactor.Value;
		set => _stopFactor.Value = value;
	}

	public decimal TrailFactor
	{
		get => _trailFactor.Value;
		set => _trailFactor.Value = value;
	}

	public DataType EntryCandleType
	{
		get => _entryCandleType.Value;
		set => _entryCandleType.Value = value;
	}

	public DataType TrailCandleType
	{
		get => _trailCandleType.Value;
		set => _trailCandleType.Value = value;
	}

	public Ais1EurUsdBreakoutStrategy()
	{
		_accountReserve = Param(nameof(AccountReserve), 0.2m)
			.SetDisplay("Account Reserve", "Equity share kept outside of trading", "Risk")
			;

		_orderReserve = Param(nameof(OrderReserve), 0.04m)
			.SetDisplay("Order Reserve", "Equity share risked per trade", "Risk")
			.SetGreaterThanZero()
			;

		_takeFactor = Param(nameof(TakeFactor), 0.8m)
			.SetDisplay("Take Factor", "Daily range multiplier for take profit", "Targets")
			.SetGreaterThanZero()
			;

		_stopFactor = Param(nameof(StopFactor), 1m)
			.SetDisplay("Stop Factor", "Daily range multiplier for stop loss", "Targets")
			.SetGreaterThanZero()
			;

		_trailFactor = Param(nameof(TrailFactor), 5m)
			.SetDisplay("Trail Factor", "Intraday range multiplier for trailing", "Targets")
			.SetGreaterThanZero()
			;

		_entryCandleType = Param(nameof(EntryCandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Entry Candle", "Primary timeframe for breakout levels", "Data");

		_trailCandleType = Param(nameof(TrailCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Trail Candle", "Secondary timeframe for trailing", "Data");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		if (Security is null)
			yield break;

		yield return (Security, EntryCandleType);

		if (TrailCandleType != EntryCandleType)
			yield return (Security, TrailCandleType);
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		ResetPositionState();
		_prevDayHigh = 0m;
		_prevDayLow = 0m;
		_prevDayClose = 0m;
		_prevTrailRange = 0m;
		_hasPrevDay = false;
		_hasPrevTrail = false;
		_maxEquity = 0m;
		_nextActionTime = DateTimeOffset.MinValue;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		ResetPositionState();
		_maxEquity = GetEquity();
		_nextActionTime = DateTimeOffset.MinValue;

		var dailySubscription = SubscribeCandles(EntryCandleType);
		dailySubscription.Bind(ProcessDailyCandle).Start();

		var intradaySubscription = SubscribeCandles(TrailCandleType);
		intradaySubscription.Bind(ProcessIntradayCandle).Start();
	}

	private void ProcessDailyCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Store the latest completed day to use as breakout reference on the next session.
		_prevDayHigh = candle.HighPrice;
		_prevDayLow = candle.LowPrice;
		_prevDayClose = candle.ClosePrice;
		_hasPrevDay = true;
	}

	private void ProcessIntradayCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		// Respect the original EA cooldown before issuing another order modification.
		if (candle.CloseTime <= _nextActionTime)
		{
			UpdateTrailRange(candle);
			return;
		}

		var equity = GetEquity();
		UpdateMaxEquity(equity);

		if (IsDrawdownBreached(equity))
		{
			UpdateTrailRange(candle);
			return;
		}

		if (!_hasPrevDay)
		{
			UpdateTrailRange(candle);
			return;
		}

		var dayRange = _prevDayHigh - _prevDayLow;
		if (dayRange <= 0m)
		{
			UpdateTrailRange(candle);
			return;
		}

		var average = (_prevDayHigh + _prevDayLow) / 2m;
		var takeDistance = dayRange * TakeFactor;
		var stopDistance = dayRange * StopFactor;

		var trailRange = _hasPrevTrail ? _prevTrailRange : candle.HighPrice - candle.LowPrice;
		var trailDistance = trailRange * TrailFactor;

		if (Position != 0m)
		{
			HandleExistingPosition(candle, trailDistance);
			UpdateTrailRange(candle);
			return;
		}

		TryEnterPosition(candle, average, stopDistance, takeDistance);
		UpdateTrailRange(candle);
	}

	private void HandleExistingPosition(ICandleMessage candle, decimal trailDistance)
	{
		if (Position > 0m)
		{
			var exitVolume = Math.Abs(Position);

			// Respect take profit first so gains are locked immediately.
			if (_longTake > 0m && candle.HighPrice >= _longTake)
			{
				SellMarket();
				ResetAfterExit(candle.CloseTime);
				return;
			}

			var trailingStop = _longStop;

			// Update trailing stop only after the trade moves into profit.
			if (trailDistance > 0m && candle.ClosePrice > _entryPrice)
			{
				var candidate = candle.ClosePrice - trailDistance;
				if (_longTrail == 0m || candidate > _longTrail)
					_longTrail = candidate;
			}

			if (_longTrail > 0m)
				trailingStop = trailingStop > 0m ? Math.Max(trailingStop, _longTrail) : _longTrail;

			if (trailingStop > 0m && candle.LowPrice <= trailingStop)
			{
				SellMarket();
				ResetAfterExit(candle.CloseTime);
			}
		}
		else if (Position < 0m)
		{
			var exitVolume = Math.Abs(Position);

			if (_shortTake > 0m && candle.LowPrice <= _shortTake)
			{
				BuyMarket();
				ResetAfterExit(candle.CloseTime);
				return;
			}

			var trailingStop = _shortStop;

			if (trailDistance > 0m && candle.ClosePrice < _entryPrice)
			{
				var candidate = candle.ClosePrice + trailDistance;
				if (_shortTrail == 0m || candidate < _shortTrail)
					_shortTrail = candidate;
			}

			if (_shortTrail > 0m)
				trailingStop = trailingStop > 0m ? Math.Min(trailingStop, _shortTrail) : _shortTrail;

			if (trailingStop > 0m && candle.HighPrice >= trailingStop)
			{
				BuyMarket();
				ResetAfterExit(candle.CloseTime);
			}
		}
	}

	private void TryEnterPosition(ICandleMessage candle, decimal average, decimal stopDistance, decimal takeDistance)
	{
		var breakoutUp = _prevDayClose > average && candle.HighPrice > _prevDayHigh;
		var breakoutDown = _prevDayClose < average && candle.LowPrice < _prevDayLow;

		if (breakoutUp)
		{
			var entryPrice = candle.ClosePrice;
			var stopPrice = _prevDayHigh - stopDistance;
			var risk = entryPrice - stopPrice;
			if (risk <= 0m)
				return;

			var volume = CalculatePositionSize(risk);
			if (volume <= 0m)
				return;

			BuyMarket();

			_entryPrice = entryPrice;
			_longStop = stopPrice;
			_longTake = entryPrice + takeDistance;
			_longTrail = 0m;
			_shortStop = 0m;
			_shortTake = 0m;
			_shortTrail = 0m;
			_nextActionTime = candle.CloseTime + Cooldown;
		}
		else if (breakoutDown)
		{
			var entryPrice = candle.ClosePrice;
			var stopPrice = _prevDayLow + stopDistance;
			var risk = stopPrice - entryPrice;
			if (risk <= 0m)
				return;

			var volume = CalculatePositionSize(risk);
			if (volume <= 0m)
				return;

			SellMarket();

			_entryPrice = entryPrice;
			_shortStop = stopPrice;
			_shortTake = entryPrice - takeDistance;
			_shortTrail = 0m;
			_longStop = 0m;
			_longTake = 0m;
			_longTrail = 0m;
			_nextActionTime = candle.CloseTime + Cooldown;
		}
	}

	private decimal CalculatePositionSize(decimal riskPerUnit)
	{
		if (riskPerUnit <= 0m)
			return 0m;

		var equity = GetEquity();
		if (equity <= 0m)
			return 0m;

		var maxRisk = equity * OrderReserve;
		if (maxRisk <= 0m)
			return 0m;

		var rawSize = maxRisk / riskPerUnit;
		if (rawSize <= 0m)
			return 0m;

		var step = Security?.VolumeStep ?? 1m;
		var minVolume = Security?.MinVolume ?? step;
		var maxVolume = Security?.MaxVolume ?? Math.Max(minVolume, step * 1000m);

		var steps = Math.Floor(rawSize / step);
		var volume = steps * step;

		if (volume < minVolume)
		{
			if (rawSize >= minVolume)
				volume = minVolume;
			else
				return 0m;
		}

		if (volume > maxVolume)
			volume = maxVolume;

		return volume;
	}

	private void UpdateTrailRange(ICandleMessage candle)
	{
		_prevTrailRange = candle.HighPrice - candle.LowPrice;
		_hasPrevTrail = true;
	}

	private void ResetAfterExit(DateTimeOffset time)
	{
		ResetPositionState();
		_nextActionTime = time + Cooldown;
	}

	private void ResetPositionState()
	{
		_entryPrice = 0m;
		_longStop = 0m;
		_longTake = 0m;
		_shortStop = 0m;
		_shortTake = 0m;
		_longTrail = 0m;
		_shortTrail = 0m;
	}

	private void UpdateMaxEquity(decimal equity)
	{
		if (equity > _maxEquity)
			_maxEquity = equity;
	}

	private bool IsDrawdownBreached(decimal equity)
	{
		if (_maxEquity <= 0m)
			return false;

		var drawdownLimit = AccountReserve - OrderReserve;
		if (drawdownLimit <= 0m)
			return false;

		var threshold = _maxEquity * (1m - drawdownLimit);
		return equity < threshold;
	}

	private decimal GetEquity()
	{
		return Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
	}
}