GitHub で見る

Probe Strategy

Overview

The Probe strategy reproduces the MetaTrader 5 expert advisor "Probe" inside the StockSharp high level framework. It monitors the Commodity Channel Index (CCI) on a configurable timeframe and reacts when the oscillator breaks out of a symmetric channel. When a breakout happens the strategy places a stop order offset from the current market price by a pip based indent. The approach seeks to capture momentum continuation following the breakout while keeping risk limited by pip based protective levels and an adaptive trailing stop.

Trading Logic

  1. Calculate the CCI on the configured candle type.
  2. Track the previous and current CCI values to detect when the indicator exits the lower or upper channel boundary.
  3. When CCI crosses upward through -CCI Channel, submit a buy stop order above the latest close using the Indent (pips) distance.
  4. When CCI crosses downward through +CCI Channel, submit a sell stop order below the latest close using the same pip indent.
  5. Only one pending stop order can remain active at a time. Opposite orders are cancelled and new signals are ignored while an order is active.

Order Management

  • Pending stop orders are withdrawn if the market moves away from the entry price by more than 1.5 * Indent (pips). This mirrors the MetaTrader logic that prevents stale orders from remaining in the book when momentum fades.
  • Once a stop order is filled the strategy stores the executed price as the entry reference. Any opposing pending orders are cancelled immediately.

Risk Management

  • An initial stop loss is derived from Stop Loss (pips) and attached to the active position using internal monitoring. When price touches the stop the position is exited with a market order.
  • Trailing behaviour starts after the floating profit exceeds Trailing Stop (pips) + Trailing Step (pips). The stop is then moved to lock in profits while respecting the minimum trailing distance.
  • All pip based distances automatically adjust for 3 and 5 digit quotes by scaling the exchange tick size.

Parameters

Parameter Description
CandleType Primary timeframe used to build candles and compute the CCI.
CciLength Averaging period of the CCI oscillator.
CciChannelLevel Absolute CCI threshold that forms the symmetric breakout channel.
IndentPips Pip distance added to the last close when placing the pending stop order.
StopLossPips Protective stop loss distance measured in pips.
TrailingStopPips Profit threshold in pips required before the trailing stop activates.
TrailingStepPips Additional profit distance needed before the trailing stop is moved again.

Notes

  • Use the Volume property of the strategy to control the traded size.
  • The strategy is designed for single position netting, matching the original Expert Advisor behaviour.
  • Chart rendering draws candles, the CCI indicator and executed trades when a chart area is available.
using System;
using System.Collections.Generic;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Probe CCI breakout strategy converted from the original MetaTrader 5 expert advisor.
/// Listens for Commodity Channel Index threshold crossovers and enters with market orders.
/// </summary>
public class ProbeStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _cciLength;
	private readonly StrategyParam<decimal> _cciChannelLevel;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;

	private decimal? _previousCci;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal _pipSize;

	public ProbeStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe used for indicator calculations", "General");

		_cciLength = Param(nameof(CciLength), 60)
			.SetGreaterThanZero()
			.SetDisplay("CCI Length", "Averaging period of the Commodity Channel Index", "Indicators");

		_cciChannelLevel = Param(nameof(CciChannelLevel), 120m)
			.SetGreaterThanZero()
			.SetDisplay("CCI Channel", "Absolute CCI level used as the channel boundary", "Indicators");

		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Protective stop loss distance expressed in pips", "Risk");

		_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (pips)", "Minimum profit required before trailing activates", "Risk");

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Additional profit required before the stop is moved again", "Risk");
	}

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

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public int CciLength
	{
		get => _cciLength.Value;
		set => _cciLength.Value = value;
	}

	public decimal CciChannelLevel
	{
		get => _cciChannelLevel.Value;
		set => _cciChannelLevel.Value = value;
	}

	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_previousCci = null;
		_entryPrice = null;
		_stopPrice = null;
		_pipSize = 0m;
	}

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

		_pipSize = CalculatePipSize();
		_previousCci = null;
		_entryPrice = null;
		_stopPrice = null;

		var cci = new CommodityChannelIndex { Length = CciLength };

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

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

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

		if (_pipSize <= 0m)
			_pipSize = CalculatePipSize();

		// Manage existing position (stop loss / trailing)
		var exited = ManagePosition(candle);
		if (exited)
		{
			_previousCci = cciValue;
			return;
		}

		// Check for CCI crossover signals
		if (_previousCci.HasValue && Position == 0m)
		{
			var channel = CciChannelLevel;
			var lower = -channel;

			// CCI crosses up from below -channel -> buy signal
			var crossUp = _previousCci.Value < lower && cciValue > lower;
			// CCI crosses down from above +channel -> sell signal
			var crossDown = _previousCci.Value > channel && cciValue < channel;

			if (crossUp)
			{
				BuyMarket();
				_entryPrice = candle.ClosePrice;

				var stopDistance = StopLossPips * _pipSize;
				_stopPrice = stopDistance > 0m ? candle.ClosePrice - stopDistance : null;
			}
			else if (crossDown)
			{
				SellMarket();
				_entryPrice = candle.ClosePrice;

				var stopDistance = StopLossPips * _pipSize;
				_stopPrice = stopDistance > 0m ? candle.ClosePrice + stopDistance : null;
			}
		}

		_previousCci = cciValue;
	}

	private bool ManagePosition(ICandleMessage candle)
	{
		if (Position == 0m)
		{
			_entryPrice = null;
			_stopPrice = null;
			return false;
		}

		var trailingStop = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;

		if (Position > 0m)
		{
			if (_entryPrice == null)
				_entryPrice = candle.ClosePrice;

			// Check stop loss
			if (_stopPrice.HasValue && candle.LowPrice <= _stopPrice.Value)
			{
				SellMarket();
				ResetTradeState();
				return true;
			}

			// Trailing stop logic
			if (TrailingStopPips > 0m && trailingStop > 0m && _entryPrice.HasValue)
			{
				var profit = candle.ClosePrice - _entryPrice.Value;
				var threshold = trailingStop + trailingStep;

				if (profit > threshold)
				{
					var desiredStop = candle.ClosePrice - trailingStop;
					if (!_stopPrice.HasValue || desiredStop > _stopPrice.Value)
						_stopPrice = desiredStop;
				}
			}
		}
		else if (Position < 0m)
		{
			if (_entryPrice == null)
				_entryPrice = candle.ClosePrice;

			// Check stop loss
			if (_stopPrice.HasValue && candle.HighPrice >= _stopPrice.Value)
			{
				BuyMarket();
				ResetTradeState();
				return true;
			}

			// Trailing stop logic
			if (TrailingStopPips > 0m && trailingStop > 0m && _entryPrice.HasValue)
			{
				var profit = _entryPrice.Value - candle.ClosePrice;
				var threshold = trailingStop + trailingStep;

				if (profit > threshold)
				{
					var desiredStop = candle.ClosePrice + trailingStop;
					if (!_stopPrice.HasValue || desiredStop < _stopPrice.Value)
						_stopPrice = desiredStop;
				}
			}
		}

		return false;
	}

	private void ResetTradeState()
	{
		_entryPrice = null;
		_stopPrice = null;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 0.01m;

		return step;
	}
}