Ver en GitHub

Estrategia de revendedor de tendencias (API/3858)

Descripción general

TrendScalperStrategy es una conversión de C# del MetaTrader 4 asesor experto Currencyprofits_01_1.mq4. El robot original es un revendedor liviano que sigue tendencias y combina un filtro cruzado EMA/SMA a corto plazo con entradas de ruptura alrededor de los máximos y mínimos más recientes. El puerto StockSharp mantiene las mismas reglas de decisión al tiempo que adopta la suscripción de velas de alto nivel y el canal de indicadores del marco.

Lógica de trading

  1. Indicadores
    • EMA rápida (predeterminado 6) en precios de cierre.
    • Lento SMA (predeterminado 12) en precios de cierre.
    • Máximo más alto (ventana 6 predeterminada) y mínimo más bajo (ventana 6 predeterminada) calculados a partir de los máximos y mínimos de las velas.
  2. Condiciones de entrada
    • Largo: el precio avanza hacia la banda baja reciente (Lowest Low) mientras que el EMA rápido está por encima del SMA lento. La estrategia envía una orden de compra de mercado con el volumen definido por la regla de gestión del dinero.
    • Corto: el precio toca la banda alta reciente (Highest High) mientras que el EMA rápida está por debajo del lento SMA. Una orden de venta de mercado se coloca utilizando el mismo cálculo de volumen.
    • El sistema permanece plano mientras una posición está abierta, reflejando el comportamiento de orden única de la versión MQL.
  3. Condiciones de salida
    • Salida larga: cuando en una posición larga abierta el máximo de la vela supera el Highest High registrado, la posición se cierra en el mercado.
    • Salida corta: cuando una posición corta abierta observa que el mínimo de la vela cae a través del Lowest Low, la posición corta se cubre en el mercado.
    • Se adjunta un stop-loss protector administrado por StartProtection a cada operación cuando StopLossPoints es mayor que cero.

Gestión monetaria

La lógica de tamaño de lote reproduce los tres modos expuestos en el script MQL:

Modo Descripción Comportamiento en el puerto
0 Lotes fijos (LotsIfNoMM). Devuelve el FixedVolume configurado.
<0 Lotes fraccionarios computados a partir del saldo de la cuenta y el factor de riesgo. Calcula ceil(balance * risk / 10000) / 10, con un límite de 100 lotes.
>0 Escalado de lotes completos a partir del equilibrio y el factor de riesgo. Utiliza la misma fórmula base, pero el resultado se redondea al siguiente número entero, con un mínimo de 1 lote y un límite de 100.

El saldo se toma de Portfolio.CurrentValue (regresando a BeginValue). Si el valor de la cartera no está disponible, la estrategia vuelve al volumen fijo, por lo que las órdenes aún se emiten durante las pruebas retrospectivas.

Gestión del riesgo

  • Stop-loss: el parámetro StopLossPoints se expresa en puntos de precio (pips). Durante OnStarted la distancia se multiplica por Security.PriceStep y se pasa a StartProtection, permitiendo a StockSharp mantener la orden de protección.
  • Posición única: la lógica aplica Position == 0 antes de abrir una nueva operación, evitando posiciones superpuestas exactamente como el experto en MT4.

Parámetros

Nombre Predeterminado Descripción
CandleType plazo de 15 minutos Serie de velas utilizadas para cálculos y señales de indicadores.
FastLength 6 Período de la EMA rápida.
SlowLength 12 Periodo de la lentitud SMA.
BreakoutWindow 6 Número de velas inspeccionadas para detectar el filtro de ruptura más alto/más bajo.
FixedVolume 0,1 lotes Volumen cuando la administración del dinero está deshabilitada o se requiere respaldo.
MoneyManagementMode 0 Selecciona entre tamaño de lote fijo, fraccionario o redondeado.
MoneyManagementRisk 40 Multiplicador de factor de riesgo utilizado en el tamaño de lote basado en el equilibrio.
StopLossPoints 50 Distancia de stop-loss en puntos de precio (convertida a precio absoluto antes de llamar a StartProtection).

