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, что соответствует принятой архитектуре платформы.
Рекомендации по использованию
Настройте инструмент, портфель и соединение в StockSharp, затем запустите стратегию.
Через интерфейс параметров или методы измените OrderVolume, StopLossPips, TakeProfitPips под текущие задачи.
При появлении ручного сигнала вызовите ManualBuy() или ManualSell(). Стратегия сама добавит соответствующие защитные заявки.
Для быстрого выхода из позиции воспользуйтесь 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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import Math, TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
class backtesting_trade_assistant_panel_strategy(Strategy):
def __init__(self):
super(backtesting_trade_assistant_panel_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._stop_loss_pips = self.Param("StopLossPips", 50.0)
self._take_profit_pips = self.Param("TakeProfitPips", 100.0)
self._sma = None
self._pip_size = 0.0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def StopLossPips(self):
return self._stop_loss_pips.Value
@StopLossPips.setter
def StopLossPips(self, value):
self._stop_loss_pips.Value = value
@property
def TakeProfitPips(self):
return self._take_profit_pips.Value
@TakeProfitPips.setter
def TakeProfitPips(self, value):
self._take_profit_pips.Value = value
def OnReseted(self):
super(backtesting_trade_assistant_panel_strategy, self).OnReseted()
self._sma = None
self._pip_size = 0.0
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def _calculate_pip_size(self):
sec = self.Security
if sec is None:
return 0.0001
step = sec.PriceStep
if step is None or float(step) <= 0:
return 0.0001
step_val = float(step)
decimals = sec.Decimals
if decimals is not None and (int(decimals) == 5 or int(decimals) == 3):
return step_val * 10.0
return step_val
def OnStarted2(self, time):
super(backtesting_trade_assistant_panel_strategy, self).OnStarted2(time)
self._pip_size = self._calculate_pip_size()
self._sma = SimpleMovingAverage()
self._sma.Length = 20
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._sma, self._process_candle).Start()
def _reset_position(self):
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
def _process_candle(self, candle, sma_value):
if candle.State != CandleStates.Finished:
return
if not self.IsFormed:
return
price = float(candle.ClosePrice)
sma_val = float(sma_value)
# Check stop-loss and take-profit
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
if self._stop_price is not None and price <= self._stop_price:
self.SellMarket(abs(float(self.Position)))
self._reset_position()
return
if self._take_price is not None and price >= self._take_price:
self.SellMarket(abs(float(self.Position)))
self._reset_position()
return
elif self.Position < 0:
if self._stop_price is not None and price >= self._stop_price:
self.BuyMarket(abs(float(self.Position)))
self._reset_position()
return
if self._take_price is not None and price <= self._take_price:
self.BuyMarket(abs(float(self.Position)))
self._reset_position()
return
# Entry: SMA crossover
if self.Position == 0:
pip = self._pip_size if self._pip_size > 0 else 1.0
sl_pips = float(self.StopLossPips)
tp_pips = float(self.TakeProfitPips)
if price > sma_val:
self.BuyMarket()
self._entry_price = price
self._stop_price = price - sl_pips * pip if sl_pips > 0 else None
self._take_price = price + tp_pips * pip if tp_pips > 0 else None
elif price < sma_val:
self.SellMarket()
self._entry_price = price
self._stop_price = price + sl_pips * pip if sl_pips > 0 else None
self._take_price = price - tp_pips * pip if tp_pips > 0 else None
def CreateClone(self):
return backtesting_trade_assistant_panel_strategy()