Ver en GitHub

Estrategia de Impulso de Precio

La estrategia de Impulso de Precio analiza cotizaciones Level1 sin procesar y reacciona a los cambios repentinos entre el mejor bid y el mejor ask. Replica el asesor experto original de MetaTrader 5 al observar saltos de precio durante un número configurable de ticks y entrando al mercado cuando el movimiento supera un umbral expresado en puntos. Los niveles de stop loss protector y take profit se aplican automáticamente a través del asistente de alto nivel StartProtection.

El enfoque es neutral al mercado: se abre una posición larga cuando el precio ask sube con respecto a una cotización anterior, mientras que se abre una posición corta cuando el bid cae por debajo de su valor previo. Un período de enfriamiento configurable evita que la estrategia vuelva a entrar inmediatamente después de una operación, igual que la implementación MQL que espera un intervalo de pausa especificado.

Cómo Funciona

  • Se suscribe a datos Level1 y almacena historiales continuos de los mejores precios bid y ask.
  • Calcula la diferencia de precio entre la última cotización y la cotización recibida HistoryGap ticks antes (con un búfer adicional definido por ExtraHistory).
  • Abre una posición larga cuando el precio ask sube más de ImpulsePoints * PriceStep y no existe exposición larga.
  • Abre una posición corta cuando el precio bid cae más del mismo umbral y no existe exposición corta.
  • Aplica niveles fijos de take profit y stop loss expresados en puntos de precio y aplica una pausa de CooldownSeconds entre órdenes.

Parámetros

  • OrderVolume – volumen enviado con cada orden de mercado. Por defecto es 0.1 lotes para coincidir con el robot fuente, pero puede optimizarse para otros instrumentos.
  • StopLossPoints – distancia desde el precio de entrada hasta el stop protector, medida en puntos del instrumento. Un valor de 0 deshabilita el stop.
  • TakeProfitPoints – distancia hasta el objetivo de take profit, también medida en puntos. Un valor de 0 deshabilita el objetivo.
  • ImpulsePoints – impulso de precio mínimo, en puntos, que debe superarse entre la cotización actual y la cotización HistoryGap ticks atrás para activar una entrada.
  • HistoryGap – número de actualizaciones Level1 que separan el precio actual de la línea base de comparación. Valores más altos requieren miradas hacia atrás más largas, lo que suaviza el ruido pero retrasa las entradas.
  • ExtraHistory – muestras Level1 adicionales retenidas en el búfer continuo para absorber ráfagas de cotizaciones cuando varios ticks llegan entre callbacks. Mantiene la lógica coherente con la implementación MT5 que sobre-muestrea el array de historial.
  • CooldownSeconds – tiempo mínimo de espera después de cualquier operación antes de poder colocar otra entrada. Garantiza que la estrategia refleje el parámetro InpSleep del experto MQL y evita reversiones rápidas.

