Открыть на GitHub

Стратегия Backtesting Trade Assistant Panel

Обзор

Backtesting Trade Assistant Panel Strategy — перенос эксперта MetaTrader 4 Backtesting Trade Assistant Panel V1.10, который в тестере предоставлял панель с кнопками BUY/SELL и полями для ввода лота, стоп-лосса и тейк-профита. В версии для StockSharp визуальные элементы заменены на параметры стратегии и публичные методы, но сценарий работы остаётся тем же: трейдер вручную инициирует сделки, а стратегия автоматически навешивает защитные ордера.

Ключевые возможности:

  • Хранит настраиваемый объём заявки, а также расстояние до стоп-лосса и тейк-профита в MetaTrader-пунктах (points).
  • Позволяет мгновенно открыть длинную или короткую позицию через методы ManualBuy() и ManualSell().
  • После каждой операции рассчитывает цену защитных ордеров и вызывает SetStopLoss / SetTakeProfit, повторяя механику MT4.
  • Предоставляет вспомогательные методы SetOrderVolume, SetStopLoss, SetTakeProfit, чтобы менять параметры прямо во время работы стратегии — аналог текстовых полей в оригинальной панели.

Параметры

Имя Описание Значение по умолчанию
OrderVolume Объём (в лотах), который используется при отправке рыночных заявок. Значение также записывается в Strategy.Volume. 0.1
StopLossPips Расстояние до стоп-лосса в пунктах MT4. Если указать 0, стоп-лосс не выставляется автоматически. 50
TakeProfitPips Расстояние до тейк-профита в пунктах MT4. Значение 0 отключает автоматический тейк-профит. 100
MagicNumber Магический номер, сохранённый для совместимости с исходным экспертом. StockSharp напрямую его не использует, но можно применить в расширениях или логах. 99

Ручные действия

В MT4 действия выполнялись нажатием кнопок, здесь используются методы:

  • SetOrderVolume(decimal volume) — обновляет параметр объёма и синхронизирует Strategy.Volume.
  • SetStopLoss(decimal pips) / SetTakeProfit(decimal pips) — изменяют расстояние до защитных ордеров (в пунктах) на лету.
  • ManualBuy() — отправляет рыночную заявку на покупку с текущим объёмом и автоматически выставляет стоп/профит, пересчитанные из пунктов в цену по данным инструмента.
  • ManualSell() — симметричная операция для продажи.
  • CloseAllPositions() — мгновенно закрывает текущую позицию по рынку, как это делалось в панели тестера.

При переводе пунктов в цену используется стандарт MT4: для инструментов с тремя или пятью знаками после запятой величина point равна PriceStep * 10, для остальных — PriceStep. Если биржевая информация отсутствует, применяется значение по умолчанию 0.0001, чтобы сохранить предсказуемость.

Особенности поведения

  • Стратегия подписывается на Level1-данные и использует лучшие bid/ask. Если они недоступны, берётся последняя цена сделки.
  • Никакой автоматической логики входа не реализовано — это чисто вспомогательный модуль для ручных операций.
  • MagicNumber присутствует для совместимости; при необходимости его можно использовать в собственных фильтрах или отчётности.
  • Перед вызовом ManualBuy()/ManualSell() можно менять объём и уровни защитных ордеров, полностью повторяя поведение исходной панели.

Отличия от оригинального эксперта

  • Графический интерфейс заменён на параметры и методы; управление осуществляется программно.
  • Ограничение по проскальзыванию (50 пунктов в вызове OrderSend) не перенесено, потому что BuyMarket/SellMarket в StockSharp не поддерживают явный параметр проскальзывания. При необходимости настройте контроль на стороне коннектора или риск-модуля.
  • Защитные ордера создаются через высокоуровневые методы StockSharp SetStopLoss и SetTakeProfit, что соответствует принятой архитектуре платформы.

Рекомендации по использованию

  1. Настройте инструмент, портфель и соединение в StockSharp, затем запустите стратегию.
  2. Через интерфейс параметров или методы измените OrderVolume, StopLossPips, TakeProfitPips под текущие задачи.
  3. При появлении ручного сигнала вызовите ManualBuy() или ManualSell(). Стратегия сама добавит соответствующие защитные заявки.
  4. Для быстрого выхода из позиции воспользуйтесь CloseAllPositions() — удобно как в бэктестах, так и при живых тренировках.
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;
	}
}