Ver en GitHub

Price Extreme Strategy

Overview

The Price Extreme Strategy replicates the MetaTrader expert adviser Price_Extreme_Strategy using the StockSharp high-level API. The system monitors a sliding channel derived from the highest high and lowest low over a configurable number of completed candles. Breakout signals are generated whenever the selected reference candle closes above the upper boundary or below the lower boundary. The logic can optionally be inverted to transform breakout conditions into fade entries.

This conversion keeps the trading workflow event-driven. Orders are submitted immediately after the close of each finished candle, matching the behaviour of the original MQL algorithm that reacted on the opening tick of the next bar.

Indicator Logic

The Price Extreme channel is rebuilt on every finished candle using StockSharp's Highest and Lowest indicators:

  • Highest tracks the maximum high over the last N candles.
  • Lowest tracks the minimum low over the last N candles.

These buffers emulate the Price_Extreme_Indicator custom study bundled with the original expert adviser. The indicator length is exposed through the Level Length parameter.

A separate Signal Shift parameter defines which closed candle is used to evaluate the breakout condition. A shift of 1 means "use the candle that just closed" (default). Larger values allow waiting for additional confirmation by referencing older bars.

Trading Rules

  1. Recalculate upper and lower channel values for every finished candle.
  2. Retrieve the candle specified by Signal Shift from the internal history buffer.
  3. Generate directional intents:
    • Breakout up: the candle's close is above the upper channel value.
    • Breakout down: the candle's close is below the lower channel value.
  4. Apply optional inversion with Reverse Signals:
    • If disabled, trade in the breakout direction (buy on breakout up, sell on breakout down).
    • If enabled, swap the reactions (sell on breakout up, buy on breakout down).
  5. Respect Enable Long and Enable Short permissions before submitting orders.
  6. Automatically close any opposite position before opening a new trade so that only one net position exists at any time.

Risk Management

The strategy provides stop-loss and take-profit handling that mirrors the point-based controls of the MQL version:

  • Stop Loss and Take Profit are expressed in price steps (Security.PriceStep).
  • Target prices are recalculated whenever the net position size changes.
  • If a finished candle overlaps the protective levels (low below the stop for longs, high above the stop for shorts, etc.), the position is closed via market order and the protective targets are cleared.
  • StartProtection() is enabled during OnStarted to leverage built-in StockSharp safeguards.

Parameters

Parameter Description Default Group
LevelLength Number of completed candles considered when computing the extreme channel. 5 Indicator
SignalShift Index of the closed candle used for breakout validation (1 = last closed candle). 1 Indicator
EnableLong Allows buying when true. true Trading
EnableShort Allows selling when true. true Trading
ReverseSignals Inverts breakout reactions (buy on breakdown, sell on breakout). false Trading
OrderVolume Volume sent with each market order. Must be greater than zero. 1 Trading
StopLossPoints Stop-loss distance measured in price steps. A value of 0 disables the stop. 0 Risk
TakeProfitPoints Take-profit distance measured in price steps. A value of 0 disables the target. 0 Risk
CandleType Primary timeframe for data subscription. 5-minute candles Data

All parameters use StrategyParam<T> with UI metadata so they can be optimized or modified from the Designer.

