Стратегия «Пауза после серии убытков» переносит на StockSharp логику советника MetaTrader 4 Pause Trading On Consecutive Loss. Исходный код анализировал историю сделок, находил последнюю последовательность убыточных ордеров и блокировал открытие новых позиций, если серия достигала заданной длины за ограниченный промежуток времени. В порте сохранён тот же принцип, но вокруг него построена минимальная модель входа по импульсу, чтобы механизм паузы можно было тестировать отдельно от других систем.
Алгоритм работы
Стратегия подписывается на свечи типа CandleType. Для каждой завершённой свечи закрытие сравнивается с предыдущим закрытием: рост провоцирует покупку, падение — продажу. Если у бычьей позиции появляется свеча с закрытием ниже открытия, она закрывается; для короткой позиции действует зеркальное правило.
После полного закрытия позиции анализируется реализованный результат (PnLManager.RealizedPnL). Отрицательные значения добавляют время закрытия в очередь FIFO, хранящую только последовательные убытки. Положительные или нулевые результаты очищают очередь — аналогично тому, как MQL-версия прекращала просмотр истории при первом неубыточном ордере.
Когда длина очереди достигает ConsecutiveLosses, вычисляется разница между самой ранней и самой поздней убыточной сделкой. Если она не превышает WithinMinutes, стратегия ставит торговлю на паузу на PauseMinutes минут, отсчитывая их от времени последнего закрытия. Во время паузы новые заявки не отправляются, но существующие позиции могут закрываться по обычным правилам.
После истечения паузы очередь сбрасывается, и торговля автоматически возобновляется. Таким образом воспроизводится поведение функций CheckLastNLossDifference и lastOrderCloseTime без повторного обхода всей истории ордеров.
Стратегия использует высокоуровневую подписку на свечи (SubscribeCandles) и встроенный менеджер прибыли StockSharp для отслеживания реализованного результата. Очередь Queue<DateTimeOffset> хранит временные метки серии убытков, что позволяет обойтись без ручного опроса истории сделок.
Параметры
Параметр
Значение по умолчанию
Описание
CandleType
Свечи с периодом 5 минут
Тип и таймфрейм свечей, на которых формируется импульсный сигнал.
OrderVolume
0.1
Объём заявки для входа и выхода из позиции.
ConsecutiveLosses
3
Число последовательных убытков, необходимое для запуска паузы.
WithinMinutes
20
Максимальное количество минут между первой и последней убыточной сделкой в серии (0 — отключить проверку).
PauseMinutes
20
Длительность паузы после срабатывания фильтра.
Дополнительные детали
Очередь пополняется только тогда, когда стратегия полностью выходит из позиции с отрицательным результатом. Частичные фиксации и прибыльные сделки не продлевают серию и не вызывают ложных срабатываний.
Проверка на окончание паузы выполняется при обработке каждой завершённой свечи. Если PauseMinutes истекли в период простоя, следующая свеча моментально разблокирует торговлю.
В варианте StockSharp используется неттинговая позиция, поэтому величина реализованной прибыли берётся как прирост PnLManager.RealizedPnL. Это повторяет логику MQL без многократного обращения к истории ордеров.
using System;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Simplified from "Pause Trading On Consecutive Loss" MetaTrader expert.
/// Uses simple momentum entries (close vs previous close) with a pause mechanism
/// that halts trading after consecutive losing trades within a time window.
/// </summary>
public class PauseTradingOnConsecutiveLossStrategy : Strategy
{
private readonly StrategyParam<DataType> _candleType;
private readonly StrategyParam<int> _consecutiveLosses;
private readonly StrategyParam<int> _pauseBars;
private decimal? _previousClose;
private int _lossStreak;
private int _pauseCountdown;
private decimal _entryPrice;
private Sides? _entryDirection;
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public int ConsecutiveLosses
{
get => _consecutiveLosses.Value;
set => _consecutiveLosses.Value = value;
}
public int PauseBars
{
get => _pauseBars.Value;
set => _pauseBars.Value = value;
}
public PauseTradingOnConsecutiveLossStrategy()
{
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
.SetDisplay("Candle Type", "Timeframe for momentum entries", "General");
_consecutiveLosses = Param(nameof(ConsecutiveLosses), 3)
.SetGreaterThanZero()
.SetDisplay("Consecutive Losses", "Losses before pausing", "Risk");
_pauseBars = Param(nameof(PauseBars), 8)
.SetGreaterThanZero()
.SetDisplay("Pause Bars", "Number of bars to pause after loss streak", "Risk");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_previousClose = null;
_lossStreak = 0;
_pauseCountdown = 0;
_entryPrice = 0;
_entryDirection = null;
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
var close = candle.ClosePrice;
if (_previousClose is null)
{
_previousClose = close;
return;
}
var volume = Volume;
if (volume <= 0)
volume = 1;
var momentumThreshold = _previousClose.Value * 0.003m;
// Check if we should pause
if (_pauseCountdown > 0)
{
_pauseCountdown--;
_previousClose = close;
return;
}
// Check for exit and track wins/losses
if (Position != 0)
{
var shouldExit = false;
if (Position > 0 && close < _previousClose.Value - momentumThreshold)
shouldExit = true;
else if (Position < 0 && close > _previousClose.Value + momentumThreshold)
shouldExit = true;
if (shouldExit)
{
// Determine if this was a winning or losing trade
var isLoss = false;
if (_entryDirection == Sides.Buy && close < _entryPrice)
isLoss = true;
else if (_entryDirection == Sides.Sell && close > _entryPrice)
isLoss = true;
if (isLoss)
{
_lossStreak++;
if (_lossStreak >= ConsecutiveLosses)
{
_pauseCountdown = PauseBars;
_lossStreak = 0;
}
}
else
{
_lossStreak = 0;
}
// Close position
if (Position > 0)
SellMarket(Position);
else if (Position < 0)
BuyMarket(Math.Abs(Position));
_entryDirection = null;
}
}
// New entry: momentum - close > prev close -> buy, close < prev close -> sell
if (Position == 0 && _entryDirection is null)
{
if (close > _previousClose.Value + momentumThreshold)
{
BuyMarket(volume);
_entryPrice = close;
_entryDirection = Sides.Buy;
}
else if (close < _previousClose.Value - momentumThreshold)
{
SellMarket(volume);
_entryPrice = close;
_entryDirection = Sides.Sell;
}
}
_previousClose = close;
}
/// <inheritdoc />
protected override void OnReseted()
{
_previousClose = null;
_lossStreak = 0;
_pauseCountdown = 0;
_entryPrice = 0;
_entryDirection = null;
base.OnReseted();
}
}
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
from StockSharp.Algo.Strategies import Strategy
class pause_trading_on_consecutive_loss_strategy(Strategy):
def __init__(self):
super(pause_trading_on_consecutive_loss_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60))).SetDisplay("Candle Type", "Timeframe for momentum entries", "General")
self._consecutive_losses = self.Param("ConsecutiveLosses", 3).SetGreaterThanZero().SetDisplay("Consecutive Losses", "Losses before pausing", "Risk")
self._pause_bars = self.Param("PauseBars", 8).SetGreaterThanZero().SetDisplay("Pause Bars", "Number of bars to pause after loss streak", "Risk")
@property
def CandleType(self): return self._candle_type.Value
@CandleType.setter
def CandleType(self, value): self._candle_type.Value = value
def OnReseted(self):
super(pause_trading_on_consecutive_loss_strategy, self).OnReseted()
self._previous_close = None
self._loss_streak = 0
self._pause_countdown = 0
self._entry_price = 0
self._entry_direction = None
def OnStarted2(self, time):
super(pause_trading_on_consecutive_loss_strategy, self).OnStarted2(time)
self._previous_close = None
self._loss_streak = 0
self._pause_countdown = 0
self._entry_price = 0
self._entry_direction = None
sub = self.SubscribeCandles(self.CandleType)
sub.Bind(self.OnProcess).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, sub)
self.DrawOwnTrades(area)
def OnProcess(self, candle):
if candle.State != CandleStates.Finished:
return
close = candle.ClosePrice
if self._previous_close is None:
self._previous_close = close
return
momentum_threshold = float(self._previous_close) * 0.003
if self._pause_countdown > 0:
self._pause_countdown -= 1
self._previous_close = close
return
if self.Position != 0:
should_exit = False
if self.Position > 0 and close < self._previous_close - momentum_threshold:
should_exit = True
elif self.Position < 0 and close > self._previous_close + momentum_threshold:
should_exit = True
if should_exit:
is_loss = False
if self._entry_direction == "buy" and close < self._entry_price:
is_loss = True
elif self._entry_direction == "sell" and close > self._entry_price:
is_loss = True
if is_loss:
self._loss_streak += 1
if self._loss_streak >= self._consecutive_losses.Value:
self._pause_countdown = self._pause_bars.Value
self._loss_streak = 0
else:
self._loss_streak = 0
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
self._entry_direction = None
if self.Position == 0 and self._entry_direction is None:
if close > self._previous_close + momentum_threshold:
self.BuyMarket()
self._entry_price = close
self._entry_direction = "buy"
elif close < self._previous_close - momentum_threshold:
self.SellMarket()
self._entry_price = close
self._entry_direction = "sell"
self._previous_close = close
def CreateClone(self):
return pause_trading_on_consecutive_loss_strategy()