Стратегия представляет собой конверсию советника MetaTrader Autotrade (barabashkakvn's edition) на платформу StockSharp. Она постоянно поддерживает две симметричные стоп-заявки вокруг текущей цены: Buy Stop выше рынка и Sell Stop ниже рынка. Когда позиции отсутствуют, отложенные ордера обновляются на каждом закрытом баре. После срабатывания стопа позиция сопровождается до тех пор, пока рынок не войдет в фазу низкой волатильности или пока прибыль/убыток не достигнет заданного порога. Реализация полностью следует требованиям AGENTS.md и использует высокоуровневый API StockSharp.
Соответствие параметров MQL5
Параметр StockSharp
Параметр MQL5
Назначение
IndentTicks
InpIndent
Расстояние в шагах цены от текущей котировки до уровня стоп-заявок.
MinProfit
MinProfit
Минимальная плавающая прибыль (в валюте счета) для выхода при стабилизации рынка.
ExpirationMinutes
ExpirationMinutes
Время жизни отложенных заявок перед их отменой и перерегистрацией.
AbsoluteFixation
AbsoluteFixation
Абсолютная величина прибыли или убытка (в валюте), при которой позиция закрывается принудительно.
StabilizationTicks
InpStabilization
Максимальный размер тела предыдущей свечи, считающийся консолидацией.
OrderVolume
Lots
Объем для Buy Stop и Sell Stop.
CandleType
Period()
Тип свечей, управляющий логикой (по умолчанию 1-минутные свечи).
Значения, выраженные в пунктах, преобразуются в реальные шаги цены с помощью Security.PriceStep. Пороговые значения по прибыли/убытку рассчитываются через Security.StepPrice, что воспроизводит принципы расчета прибыли в MQL5.
Торговая логика
Установка отложенных ордеров
Обработка ведется только на закрытых свечах (CandleStates.Finished).
Первая свеча используется для сохранения истории (open/close) и мгновенной постановки ордеров.
При отсутствии позиции очищаются неактивные ссылки и выставляются:
Buy Stop по цене Close + IndentTicks * PriceStep.
Sell Stop по цене Close - IndentTicks * PriceStep.
Для каждого стопа рассчитывается срок действия CloseTime + ExpirationMinutes. По истечении срока заявка отменяется и будет перерегистрирована на следующей свече.
Сопровождение позиции
При срабатывании одного стопа противоположный ордер отменяется, чтобы избежать хеджирования на неттинговой модели учета.
Сохраняется размер тела предыдущей свечи (|Open - Close|) для контроля спокойного рынка.
На каждой свече при открытой позиции рассчитывается нереализованная прибыль относительно PositionAvgPrice.
Если прибыль больше MinProfit, а тело предыдущей свечи меньше StabilizationTicks * PriceStep, позиция закрывается рыночным ордером.
Если абсолютная прибыль или убыток превышает AbsoluteFixation, позиция также закрывается.
После выхода в ноль все остаточные отложенные ордера снимаются.
Дополнительные нюансы
Стратегия рассчитана на одну позицию (неттинг). Параметр OrderVolume автоматически устанавливает Volume стратегии.
В отсутствии потоков Bid/Ask при тестировании используется цена закрытия свечи для расчета уровней стопов.
Проверяется состояние IsFormedAndOnlineAndAllowTrading() перед размещением новых заявок.
Особенности реализации и отличия от MQL5
Расчет прибыли требует корректных значений Security.PriceStep и Security.StepPrice. При их отсутствии используется fallback 1.
В оригинальном советнике противоположный стоп мог оставаться активным при открытой позиции. В версии StockSharp он снимается сразу после исполнения, что соответствует неттинговой модели.
Срок действия отложенных ордеров опирается на CloseTime свечи. Для лент без этого поля необходимо адаптировать источник данных.
Параметр CandleType позволяет использовать любые типы свечей, поддерживаемые StockSharp.
Рекомендации по использованию
Подберите CandleType в соответствии с таймфреймом, использованным в MetaTrader.
Настройте IndentTicks, StabilizationTicks, MinProfit и AbsoluteFixation с учетом шага цены и стоимости тика инструмента.
Убедитесь, что портфель работает в нужном режиме учета. Стратегия рассчитана на неттинг и закрывает позицию перед повторной установкой стопов.
Используйте параметры для оптимизации в Designer/Backtester, чтобы адаптировать стратегию к различным инструментам.
Отслеживайте журнал: стратегия не размещает заявки, пока не будут доступны завершенные свечи и не разрешена торговля.
Дисклеймер
Алгоритмическая торговля сопряжена с рисками. Проведите полноценное тестирование, учитывайте биржевые ограничения (минимальные расстояния до стопов) и только затем переходите к торговле на реальном счете.
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();
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Unit, UnitTypes
from StockSharp.Algo.Strategies import Strategy
class autotrade_pending_stops_strategy(Strategy):
def __init__(self):
super(autotrade_pending_stops_strategy, self).__init__()
self._indent_ticks = self.Param("IndentTicks", 200)
self._min_profit = self.Param("MinProfit", 2.0)
self._expiration_minutes = self.Param("ExpirationMinutes", 41)
self._absolute_fixation = self.Param("AbsoluteFixation", 43.0)
self._stabilization_ticks = self.Param("StabilizationTicks", 25)
self._order_volume = self.Param("OrderVolume", 1.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4)))
self._prev_open = 0.0
self._prev_close = 0.0
self._has_prev_candle = False
self._tick_size = 1.0
self._entry_price = 0.0
@property
def IndentTicks(self):
return self._indent_ticks.Value
@IndentTicks.setter
def IndentTicks(self, value):
self._indent_ticks.Value = value
@property
def MinProfit(self):
return self._min_profit.Value
@MinProfit.setter
def MinProfit(self, value):
self._min_profit.Value = value
@property
def ExpirationMinutes(self):
return self._expiration_minutes.Value
@ExpirationMinutes.setter
def ExpirationMinutes(self, value):
self._expiration_minutes.Value = value
@property
def AbsoluteFixation(self):
return self._absolute_fixation.Value
@AbsoluteFixation.setter
def AbsoluteFixation(self, value):
self._absolute_fixation.Value = value
@property
def StabilizationTicks(self):
return self._stabilization_ticks.Value
@StabilizationTicks.setter
def StabilizationTicks(self, value):
self._stabilization_ticks.Value = value
@property
def OrderVolume(self):
return self._order_volume.Value
@OrderVolume.setter
def OrderVolume(self, value):
self._order_volume.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(autotrade_pending_stops_strategy, self).OnStarted2(time)
self._prev_open = 0.0
self._prev_close = 0.0
self._has_prev_candle = False
self._entry_price = 0.0
self.Volume = self._order_volume.Value
self._tick_size = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
if self._tick_size <= 0.0:
self._tick_size = 1.0
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.ProcessCandle).Start()
def ProcessCandle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
open_price = float(candle.OpenPrice)
if not self._has_prev_candle:
self._prev_open = open_price
self._prev_close = close
self._has_prev_candle = True
self._ensure_pending_orders(candle)
return
if self.Position == 0:
self._ensure_pending_orders(candle)
else:
self._manage_open_position(candle)
self._prev_open = open_price
self._prev_close = close
def _ensure_pending_orders(self, candle):
if not self.IsFormedAndOnlineAndAllowTrading():
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
order_vol = float(self._order_volume.Value)
indent = int(self._indent_ticks.Value) * self._tick_size
buy_price = close + indent
sell_price = close - indent
pos = float(self.Position)
if high >= buy_price and pos <= 0:
if pos < 0:
self.BuyMarket(abs(pos))
self.BuyMarket(order_vol)
self._entry_price = buy_price
elif low <= sell_price and pos >= 0:
if pos > 0:
self.SellMarket(abs(pos))
self.SellMarket(order_vol)
self._entry_price = sell_price
def _manage_open_position(self, candle):
if self._entry_price == 0.0:
return
close = float(candle.ClosePrice)
high = float(candle.HighPrice)
low = float(candle.LowPrice)
if self.Position > 0:
price_diff = close - self._entry_price
else:
price_diff = self._entry_price - close
prev_body_size = abs(self._prev_close - self._prev_open)
exit_by_profit = price_diff > 0.0 and prev_body_size < close * 0.001
exit_by_loss = price_diff < -close * 0.005
if self.Position > 0 and (exit_by_profit or exit_by_loss):
self.SellMarket()
elif self.Position < 0 and (exit_by_profit or exit_by_loss):
self.BuyMarket()
def OnReseted(self):
super(autotrade_pending_stops_strategy, self).OnReseted()
self._prev_open = 0.0
self._prev_close = 0.0
self._has_prev_candle = False
self._tick_size = 1.0
self._entry_price = 0.0
def CreateClone(self):
return autotrade_pending_stops_strategy()