Ver en GitHub

Estrategia de Dynamic Averaging

Descripción general

Dynamic Averaging es un port directo del asesor experto de MetaTrader 5 "Dynamic averaging.mq5" (id 23319). La estrategia combina un oscilador Stochastic rápido con un filtro de volatilidad basado en la desviación estándar. Las operaciones solo se permiten mientras la volatilidad del mercado permanece por debajo de su promedio móvil, forzando las entradas durante consolidaciones donde los reversos del Stochastic son más fiables.

Parámetros

  • TradeVolume – tamaño de orden para cada nueva entrada. Se duplica automáticamente tras una secuencia perdedora y se restablece tras una ganadora.
  • MinimumProfit – beneficio flotante (en moneda de cuenta) que cierra todas las posiciones abiertas una vez superado.
  • SlidingWindowDays – número de días calendario utilizados para promediar los valores de desviación estándar y construir la línea base de volatilidad.
  • StochasticKPeriod – número de barras para el cálculo de %K.
  • StochasticDPeriod – longitud de suavizado para la línea %D.
  • StochasticSlowPeriod – período de ralentización final para el oscilador Stochastic.
  • StdDevPeriod – período de retrovisión para el indicador de desviación estándar.
  • CandleType – velas fuente para los cálculos (por defecto marco temporal de 15 minutos).

Reglas de negociación

  1. La estrategia opera solo con velas terminadas. Al cierre de cada barra los filtros de Stochastic y volatilidad se actualizan mediante SubscribeCandles().BindEx.
  2. Calcular la volatilidad del mercado usando StandardDeviation(StdDevPeriod) y compararla con la volatilidad promedio calculada por SimpleMovingAverage sobre las últimas SlidingWindowDays barras.
  3. Si la desviación estándar actual está por encima del promedio móvil, se salta la barra.
  4. Cuando la volatilidad es baja:
    • Entrar largo si %K está por debajo de 25 y la pendiente de los dos valores de %K anteriores es positiva (último valor menos el valor hace dos barras).
    • Entrar corto si %K está por encima de 75 y la pendiente de los dos valores de %K anteriores es negativa.
  5. Las posiciones se revierten enviando suficiente volumen para aplanar el lado opuesto más la nueva exposición de TradeVolume.
  6. Cuando el PnL flotante de la posición abierta supera MinimumProfit, la estrategia sale inmediatamente del mercado.

Dimensionamiento de posición y recuperación

  • El tamaño de orden inicial es igual a TradeVolume.
  • Después de cerrar la posición, se inspecciona el cambio de PnL realizado.
    • Una pérdida duplica el tamaño de la próxima operación (paso martingala) para replicar el comportamiento del EA original.
    • Una ganancia o breakeven restablece el tamaño al TradeVolume base.

Detalles de implementación

  • Las velas, los valores de Stochastic y desviación estándar se procesan a través de la API de alto nivel con BindEx, evitando la gestión manual de buffers.
  • La ventana deslizante de volatilidad convierte días calendario en conteos de barras usando el marco temporal de velas si está disponible.
  • El control de beneficio flotante se basa en el cierre de la vela actual y PositionAvgPrice, coincidiendo con la implementación MQL que suma solo el beneficio de posición abierta.
  • Todos los comentarios de código están escritos en inglés; no se proporciona versión en Python según los requisitos de la tarea.
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;

public class DynamicAveragingStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	public int FastPeriod { get => _fastPeriod.Value; set => _fastPeriod.Value = value; }
	public int SlowPeriod { get => _slowPeriod.Value; set => _slowPeriod.Value = value; }
	public int StopLossPoints { get => _stopLossPoints.Value; set => _stopLossPoints.Value = value; }
	public int TakeProfitPoints { get => _takeProfitPoints.Value; set => _takeProfitPoints.Value = value; }

	public DynamicAveragingStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 14).SetGreaterThanZero().SetDisplay("Fast Period", "Fast EMA period", "Indicator");
		_slowPeriod = Param(nameof(SlowPeriod), 50).SetGreaterThanZero().SetDisplay("Slow Period", "Slow EMA period", "Indicator");
		_stopLossPoints = Param(nameof(StopLossPoints), 200).SetNotNegative().SetDisplay("Stop Loss", "Stop-loss in price steps", "Risk");
		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400).SetNotNegative().SetDisplay("Take Profit", "Take-profit in price steps", "Risk");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	protected override void OnReseted()
	{
		base.OnReseted();
		_fast = null; _slow = null;
		_prevFast = 0; _prevSlow = 0; _entryPrice = 0; _cooldown = 0;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };
		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

	private void ProcessCandle(ICandleMessage candle, decimal fastValue, decimal slowValue)
	{
		if (candle.State != CandleStates.Finished) return;
		if (!_fast.IsFormed || !_slow.IsFormed) { _prevFast = fastValue; _prevSlow = slowValue; return; }
		if (_cooldown > 0) { _cooldown--; _prevFast = fastValue; _prevSlow = slowValue; return; }

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step) { SellMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step) { BuyMarket(); _entryPrice = 0; _cooldown = 100; _prevFast = fastValue; _prevSlow = slowValue; return; }
		}

		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{ if (Position < 0) BuyMarket(); BuyMarket(); _entryPrice = close; _cooldown = 100; }
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{ if (Position > 0) SellMarket(); SellMarket(); _entryPrice = close; _cooldown = 100; }

		_prevFast = fastValue; _prevSlow = slowValue;
	}
}