Notas de implementación

  • El encadenamiento de indicadores se basa en el flujo de trabajo de alto nivel SubscribeCandles().Bind(...); no se requiere almacenamiento en búfer en serie manual.
  • Los comentarios en el código se agregaron en inglés para cumplir con las pautas del repositorio.
  • No se modificaron pruebas unitarias; El foco de esta conversión es la estrategia y la documentación que la acompaña.

Consejos de uso

  • Seleccione un intervalo de vela que coincida con el entorno comercial original (por ejemplo, marcos de tiempo intradiarios cortos para el especulación).
  • Asegúrese de que la cartera tenga un PriceStep válido para que la conversión de stop-loss a precio absoluto funcione correctamente.
  • Ajuste MoneyManagementRisk con cuidado: los valores más altos conducen a posiciones más grandes debido al cálculo ceil(balance * risk / 10000) heredado del experto MQL.

Ventana exacta: los máximos y mínimos proceden de las BreakoutWindow velas anteriores terminadas, excluida la vela actual. Compra: EMA(FastLength) > SMA(SlowLength) y currentLow <= previousWindowLow; venta: tendencia contraria y currentHigh >= previousWindowHigh. El largo se cierra con currentHigh > previousWindowHigh y el corto con currentLow < previousWindowLow. Se exige calentamiento completo. Si StopLossPoints > 0, StartProtection habilita un stop real.

namespace StockSharp.Samples.Strategies;

using System;
using System.Collections.Generic;
using System.Linq;

using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

