Martingale Breakout Strategy — порт MetaTrader-советника MartinGaleBreakout.mq5 на платформу StockSharp. Алгоритм ждет
аномально крупной свечи-пробоя и открывает единственную сделку в направлении пробоя. В оригинале управление позициями ведется
через «магический номер», но в StockSharp это достигается изоляцией стратегии, поэтому поведение полностью совпадает.
В основе системы лежат две идеи:
Фильтр пробоев. Для каждой завершённой свечи рассчитывается диапазон (High - Low) и сравнивается со средним диапазоном
предыдущих десяти свечей. Если текущая свеча более чем втрое шире среднего и закрывается в сторону пробоя, формируется сигнал.
Восстановление по типу мартингейла. Стратегия отслеживает нереализованную прибыль/убыток. Когда плавающий результат
достигает заданного убытка, все позиции закрываются, а цель по прибыли увеличивается на размер потери. После выполнения новой
цели значения порогов возвращаются к исходным, как и в версии на MQL5.
Порт сохраняет все параметры управления капиталом из исходника: процент баланса, доступный под маржу, целевую прибыль и стоп в
процентах от баланса, а также множитель, расширяющий дистанцию тейк-профита при восстановлении.
Логика торговли
Подписаться на заданный тип свечей и обрабатывать только завершенные бары.
Поддерживать кольцевой буфер из десяти диапазонов, чтобы вычислять эталонное среднее для определения «аномальности» свечи.
Рассчитывать плавающий PnL через средние цены входа для лонгов и шортов. При достижении целевой прибыли или стоп-убытка
немедленно закрывать позиции и сбрасывать состояние восстановления.
Не открывать новые сделки, пока у стратегии уже есть позиция либо торговля недоступна по состоянию соединения.
При бычьем пробое вычислять объем так, чтобы ожидаемая прибыль соответствовала текущей цели. Во время восстановления расстояние
до тейк-профита умножается на RecoveryPointsMultiplier, аналог TP_Points_Multiplier в MQL5.
Проверять рассчитанный объем на соответствие шагу, минимуму и максимуму инструмента, а также на достаточность маржи. При
прохождении проверок отправлять рыночную заявку на покупку.
При медвежьем пробое повторять процедуру и выставлять рыночную заявку на продажу.
Такой набор правил полностью воспроизводит поведение оригинального советника, включая переход в режим восстановления после
срабатывания стоп-лосса.
Параметры
Параметр
Описание
Значение по умолчанию
TakeProfitPoints
Расстояние от входа до тейк-профита в шагах цены.
50
BalancePercentAvailable
Максимальный процент баланса, который можно задействовать под маржу.
50
TakeProfitPercentOfBalance
Целевая прибыль в процентах от текущего баланса.
0.1
StopLossPercentOfBalance
Стоп-убыток в процентах от текущего баланса.
10
RecoveryStartFraction
Доля стоп-убытка, используемая до включения режима восстановления.
0.1
RecoveryPointsMultiplier
Множитель дистанции тейк-профита во время восстановления.
1
CandleType
Тип свечей, по которым строятся сигналы (таймфрейм, тиковые свечи и т. д.).
15-минутные свечи
Дополнительные замечания
Расчет объема повторяет помощник CalcLotWithTP: целевая прибыль делится на ожидаемое движение и переводится в объем с
учётом шага объема инструмента.
Проверка маржи выполнена по аналогии с функциями CheckVolumeValue и ограничением доли баланса из оригинального кода.
Заявки отклоняются, если требуемая маржа превышает выделенный процент или свободные средства портфеля.
Перед закрытием позиций стратегия отменяет активные заявки, что соответствует поведению CloseAllOrders в MQL5.
Буфер диапазонов хранит только десять значений, полностью заменяя цикл по iHigh/iLow из исходного советника и не
требуя загрузки глубокой истории.
using System;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Breakout strategy that detects abnormally large candles and enters in the breakout direction.
/// Uses a simple martingale recovery: after a losing trade, the next entry is taken more aggressively.
/// </summary>
public class MartingaleBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _lookback;
private readonly StrategyParam<decimal> _breakoutMultiplier;
private readonly StrategyParam<decimal> _takeProfitPct;
private readonly StrategyParam<decimal> _stopLossPct;
private readonly StrategyParam<DataType> _candleType;
private readonly decimal[] _rangeBuffer = new decimal[10];
private int _rangeBufferCount;
private int _rangeBufferIndex;
private decimal _rangeBufferSum;
private decimal _entryPrice;
private Sides? _entrySide;
private bool _lastWasLoss;
public int Lookback
{
get => _lookback.Value;
set => _lookback.Value = value;
}
public decimal BreakoutMultiplier
{
get => _breakoutMultiplier.Value;
set => _breakoutMultiplier.Value = value;
}
public decimal TakeProfitPct
{
get => _takeProfitPct.Value;
set => _takeProfitPct.Value = value;
}
public decimal StopLossPct
{
get => _stopLossPct.Value;
set => _stopLossPct.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public MartingaleBreakoutStrategy()
{
_lookback = Param(nameof(Lookback), 10)
.SetDisplay("Lookback", "Number of candles for average range", "General");
_breakoutMultiplier = Param(nameof(BreakoutMultiplier), 3m)
.SetDisplay("Breakout Mult", "Multiplier above avg range for breakout", "General");
_takeProfitPct = Param(nameof(TakeProfitPct), 1m)
.SetDisplay("Take Profit %", "Take profit as percentage of entry price", "Trading");
_stopLossPct = Param(nameof(StopLossPct), 0.5m)
.SetDisplay("Stop Loss %", "Stop loss as percentage of entry price", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
.SetDisplay("Candle Type", "Candle type", "General");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_rangeBufferCount = 0;
_rangeBufferIndex = 0;
_rangeBufferSum = 0m;
_entryPrice = 0m;
_entrySide = null;
_lastWasLoss = false;
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 closePrice = candle.ClosePrice;
// Check exit conditions first
if (Position != 0 && _entryPrice > 0)
{
var tp = _lastWasLoss ? TakeProfitPct * 1.5m : TakeProfitPct;
var sl = StopLossPct;
if (_entrySide == Sides.Buy)
{
var pnlPct = (closePrice - _entryPrice) / _entryPrice * 100m;
if (pnlPct >= tp || pnlPct <= -sl)
{
_lastWasLoss = pnlPct < 0;
SellMarket();
_entryPrice = 0;
_entrySide = null;
UpdateRangeStatistics(candle);
return;
}
}
else if (_entrySide == Sides.Sell)
{
var pnlPct = (_entryPrice - closePrice) / _entryPrice * 100m;
if (pnlPct >= tp || pnlPct <= -sl)
{
_lastWasLoss = pnlPct < 0;
BuyMarket();
_entryPrice = 0;
_entrySide = null;
UpdateRangeStatistics(candle);
return;
}
}
}
// Entry logic - only when flat
if (Position == 0)
{
var range = candle.HighPrice - candle.LowPrice;
if (_rangeBufferCount >= _rangeBuffer.Length)
{
var avgRange = _rangeBufferSum / _rangeBuffer.Length;
if (range > avgRange * BreakoutMultiplier)
{
var body = candle.ClosePrice - candle.OpenPrice;
if (body > 0 && body > range * 0.4m)
{
// Bullish breakout
BuyMarket();
_entryPrice = closePrice;
_entrySide = Sides.Buy;
}
else if (body < 0 && Math.Abs(body) > range * 0.4m)
{
// Bearish breakout
SellMarket();
_entryPrice = closePrice;
_entrySide = Sides.Sell;
}
}
}
}
UpdateRangeStatistics(candle);
}
private void UpdateRangeStatistics(ICandleMessage candle)
{
var range = candle.HighPrice - candle.LowPrice;
if (_rangeBufferCount < _rangeBuffer.Length)
{
_rangeBuffer[_rangeBufferIndex] = range;
_rangeBufferSum += range;
_rangeBufferCount++;
_rangeBufferIndex = (_rangeBufferIndex + 1) % _rangeBuffer.Length;
return;
}
_rangeBufferSum -= _rangeBuffer[_rangeBufferIndex];
_rangeBuffer[_rangeBufferIndex] = range;
_rangeBufferSum += range;
_rangeBufferIndex = (_rangeBufferIndex + 1) % _rangeBuffer.Length;
}
/// <inheritdoc />
protected override void OnReseted()
{
Array.Clear(_rangeBuffer);
_rangeBufferCount = 0;
_rangeBufferIndex = 0;
_rangeBufferSum = 0m;
_entryPrice = 0m;
_entrySide = null;
_lastWasLoss = false;
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
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
class martingale_breakout_strategy(Strategy):
def __init__(self):
super(martingale_breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(60)))
self._lookback = self.Param("Lookback", 10)
self._breakout_multiplier = self.Param("BreakoutMultiplier", 3.0)
self._take_profit_pct = self.Param("TakeProfitPct", 1.0)
self._stop_loss_pct = self.Param("StopLossPct", 0.5)
self._range_buffer = [0.0] * 10
self._range_buffer_count = 0
self._range_buffer_index = 0
self._range_buffer_sum = 0.0
self._entry_price = 0.0
self._entry_side = 0
self._last_was_loss = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def Lookback(self):
return self._lookback.Value
@Lookback.setter
def Lookback(self, value):
self._lookback.Value = value
@property
def BreakoutMultiplier(self):
return self._breakout_multiplier.Value
@BreakoutMultiplier.setter
def BreakoutMultiplier(self, value):
self._breakout_multiplier.Value = value
@property
def TakeProfitPct(self):
return self._take_profit_pct.Value
@TakeProfitPct.setter
def TakeProfitPct(self, value):
self._take_profit_pct.Value = value
@property
def StopLossPct(self):
return self._stop_loss_pct.Value
@StopLossPct.setter
def StopLossPct(self, value):
self._stop_loss_pct.Value = value
def OnReseted(self):
super(martingale_breakout_strategy, self).OnReseted()
self._range_buffer = [0.0] * 10
self._range_buffer_count = 0
self._range_buffer_index = 0
self._range_buffer_sum = 0.0
self._entry_price = 0.0
self._entry_side = 0
self._last_was_loss = False
def OnStarted2(self, time):
super(martingale_breakout_strategy, self).OnStarted2(time)
self._range_buffer = [0.0] * 10
self._range_buffer_count = 0
self._range_buffer_index = 0
self._range_buffer_sum = 0.0
self._entry_price = 0.0
self._entry_side = 0
self._last_was_loss = False
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _update_range_statistics(self, candle):
r = float(candle.HighPrice) - float(candle.LowPrice)
buf_len = len(self._range_buffer)
if self._range_buffer_count < buf_len:
self._range_buffer[self._range_buffer_index] = r
self._range_buffer_sum += r
self._range_buffer_count += 1
self._range_buffer_index = (self._range_buffer_index + 1) % buf_len
return
self._range_buffer_sum -= self._range_buffer[self._range_buffer_index]
self._range_buffer[self._range_buffer_index] = r
self._range_buffer_sum += r
self._range_buffer_index = (self._range_buffer_index + 1) % buf_len
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
buf_len = len(self._range_buffer)
# Check exit conditions first
if self.Position != 0 and self._entry_price > 0:
tp = float(self.TakeProfitPct) * 1.5 if self._last_was_loss else float(self.TakeProfitPct)
sl = float(self.StopLossPct)
if self._entry_side == 1:
pnl_pct = (close - self._entry_price) / self._entry_price * 100.0
if pnl_pct >= tp or pnl_pct <= -sl:
self._last_was_loss = pnl_pct < 0
self.SellMarket()
self._entry_price = 0.0
self._entry_side = 0
self._update_range_statistics(candle)
return
elif self._entry_side == -1:
pnl_pct = (self._entry_price - close) / self._entry_price * 100.0
if pnl_pct >= tp or pnl_pct <= -sl:
self._last_was_loss = pnl_pct < 0
self.BuyMarket()
self._entry_price = 0.0
self._entry_side = 0
self._update_range_statistics(candle)
return
# Entry logic - only when flat
if self.Position == 0:
r = float(candle.HighPrice) - float(candle.LowPrice)
if self._range_buffer_count >= buf_len:
avg_range = self._range_buffer_sum / buf_len
if r > avg_range * float(self.BreakoutMultiplier):
body = float(candle.ClosePrice) - float(candle.OpenPrice)
if body > 0 and body > r * 0.4:
self.BuyMarket()
self._entry_price = close
self._entry_side = 1
elif body < 0 and abs(body) > r * 0.4:
self.SellMarket()
self._entry_price = close
self._entry_side = -1
self._update_range_statistics(candle)
def CreateClone(self):
return martingale_breakout_strategy()