Auf GitHub ansehen

Autotrade Pending Stops Strategy

Overview

This strategy is a C# conversion of the MetaTrader expert advisor Autotrade (barabashkakvn's edition). It continuously maintains two symmetric stop entry orders around the current market price. Whenever the market remains flat and no position is open, the strategy refreshes both pending orders. When a stop order fills, the position is actively monitored: exits are triggered once the price action stabilizes or when an absolute profit/loss threshold is met. The implementation uses the high-level StockSharp API as required by the project guidelines.

Mapping from the original inputs

StockSharp parameter MQL5 input Description
IndentTicks InpIndent Distance (in price steps) between the current price and the stop entry orders.
MinProfit MinProfit Minimum floating profit (account currency) needed to exit during a quiet market phase.
ExpirationMinutes ExpirationMinutes Lifetime of the pending stop orders before they are cancelled and recreated.
AbsoluteFixation AbsoluteFixation Absolute profit or loss level (currency) that forces the position to close.
StabilizationTicks InpStabilization Maximum size of the previous candle body that is treated as a consolidation zone.
OrderVolume Lots Volume used for both the buy stop and the sell stop orders.
CandleType Period() Candle series that drives the logic (default 1-minute time frame).

All numerical inputs that represent price distances are converted from "points" to actual price steps through the Security.PriceStep value. Profit-based thresholds are calculated using Security.StepPrice, which mirrors the MQL profit calculations that operate in the deposit currency.

Trading logic

Pending order deployment

  1. The strategy reacts only to finished candles (CandleStates.Finished).
  2. The very first candle is used to seed historical data (previous open/close) and immediately schedule pending orders.
  3. When no position is open, any inactive references are cleared and:
    • A buy stop is placed at Close + IndentTicks * PriceStep.
    • A sell stop is placed at Close - IndentTicks * PriceStep.
  4. Each pending order receives an expiration timestamp equal to CloseTime + ExpirationMinutes minutes. When that time is reached the order is cancelled and recreated on the next candle.

Position management

  1. Once either stop order is executed, the opposite pending order is cancelled to avoid unwanted hedging on the netting-based StockSharp account model.
  2. The strategy keeps the previous candle body (|Open - Close|) to detect quiet market conditions.
  3. For every candle with an open position:
    • Unrealized profit is estimated in currency using the price difference versus PositionAvgPrice, scaled by Security.PriceStep and Security.StepPrice.
    • If the profit exceeds MinProfit and the previous candle body is below StabilizationTicks * PriceStep, the position is closed at market.
    • Regardless of stabilization, if the absolute profit or loss exceeds AbsoluteFixation, the position is also closed at market.
  4. Whenever the position returns to flat, all remaining pending orders are cleared.

Additional behaviors

  • Only one position is allowed at a time; order volumes are netted using OrderVolume.
  • Because StockSharp does not expose bid/ask during backtests in the same way as MetaTrader, the close price of the completed candle is used as the reference level for new stop orders.
  • The strategy automatically refreshes the cached Volume value whenever OrderVolume is adjusted via parameters or optimization.

Implementation notes and differences

  • Profit calculations rely on Security.PriceStep and Security.StepPrice. Ensure these fields are filled in the instrument metadata; otherwise the default value 1 is used as a fallback.
  • The original MQL version allowed temporary hedging (multiple orders in opposite directions). The StockSharp port cancels the unused stop immediately after a fill to comply with the platform's netting model.
  • Pending order expiration uses the candle's CloseTime. If historical data lacks close timestamps, adjust the feed to provide them or extend the code accordingly.
  • The strategy works with any candle data type by adjusting CandleType. Default candles are time-frame based (TimeSpan.FromMinutes(1).TimeFrame()).

Usage recommendations

  1. Configure the candle series that matches the chart period used in MetaTrader.
  2. Set IndentTicks, StabilizationTicks, and profit thresholds in relation to the instrument's tick size and tick value.
  3. Verify that the portfolio uses hedging or netting as desired. The strategy assumes netting and will flat the book before rearming stop orders.
  4. Use the provided parameters for optimization in StockSharp Designer or Backtester to adapt the behaviour to different markets.
  5. Monitor the log output: the code relies on finished candles and market availability (IsFormedAndOnlineAndAllowTrading()) before it submits new orders.

Risk disclaimer

Automated trading involves substantial risk. Backtest thoroughly, validate the parameters on historical data, and confirm broker-specific requirements (such as minimum distances for stop orders) before deploying the strategy on a live account.

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>
/// Conversion of the MQL Autotrade strategy that places symmetric stop orders around the market.
/// Pending stop entries are refreshed on every candle while no position is open.
/// Positions are closed when the market calms down or when absolute profit/loss thresholds are reached.
/// </summary>
public class AutotradePendingStopsStrategy : Strategy
{
	private readonly StrategyParam<int> _indentTicks;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<int> _expirationMinutes;
	private readonly StrategyParam<decimal> _absoluteFixation;
	private readonly StrategyParam<int> _stabilizationTicks;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevOpen;
	private decimal _prevClose;
	private bool _hasPrevCandle;

	private decimal _tickSize = 1m;
	private decimal _tickValue = 1m;