/// <summary>
/// Fast EMA versus slow SMA direction filtered by a sweep of the previous
/// candle-high/low band. Independent natural exits and a native stop protect fills.
/// </summary>
public class TrendScalperStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _breakoutWindow;
	private readonly StrategyParam<decimal> _fixedVolume;
	private readonly StrategyParam<int> _moneyManagementMode;
	private readonly StrategyParam<decimal> _moneyManagementRisk;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<DataType> _candleType;

	private readonly Queue<decimal> _fastSeed = new();
	private readonly Queue<decimal> _slowCloses = new();
	private readonly Queue<decimal> _highs = new();
	private readonly Queue<decimal> _lows = new();
	private decimal? _fastEma;
	private Order _pendingOrder;

	public int FastLength { get => _fastLength.Value; set => _fastLength.Value = value; }
	public int SlowLength { get => _slowLength.Value; set => _slowLength.Value = value; }
	public int BreakoutWindow { get => _breakoutWindow.Value; set => _breakoutWindow.Value = value; }
	public decimal FixedVolume { get => _fixedVolume.Value; set => _fixedVolume.Value = value; }
	public int MoneyManagementMode { get => _moneyManagementMode.Value; set => _moneyManagementMode.Value = value; }
	public decimal MoneyManagementRisk { get => _moneyManagementRisk.Value; set => _moneyManagementRisk.Value = value; }
	public decimal StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }

	public TrendScalperStrategy()
	{
		_fastLength = Param(nameof(FastLength), 6).SetGreaterThanZero();
		_slowLength = Param(nameof(SlowLength), 12).SetGreaterThanZero();
		_breakoutWindow = Param(nameof(BreakoutWindow), 6).SetGreaterThanZero();
		_fixedVolume = Param(nameof(FixedVolume), 0.1m).SetGreaterThanZero();
		_moneyManagementMode = Param(nameof(MoneyManagementMode), 0);
		_moneyManagementRisk = Param(nameof(MoneyManagementRisk), 40m).SetNotNegative();
		_stopLossPoints = Param(nameof(StopLossPoints), 50m).SetNotNegative();
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame());
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
		=> [(Security, CandleType), (Security, DataType.Level1)];

	protected override void OnReseted()
	{
		base.OnReseted();
		ClearState();
	}

	private void ClearState()
	{
		_fastSeed.Clear();
		_slowCloses.Clear();
		_highs.Clear();
		_lows.Clear();
		_fastEma = null;
		_pendingOrder = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		ClearState();
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			throw new InvalidOperationException("Trend Scalper requires a positive PriceStep.");

		if (StopLossPoints > 0m)
		{
			StartProtection(new Unit(), new Unit(StopLossPoints * step, UnitTypes.Absolute),
				useMarketOrders: true, isLocalStop: true);
			foreach (var field in new[] { Level1Fields.BestBidPrice, Level1Fields.BestAskPrice })
			{
				var quotes = new Subscription(DataType.Level1, Security);
				quotes.MarketData.BuildField = field;
				SubscribeLevel1(quotes).Bind(ObserveQuote).Start();
			}
		}
		SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
	}

	private void ObserveQuote(Level1ChangeMessage message)
	{
		// A Level1 stream is needed for native stop processing between candle closes.
	}

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

		var hasBand = _highs.Count == BreakoutWindow;
		var bandHigh = hasBand ? _highs.Max() : 0m;
		var bandLow = hasBand ? _lows.Min() : 0m;
		_highs.Enqueue(candle.HighPrice);
		_lows.Enqueue(candle.LowPrice);
		while (_highs.Count > BreakoutWindow) _highs.Dequeue();
		while (_lows.Count > BreakoutWindow) _lows.Dequeue();

		var price = candle.ClosePrice;
		if (_fastEma is null)
		{
			_fastSeed.Enqueue(price);
			if (_fastSeed.Count == FastLength)
				_fastEma = _fastSeed.Average();
		}
		else
			_fastEma += (price - _fastEma.Value) * 2m / (FastLength + 1);

		_slowCloses.Enqueue(price);
		while (_slowCloses.Count > SlowLength)
			_slowCloses.Dequeue();
		if (!hasBand || _fastEma is null || _slowCloses.Count < SlowLength ||
			_pendingOrder is not null && _pendingOrder.State is not (OrderStates.Done or OrderStates.Failed))
			return;

		if (Position > 0m && candle.HighPrice > bandHigh)
		{
			Submit(Sides.Sell, Position, "Trend scalper band exit");
			return;
		}
		if (Position < 0m && candle.LowPrice < bandLow)
		{
			Submit(Sides.Buy, Math.Abs(Position), "Trend scalper band exit");
			return;
		}
		if (Position != 0m)
			return;

		var slow = _slowCloses.Average();
		if (_fastEma > slow && candle.LowPrice <= bandLow)
			Enter(Sides.Buy);
		else if (_fastEma < slow && candle.HighPrice >= bandHigh)
			Enter(Sides.Sell);
	}

	private void Enter(Sides side)
	{
		var volume = CalculateOrderVolume();
		if (volume > 0m)
			Submit(side, volume, "Trend scalper entry");
	}

	private decimal CalculateOrderVolume()
	{
		var volume = FixedVolume;
		if (MoneyManagementMode != 0)
		{
			var balance = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			if (balance > 0m)
			{
				var fractional = Math.Ceiling(balance * MoneyManagementRisk / 10000m) / 10m;
				volume = MoneyManagementMode > 0 ? Math.Max(1m, Math.Ceiling(fractional)) : fractional;
				volume = Math.Min(volume, 100m);
			}
		}
		if (Security?.VolumeStep is decimal step && step > 0m)
			volume = decimal.Floor(volume / step) * step;
		if (Security?.MinVolume is decimal min && min > 0m)
			volume = Math.Max(volume, min);
		if (Security?.MaxVolume is decimal max && max > 0m)
			volume = Math.Min(volume, max);
		return volume;
	}

	private void Submit(Sides side, decimal volume, string comment)
	{
		_pendingOrder = new Order
		{
			Security = Security,
			Portfolio = Portfolio,
			Type = OrderTypes.Market,
			Side = side,
			Volume = volume,
			Comment = comment,
		};
		RegisterOrder(_pendingOrder);
	}
}