Usage Guidelines

  1. Attach the strategy to a security and set the Candle Type to match the timeframe used in the original MetaTrader setup.
  2. Adjust Level Length if a wider or narrower Price Extreme channel is desired.
  3. Configure Signal Shift to control how many closed candles to wait before evaluating the breakout.
  4. Select desired trade directions via Enable Long, Enable Short, and Reverse Signals`.
  5. Define Order Volume, Stop Loss, and Take Profit to match risk preferences. Remember that both protective values operate in price steps.
  6. Launch the strategy. Candles, indicator bands, and executed trades are plotted automatically when a chart area is available.

Additional Notes

  • The strategy intentionally operates on a single net position, mirroring the hedging logic of the MQL expert by flattening the opposite side before entering a new trade.
  • Protective stops and targets are evaluated on completed candles. When live trading, this approximates the server-side protective orders used by the original script.
  • No Python port is included, as requested.
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>
/// Breakout strategy based on the Price Extreme indicator.
/// </summary>
public class PriceExtremeStrategy : Strategy
{
	private readonly StrategyParam<int> _levelLength;
	private readonly StrategyParam<int> _signalShift;
	private readonly StrategyParam<bool> _enableLong;
	private readonly StrategyParam<bool> _enableShort;
	private readonly StrategyParam<bool> _reverseSignals;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<ICandleMessage> _history = new();
	private readonly List<decimal> _highs = new();
	private readonly List<decimal> _lows = new();
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _prevPosition;
	private decimal _entryPrice;
	private decimal _prevUpper;
	private decimal _prevLower;

	/// <summary>
	/// Number of candles used to build extreme levels.
	/// </summary>
	public int LevelLength
	{
		get => _levelLength.Value;
		set => _levelLength.Value = value;
	}

	/// <summary>
	/// Shift in candles used for the breakout signal.
	/// </summary>
	public int SignalShift
	{
		get => _signalShift.Value;
		set => _signalShift.Value = value;
	}

	/// <summary>
	/// Enable long trades.
	/// </summary>
	public bool EnableLong
	{
		get => _enableLong.Value;
		set => _enableLong.Value = value;
	}

	/// <summary>
	/// Enable short trades.
	/// </summary>
	public bool EnableShort
	{
		get => _enableShort.Value;
		set => _enableShort.Value = value;
	}

	/// <summary>
	/// Reverse long and short signals.
	/// </summary>
	public bool ReverseSignals
	{
		get => _reverseSignals.Value;
		set => _reverseSignals.Value = value;
	}

	/// <summary>
	/// Order volume in lots.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

	/// <summary>
	/// Type of candles used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes <see cref="PriceExtremeStrategy"/>.
	/// </summary>
	public PriceExtremeStrategy()
	{
		_levelLength = Param(nameof(LevelLength), 20)
			.SetGreaterThanZero()
			.SetDisplay("Level Length", "Number of candles for price extremes", "Indicator")
			
			.SetOptimize(3, 30, 1);

		_signalShift = Param(nameof(SignalShift), 1)
			.SetGreaterThanZero()
			.SetDisplay("Signal Shift", "Closed candles used for breakout", "Indicator");

		_enableLong = Param(nameof(EnableLong), true)
			.SetDisplay("Enable Long", "Allow buying trades", "Trading");

		_enableShort = Param(nameof(EnableShort), true)
			.SetDisplay("Enable Short", "Allow selling trades", "Trading");

		_reverseSignals = Param(nameof(ReverseSignals), false)
			.SetDisplay("Reverse Signals", "Invert breakout direction", "Trading");

		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Volume sent with market orders", "Trading");

		_stopLossPoints = Param(nameof(StopLossPoints), 0)
			.SetDisplay("Stop Loss", "Protective stop in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 0)
			.SetDisplay("Take Profit", "Profit target in price steps", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe", "Data");
	}

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

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

		_history.Clear();
		_highs.Clear();
		_lows.Clear();
		_prevUpper = 0m;
		_prevLower = 0m;
		_entryPrice = 0m;
		_prevPosition = 0m;
		ResetTargets();
	}

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

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

		StartProtection(
			takeProfit: new Unit(3, UnitTypes.Percent),
			stopLoss: new Unit(2, UnitTypes.Percent));

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

	private bool CanOpenLong => EnableLong && OrderVolume > 0m;
	private bool CanOpenShort => EnableShort && OrderVolume > 0m;

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

		_highs.Add(candle.HighPrice);
		_lows.Add(candle.LowPrice);
		_history.Add(candle);

		var maxHistory = Math.Max(LevelLength + SignalShift + 2, 10);
		if (_history.Count > maxHistory)
		{
			var removeCount = _history.Count - maxHistory;
			_history.RemoveRange(0, removeCount);
			_highs.RemoveRange(0, removeCount);
			_lows.RemoveRange(0, removeCount);
		}

		if (_highs.Count < LevelLength)
			return;

		var upper = decimal.MinValue;
		var lower = decimal.MaxValue;
		for (var i = _highs.Count - LevelLength; i < _highs.Count; i++)
		{
			if (_highs[i] > upper) upper = _highs[i];
			if (_lows[i] < lower) lower = _lows[i];
		}

		if (_history.Count < SignalShift)
			return;

		var signalCandle = _history[_history.Count - SignalShift];

		var breakoutUp = candle.ClosePrice > _prevUpper && _prevUpper > 0;
		var breakoutDown = candle.ClosePrice < _prevLower && _prevLower > 0;

		_prevUpper = upper;
		_prevLower = lower;

		var wantLong = ReverseSignals ? breakoutDown : breakoutUp;
		var wantShort = ReverseSignals ? breakoutUp : breakoutDown;

		if (wantLong && CanOpenLong && Position == 0)
		{
			BuyMarket();
		}
		else if (wantShort && CanOpenShort && Position == 0)
		{
			SellMarket();
		}
	}

	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);
		if (trade?.Trade == null) return;
		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;
		if (Position == 0)
			_entryPrice = 0m;
	}

	private void UpdateTargets()
	{
		_stopPrice = null;
		_takePrice = null;

		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m || Position == 0m)
			return;

		if (Position > 0m)
		{
			if (StopLossPoints > 0)
				_stopPrice = _entryPrice - StopLossPoints * step;

			if (TakeProfitPoints > 0)
				_takePrice = _entryPrice + TakeProfitPoints * step;
		}
		else if (Position < 0m)
		{
			if (StopLossPoints > 0)
				_stopPrice = _entryPrice + StopLossPoints * step;

			if (TakeProfitPoints > 0)
				_takePrice = _entryPrice - TakeProfitPoints * step;
		}
	}

	private void ApplyRiskManagement(ICandleMessage candle)
	{
		if (Position > 0m)
		{
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
				return;
			}

			if (_takePrice.HasValue && candle.HighPrice >= _takePrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
			}
		}
		else if (Position < 0m)
		{
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
				return;
			}

			if (_takePrice.HasValue && candle.LowPrice <= _takePrice.Value)
			{
				if (Position > 0) SellMarket(Position); else if (Position < 0) BuyMarket(-Position);
				ResetTargets();
			}
		}
	}

	private void ResetTargets()
	{
		_stopPrice = null;
		_takePrice = null;
		_prevPosition = Position;
	}
}