	/// <summary>
	/// Distance in price steps from the current market to the pending stop entries.
	/// </summary>
	public int IndentTicks
	{
		get => _indentTicks.Value;
		set => _indentTicks.Value = value;
	}

	/// <summary>
	/// Minimal profit in account currency required to exit when price action stabilizes.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// Lifetime of pending stop orders in minutes.
	/// </summary>
	public int ExpirationMinutes
	{
		get => _expirationMinutes.Value;
		set => _expirationMinutes.Value = value;
	}

	/// <summary>
	/// Absolute profit or loss that forces the position to close.
	/// </summary>
	public decimal AbsoluteFixation
	{
		get => _absoluteFixation.Value;
		set => _absoluteFixation.Value = value;
	}

	/// <summary>
	/// Maximum size of the previous candle body that is treated as consolidation.
	/// </summary>
	public int StabilizationTicks
	{
		get => _stabilizationTicks.Value;
		set => _stabilizationTicks.Value = value;
	}

	/// <summary>
	/// Order volume used for entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set
		{
			_orderVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Candle type used to drive the strategy logic.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public AutotradePendingStopsStrategy()
	{
		_indentTicks = Param(nameof(IndentTicks), 200)
		.SetGreaterThanZero()
		.SetDisplay("Indent Ticks", "Distance in ticks between price and pending stop orders", "Entries");

		_minProfit = Param(nameof(MinProfit), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Min Profit", "Minimum profit to close during low volatility", "Risk");

		_expirationMinutes = Param(nameof(ExpirationMinutes), 41)
		.SetGreaterThanZero()
		.SetDisplay("Order Expiration", "Lifetime of pending stops in minutes", "Entries");

		_absoluteFixation = Param(nameof(AbsoluteFixation), 43m)
		.SetGreaterThanZero()
		.SetDisplay("Absolute Fixation", "Profit or loss in currency that forces exit", "Risk");

		_stabilizationTicks = Param(nameof(StabilizationTicks), 25)
		.SetGreaterThanZero()
		.SetDisplay("Stabilization Ticks", "Maximum candle body considered as flat market", "Exits");

		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Default volume for both stop orders", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Time frame that drives order refresh", "General");

		Volume = _orderVolume.Value;
	}

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

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

		// Reset runtime state when the strategy is reloaded.
		_prevOpen = 0m;
		_prevClose = 0m;
		_hasPrevCandle = false;
		_entryPrice = 0m;
	}

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

		Volume = _orderVolume.Value;

		// Cache price step and tick value for fast profit calculations.
		_tickSize = Security.PriceStep ?? 1m;
		_tickValue = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? _tickSize;

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Only act on completed candles to stay aligned with the original MQL logic.
		if (candle.State != CandleStates.Finished)
		return;

		if (!_hasPrevCandle)
		{
			// Store the first candle so that stabilization checks have history.
			_prevOpen = candle.OpenPrice;
			_prevClose = candle.ClosePrice;
			_hasPrevCandle = true;

			EnsurePendingOrders(candle);
			return;
		}

		UpdatePendingOrdersLifetime(candle);

		if (Position == 0)
		{
			// Refresh pending orders as soon as the market is flat.
			EnsurePendingOrders(candle);
		}
		else
		{
			// Manage the active position and close it when required.
			ManageOpenPosition(candle);
		}

		// Keep the previous candle body for stabilization checks on the next bar.
		_prevOpen = candle.OpenPrice;
		_prevClose = candle.ClosePrice;
	}

	private decimal _entryPrice;

	private void EnsurePendingOrders(ICandleMessage candle)
	{
		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		var indent = IndentTicks * _tickSize;
		var buyPrice = candle.ClosePrice + indent;
		var sellPrice = candle.ClosePrice - indent;

		// Simulate stop-order breakout: if high breaches buy level, go long
		if (candle.HighPrice >= buyPrice && Position <= 0)
		{
			if (Position < 0)
				BuyMarket(Math.Abs(Position));
			BuyMarket(OrderVolume);
			_entryPrice = buyPrice;
		}
		// if low breaches sell level, go short
		else if (candle.LowPrice <= sellPrice && Position >= 0)
		{
			if (Position > 0)
				SellMarket(Math.Abs(Position));
			SellMarket(OrderVolume);
			_entryPrice = sellPrice;
		}
	}

	private void UpdatePendingOrdersLifetime(ICandleMessage candle)
	{
		// No pending orders in simplified version - nothing to expire.
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		var entryPrice = _entryPrice;
		if (entryPrice == 0)
			return;

		var priceDiff = Position > 0 ? candle.ClosePrice - entryPrice : entryPrice - candle.ClosePrice;
		var prevBodySize = Math.Abs(_prevClose - _prevOpen);

		// Exit if profitable and market consolidating, or if loss exceeds threshold
		var exitByProfit = priceDiff > 0 && prevBodySize < candle.ClosePrice * 0.001m;
		var exitByLoss = priceDiff < -candle.ClosePrice * 0.005m;

		if (Position > 0 && (exitByProfit || exitByLoss))
		{
			SellMarket();
		}
		else if (Position < 0 && (exitByProfit || exitByLoss))
		{
			BuyMarket();
		}
	}

}