GitHub で見る

Backtesting Trade Assistant Panel Strategy

Overview

The Backtesting Trade Assistant Panel Strategy is a manual helper converted from the MetaTrader 4 expert advisor Backtesting Trade Assistant Panel V1.10. The original script created a graphical control panel inside the tester that let the operator change lot size, stop-loss and take-profit distances, and instantly submit BUY or SELL market orders. The StockSharp port offers the same workflow inside a strategy component by exposing strongly typed parameters and public helper methods instead of on-chart widgets.

Key capabilities:

  • Maintain configurable order volume together with MetaTrader-style stop-loss and take-profit distances (measured in “points”).
  • Issue long or short market orders on demand through the ManualBuy() and ManualSell() helpers.
  • Automatically attach stop-loss and take-profit offsets after each manual entry using the converted point values.
  • Provide utility methods that update the trading volume and risk distances at runtime, mimicking the editable text fields of the MT4 panel.

Parameters

Name Description Default
OrderVolume Volume in lots applied to manual market orders. Changing the value also updates the base Strategy.Volume. 0.1
StopLossPips Distance from the fill price to the protective stop, expressed in MetaTrader points. Set to 0 to disable automatic stop-loss placement. 50
TakeProfitPips Distance from the fill price to the profit target, expressed in MetaTrader points. Set to 0 to disable automatic take-profit placement. 100
MagicNumber Identifier preserved from the original EA for bookkeeping. It is not used directly by StockSharp execution logic but can be referenced in custom extensions. 99

Manual operations

The original EA relied on clickable buttons. In StockSharp the same actions are available as public methods:

  • SetOrderVolume(decimal volume) – synchronizes the OrderVolume parameter and the internal Strategy.Volume value.
  • SetStopLoss(decimal pips) / SetTakeProfit(decimal pips) – adjust the protective distances while the strategy is running. Values are interpreted in MetaTrader points, exactly like the MT4 text boxes.
  • ManualBuy() – submits a market buy order using the current volume. After execution the strategy converts the configured stop-loss and take-profit points into price offsets (using symbol metadata) and calls SetStopLoss/SetTakeProfit to register protective orders for the resulting net position.
  • ManualSell() – symmetric helper for market sell orders.
  • CloseAllPositions() – closes the entire exposure at market price. This mirrors the workflow where the tester could flatten positions manually.

All protective distances are converted with the same pip-size heuristic as in MT4: five- and three-digit symbols multiply PriceStep by ten to obtain a single “point”, while other symbols rely on the raw PriceStep. If market data does not provide the necessary metadata, a fallback size of 0.0001 is used to preserve consistent behaviour.

Behavioural notes

  • The strategy subscribes to Level1 updates to keep track of the best bid/ask. When those prices are unavailable it falls back to the last trade price before attaching protective offsets.
  • No automatic trading signals are generated—this module acts strictly as a manual execution assistant just like the MT4 panel.
  • Because StockSharp manages protective orders natively, there is no need for an explicit magic number. The field is included purely for parity with the source expert advisor.
  • Stop-loss and take-profit distances can be adjusted at any time before triggering ManualBuy()/ManualSell() to emulate editing the MT4 text fields prior to pressing the buttons.

Differences from the original EA

  • The MetaTrader user interface is replaced by strategy parameters and method calls. All functionality is available programmatically without rendering chart controls.
  • Slippage handling from the MT4 OrderSend call (fixed at 50 points) is not reproduced because StockSharp’s BuyMarket/SellMarket helpers do not expose a direct slippage argument. The surrounding environment should manage execution tolerance if required.
  • Protective orders are created with StockSharp’s high-level SetStopLoss/SetTakeProfit helpers instead of direct OrderSend calls, keeping the implementation consistent with StockSharp conventions.

Usage tips

  1. Configure the desired symbol, portfolio, and connector in StockSharp as usual, then start the strategy.
  2. Adjust OrderVolume, StopLossPips, and TakeProfitPips through the parameter grid or the provided setter methods.
  3. Call ManualBuy() or ManualSell() whenever a discretionary entry is needed. The helper will attach the requested protective orders automatically.
  4. Use CloseAllPositions() to flatten the exposure instantly during backtests or live discretionary trading sessions.
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>
/// Trade assistant strategy with configurable stop-loss and take-profit.
/// Simplified from the backtesting trade assistant panel.
/// </summary>
public class BacktestingTradeAssistantPanelStrategy : Strategy
{
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<DataType> _candleType;

	private SimpleMovingAverage _sma;
	private decimal _pipSize;
	private decimal _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;

	/// <summary>
	/// Stop loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public BacktestingTradeAssistantPanelStrategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Stop-loss distance in pips", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 100m)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Take-profit distance in pips", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Candle series for trading signals", "General");
	}

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

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

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

		_pipSize = CalculatePipSize();

		_sma = new SimpleMovingAverage { Length = 20 };

		SubscribeCandles(CandleType)
			.Bind(_sma, ProcessCandle)
			.Start();
	}

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

		if (!IsFormed)
			return;

		var price = candle.ClosePrice;

		// Check stop-loss and take-profit
		if (Position != 0 && _entryPrice > 0m)
		{
			if (Position > 0)
			{
				if (_stopPrice.HasValue && price <= _stopPrice.Value)
				{
					SellMarket(Math.Abs(Position));
					ResetPosition();
					return;
				}
				if (_takePrice.HasValue && price >= _takePrice.Value)
				{
					SellMarket(Math.Abs(Position));
					ResetPosition();
					return;
				}
			}
			else if (Position < 0)
			{
				if (_stopPrice.HasValue && price >= _stopPrice.Value)
				{
					BuyMarket(Math.Abs(Position));
					ResetPosition();
					return;
				}
				if (_takePrice.HasValue && price <= _takePrice.Value)
				{
					BuyMarket(Math.Abs(Position));
					ResetPosition();
					return;
				}
			}
		}

		// Entry: SMA crossover
		if (Position == 0)
		{
			var pip = _pipSize > 0m ? _pipSize : 1m;

			if (price > smaValue)
			{
				BuyMarket();
				_entryPrice = price;
				_stopPrice = StopLossPips > 0m ? price - StopLossPips * pip : null;
				_takePrice = TakeProfitPips > 0m ? price + TakeProfitPips * pip : null;
			}
			else if (price < smaValue)
			{
				SellMarket();
				_entryPrice = price;
				_stopPrice = StopLossPips > 0m ? price + StopLossPips * pip : null;
				_takePrice = TakeProfitPips > 0m ? price - TakeProfitPips * pip : null;
			}
		}
	}

	private void ResetPosition()
	{
		_entryPrice = 0m;
		_stopPrice = null;
		_takePrice = null;
	}

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

		var decimals = Security?.Decimals ?? 0;
		return decimals is 5 or 3 ? step * 10m : step;
	}
}