View on GitHub

Price Impulse Strategy

The Price Impulse strategy scans raw Level1 quotes and reacts to sudden changes between the best bid and best ask. It mirrors the original MetaTrader 5 expert advisor by watching price jumps over a configurable number of ticks and entering the market when the move exceeds a point-based threshold. Protective stop loss and take profit offsets are applied automatically through the high level StartProtection helper.

The approach is market-neutral: a long position opens when the ask price surges upward compared to an older quote, while a short position opens when the bid collapses below its previous value. A configurable cooldown keeps the strategy from re-entering immediately after a trade, just like the MQL implementation that waits for a specified sleep interval.

How it Works

  • Subscribes to Level1 data and stores rolling histories of best bid and best ask prices.
  • Computes the price difference between the latest quote and the quote that arrived HistoryGap ticks earlier (with extra buffering defined by ExtraHistory).
  • Opens a long position when the ask price rises by more than ImpulsePoints * PriceStep and no long exposure exists.
  • Opens a short position when the bid price drops by more than the same threshold and no short exposure exists.
  • Applies fixed take profit and stop loss levels expressed in price points and enforces a CooldownSeconds pause between orders.

Parameters

  • OrderVolume – volume sent with every market order. Defaults to 0.1 lots to match the source robot but can be optimized for other instruments.
  • StopLossPoints – distance from the entry price to the protective stop, measured in instrument points. A value of 0 disables the stop.
  • TakeProfitPoints – distance to the take profit target, also measured in points. A value of 0 disables the target.
  • ImpulsePoints – minimum price impulse, in points, that must be exceeded between the current quote and the quote HistoryGap ticks back to trigger an entry.
  • HistoryGap – number of Level1 updates separating the current price from the comparison baseline. Higher values require larger lookbacks, which smooths noise but delays entries.
  • ExtraHistory – additional Level1 samples retained in the rolling buffer to absorb bursts of quotes when several ticks arrive between callbacks. Keeps the logic consistent with the MT5 implementation that over-samples the history array.
  • CooldownSeconds – minimum waiting time after any trade before another entry can be placed. Ensures the strategy mirrors the MQL expert’s InpSleep parameter and prevents rapid flip-flopping.

Notes

  • The point distance parameters are automatically converted using Security.PriceStep (or Security.MinPriceStep as a fallback), so the same configuration adapts to different tick sizes.
  • Trading only begins once the strategy is online, the history buffers contain enough data, and the impulse condition is satisfied.
  • Because decisions are taken on raw quote updates, the strategy works best on liquid instruments with reliable Level1 feeds.
  • There is no Python port for this strategy. Only the C# version is provided, matching the user request.
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);
	}
}