Ver en GitHub

Estrategia de Rompimiento AIS1 EURUSD

Esta estrategia reproduce el asesor experto original AIS1 "A System: EURUSD Daily Metrics" usando la API de alto nivel de StockSharp. Opera rompimientos de EURUSD comparando la acción del precio actual con el rango del día anterior y gestiona las operaciones con dimensionamiento adaptativo de posición más un trailing stop de cuatro horas.

Descripción general de la estrategia

  • Mercado: Instrumentos spot/CFD/forex de EURUSD.
  • Marco temporal primario: Las velas diarias proporcionan el máximo, mínimo y cierre de referencia.
  • Marco temporal secundario: Las velas de 4 horas impulsan las actualizaciones del trailing stop y las verificaciones de entrada.
  • Dirección: Se permiten operaciones largas y cortas.
  • Estilo: Continuación de rompimiento con objetivos y stops escalados por volatilidad.

Lógica de trading

  1. Rastrear la vela diaria anterior completada. Calcular el punto medio, el rango y las distancias derivadas de stop/take usando multiplicadores configurables (StopFactor, TakeFactor).
  2. Evaluar cada vela de 4 horas completada:
    • Entrada larga: El cierre diario anterior está por encima del punto medio y el máximo de 4 horas rompe por encima del máximo diario anterior.
    • Entrada corta: El cierre diario anterior está por debajo del punto medio y el mínimo de 4 horas rompe por debajo del mínimo diario anterior.
  3. El tamaño de la posición se determina a partir del patrimonio actual del portafolio y la participación de riesgo configurada (OrderReserve). El volumen se redondea a los pasos de trading del instrumento.
  4. Para las posiciones abiertas, la estrategia aplica tres capas de control de salida:
    • Stop-loss fijo en el lado opuesto del rango diario escalado por StopFactor.
    • Take-profit fijo a una distancia de TakeFactor × rango diario.
    • Trailing stop dinámico usando el rango de 4 horas anterior multiplicado por TrailFactor. El trailing stop se activa solo después de que la operación se mueve en ganancia.
  5. Un período de espera de cinco segundos después de cualquier operación o salida refleja el comportamiento del EA original y previene modificaciones rápidas.

Gestión del riesgo

  • OrderReserve define la fracción del patrimonio actual que puede arriesgarse en la próxima operación. Si el tamaño calculado está por debajo del mínimo del instrumento, la operación se omite.
  • AccountReserve rastrea el patrimonio máximo y deja de abrir o gestionar operaciones una vez que el drawdown del patrimonio supera AccountReserve - OrderReserve (16% con los parámetros predeterminados).
  • Las salidas de trailing y los objetivos fijos aseguran que las posiciones se cierren incluso si las nuevas operaciones están bloqueadas por el guard de drawdown.

Parámetros

Parámetro Descripción
AccountReserve Porción del patrimonio excluida del trading, utilizada para calcular el drawdown permitido antes de que el trading se pause.
OrderReserve Participación del patrimonio arriesgada por operación. Determina la pérdida máxima usando la distancia del stop.
TakeFactor Multiplicador aplicado al rango diario anterior para establecer la distancia del take-profit.
StopFactor Multiplicador aplicado al rango diario anterior para establecer la distancia del stop-loss.
TrailFactor Multiplicador aplicado al rango de 4 horas anterior para mover el trailing stop una vez que la posición es rentable.
EntryCandleType Tipo de vela (diaria por defecto) usada para los niveles de rompimiento.
TrailCandleType Tipo de vela (4 horas por defecto) usada para evaluación intradiaria y trailing.

Notas sobre la conversión

  • La versión de StockSharp activa las entradas y actualizaciones de trailing en velas de 4 horas completadas. El asesor experto MQL original reaccionaba a cada tick; usar velas mantiene la lógica robusta dentro de la API de alto nivel.
  • Stop-loss, take-profit y salidas de trailing se ejecutan con órdenes de mercado cuando los respectivos niveles de precio son tocados dentro de la vela procesada.
  • Las verificaciones de margen de la versión MQL se reemplazan con dimensionamiento basado en patrimonio para permanecer neutral a la plataforma mientras se respetan las restricciones de riesgo originales.
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;
	}
}