Notas

  • Los parámetros de distancia en puntos se convierten automáticamente usando Security.PriceStep (o Security.MinPriceStep como respaldo), por lo que la misma configuración se adapta a diferentes tamaños de tick.
  • El trading solo comienza cuando la estrategia está en línea, los búferes de historial contienen suficientes datos y se cumple la condición de impulso.
  • Dado que las decisiones se toman en actualizaciones de cotizaciones en bruto, la estrategia funciona mejor en instrumentos líquidos con feeds Level1 confiables.
  • No existe versión en Python para esta estrategia. Solo se proporciona la versión en C#, cumpliendo con la solicitud del usuario.
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>
/// Price impulse strategy that trades on rapid price moves using candle close prices.
/// </summary>
public class PriceImpulseStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _impulsePoints;
	private readonly StrategyParam<int> _historyGap;
	private readonly StrategyParam<int> _extraHistory;
	private readonly StrategyParam<int> _cooldownSeconds;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _priceHistory = [];

	private decimal _tickSize;
	private DateTimeOffset? _lastTradeTime;
	private decimal? _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;

	/// <summary>
	/// Volume used for each market order.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in price points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take profit distance expressed in price points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Minimum impulse measured in price points to trigger a trade.
	/// </summary>
	public int ImpulsePoints
	{
		get => _impulsePoints.Value;
		set => _impulsePoints.Value = value;
	}

	/// <summary>
	/// Number of candles between price comparisons.
	/// </summary>
	public int HistoryGap
	{
		get => _historyGap.Value;
		set => _historyGap.Value = value;
	}

	/// <summary>
	/// Additional samples kept in the rolling buffer.
	/// </summary>
	public int ExtraHistory
	{
		get => _extraHistory.Value;
		set => _extraHistory.Value = value;
	}

	/// <summary>
	/// Minimum number of seconds between two trades.
	/// </summary>
	public int CooldownSeconds
	{
		get => _cooldownSeconds.Value;
		set => _cooldownSeconds.Value = value;
	}

	/// <summary>
	/// Candle type used for price monitoring.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	private int HistoryCapacity => Math.Max(HistoryGap + ExtraHistory + 1, HistoryGap + 1);

	/// <summary>
	/// Initializes strategy parameters with sensible defaults.
	/// </summary>
	public PriceImpulseStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetDisplay("Order Volume", "Volume used for each market order", "Trading")
			.SetGreaterThanZero()
			.SetOptimize(0.1m, 2m, 0.1m);

		_stopLossPoints = Param(nameof(StopLossPoints), 150)
			.SetDisplay("Stop Loss Points", "Stop loss distance expressed in price points", "Risk")
			.SetNotNegative()
			.SetOptimize(50, 300, 50);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 50)
			.SetDisplay("Take Profit Points", "Take profit distance expressed in price points", "Risk")
			.SetNotNegative()
			.SetOptimize(10, 200, 10);

		_impulsePoints = Param(nameof(ImpulsePoints), 15)
			.SetDisplay("Impulse Points", "Minimum price impulse required to trade", "Signals")
			.SetGreaterThanZero()
			.SetOptimize(5, 40, 5);

		_historyGap = Param(nameof(HistoryGap), 15)
			.SetDisplay("Gap Candles", "Number of candles between comparison points", "Signals")
			.SetNotNegative()
			.SetOptimize(5, 40, 5);

		_extraHistory = Param(nameof(ExtraHistory), 15)
			.SetDisplay("Extra History", "Additional samples kept to absorb bursts", "Signals")
			.SetNotNegative()
			.SetOptimize(0, 30, 5);

		_cooldownSeconds = Param(nameof(CooldownSeconds), 100)
			.SetDisplay("Cooldown Seconds", "Minimum number of seconds between trades", "Risk")
			.SetNotNegative()
			.SetOptimize(0, 300, 20);

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle type for price tracking", "General");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_priceHistory.Clear();
		_tickSize = 0m;
		_lastTradeTime = null;
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

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

		_tickSize = Security?.PriceStep ?? 1m;
		if (_tickSize <= 0)
			_tickSize = 1m;

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(ProcessCandle).Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

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

		var currentPrice = candle.ClosePrice;
		_priceHistory.Add(currentPrice);

		var capacity = HistoryCapacity;
		while (_priceHistory.Count > capacity)
			_priceHistory.RemoveAt(0);

		var candleTime = candle.CloseTime;

		// Check SL/TP for existing positions.
		if (Position > 0)
		{
			var stopLossPrice = _stopLossPrice;
			var takeProfitPrice = _takeProfitPrice;

			if (stopLossPrice is decimal longStop && candle.LowPrice <= longStop)
			{ SellMarket(Position); _entryPrice = null; _stopLossPrice = null; _takeProfitPrice = null; return; }
			if (takeProfitPrice is decimal longTake && candle.HighPrice >= longTake)
			{ SellMarket(Position); _entryPrice = null; _stopLossPrice = null; _takeProfitPrice = null; return; }
		}
		else if (Position < 0)
		{
			var stopLossPrice = _stopLossPrice;
			var takeProfitPrice = _takeProfitPrice;

			if (stopLossPrice is decimal shortStop && candle.HighPrice >= shortStop)
			{ BuyMarket(Math.Abs(Position)); _entryPrice = null; _stopLossPrice = null; _takeProfitPrice = null; return; }
			if (takeProfitPrice is decimal shortTake && candle.LowPrice <= shortTake)
			{ BuyMarket(Math.Abs(Position)); _entryPrice = null; _stopLossPrice = null; _takeProfitPrice = null; return; }
		}

		if (_priceHistory.Count <= HistoryGap)
			return;

		var impulseThreshold = ImpulsePoints * _tickSize;
		var lastIndex = _priceHistory.Count - 1;
		var compareIndex = lastIndex - HistoryGap;
		if (compareIndex < 0) return;

		var comparisonPrice = _priceHistory[compareIndex];
		var upImpulse = currentPrice - comparisonPrice;
		var downImpulse = comparisonPrice - currentPrice;

		if (upImpulse > impulseThreshold && Position <= 0 && IsCooldownPassed(candleTime))
		{
			BuyMarket(OrderVolume);
			_entryPrice = currentPrice;
			_stopLossPrice = StopLossPoints > 0 ? currentPrice - StopLossPoints * _tickSize : null;
			_takeProfitPrice = TakeProfitPoints > 0 ? currentPrice + TakeProfitPoints * _tickSize : null;
			_lastTradeTime = candleTime;
			return;
		}

		if (downImpulse > impulseThreshold && Position >= 0 && IsCooldownPassed(candleTime))
		{
			SellMarket(OrderVolume);
			_entryPrice = currentPrice;
			_stopLossPrice = StopLossPoints > 0 ? currentPrice + StopLossPoints * _tickSize : null;
			_takeProfitPrice = TakeProfitPoints > 0 ? currentPrice - TakeProfitPoints * _tickSize : null;
			_lastTradeTime = candleTime;
		}
	}

	private bool IsCooldownPassed(DateTimeOffset time)
	{
		if (_lastTradeTime is null)
			return true;

		var cooldownSeconds = CooldownSeconds;
		if (cooldownSeconds <= 0)
			return true;

		return time - _lastTradeTime.Value >= TimeSpan.FromSeconds(cooldownSeconds);
	}
}