MartinGale Breakout Strategy — это портированный в StockSharp советник MetaTrader 4 MartinGaleBreakout. Алгоритм отслеживает аномальные свечи и после их появления открывает позицию по направлению пробоя. При достижении определённого убытка включается режим восстановления: целевой профит увеличивается так, чтобы компенсировать предыдущие потери, что воспроизводит мартингейл-подход оригинала.
Стратегия анализирует выбранный таймфрейм (по умолчанию 15-минутные свечи) и хранит последние 11 завершённых свечей. Если диапазон текущей свечи превышает средний диапазон предыдущих десяти свечей более чем в три раза и цена закрытия находится в доминирующей половине диапазона, формируется сигнал на вход. Далее рассчитывается объём позиции, необходимый для достижения денежной цели по профиту, при соблюдении ограничений по доступной части баланса.
Логика работы
Подписка на поток свечей заданного типа.
Накопление 11 завершённых свечей и пересчёт среднего диапазона для определения аномальности.
Определение бычьего пробоя, если свеча закрылась в верхней половине диапазона и в три раза больше среднего диапазона.
Определение медвежьего пробоя при зеркальных условиях.
Открытие рыночной позиции только при отсутствии открытых позиций и если требуемый капитал не превышает заданный процент от баланса.
Контроль плавающей прибыли/убытка по текущей позиции и принудительное закрытие при достижении целей по прибыли или убытку.
При срабатывании стоп-лосса:
Позиция закрывается рыночным ордером.
Цель по прибыли увеличивается на величину понесённого убытка.
Лимит по убытку устанавливается в максимальное значение и активируется режим восстановления.
После успешного выхода из восстановления параметры цели и стоп-лосса сбрасываются к исходным значениям.
Настройки
Параметр
Описание
Значение по умолчанию
TakeProfitPoints
Базовое расстояние до тейк-профита в пунктах инструмента.
50
BalancePercentageAvailable
Максимальная доля баланса, доступная для открытия новой позиции.
50%
TakeProfitBalancePercent
Целевой профит в процентах от баланса.
0.1%
StopLossBalancePercent
Предельная просадка перед переходом к восстановлению.
10%
StartRecoveryFactor
Доля стоп-лосса, при которой активируется режим восстановления.
0.2
TakeProfitPointsMultiplier
Множитель расстояния до тейк-профита во время восстановления.
1
CandleType
Тип свечей, используемых для поиска пробоя.
15 минут
Управление рисками
Объём сделки рассчитывается через тик-размер и тик-стоимость, чтобы получить заданную денежную прибыль при достижении цели.
Объём корректируется с учётом шага, минимального и максимального значения объёма, заданных биржей.
Перед открытием позиции оценивается требуемый капитал; если он превышает допустимую долю баланса, вход игнорируется.
Восстановление после стоп-лосса увеличивает дистанцию тейк-профита, имитируя мартингейл, но количество одновременно открытых позиций ограничено одной.
Дополнительные замечания
Для корректной работы необходимо, чтобы у стратегии был доступ к актуальной информации о портфеле.
Комиссии учитываются косвенно — через расчёт плавающего результата по текущей позиции.
Используются только рыночные ордера, отложенные заявки не формируются.
using System;
using System.Collections.Generic;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Breakout strategy with martingale-style recovery.
/// Detects abnormally large candles relative to recent history and enters in the breakout direction.
/// After a stop-loss, enters recovery mode with a wider take-profit target.
/// </summary>
public class MartinGaleBreakoutStrategy : Strategy
{
private readonly StrategyParam<int> _requiredHistory;
private readonly StrategyParam<decimal> _breakoutFactor;
private readonly StrategyParam<decimal> _takeProfitPct;
private readonly StrategyParam<decimal> _stopLossPct;
private readonly StrategyParam<decimal> _recoveryMultiplier;
private readonly StrategyParam<DataType> _candleType;
private readonly List<decimal> _ranges = new();
private decimal _entryPrice;
private Sides? _entrySide;
private bool _recovering;
public int RequiredHistory
{
get => _requiredHistory.Value;
set => _requiredHistory.Value = value;
}
public decimal BreakoutFactor
{
get => _breakoutFactor.Value;
set => _breakoutFactor.Value = value;
}
public decimal TakeProfitPct
{
get => _takeProfitPct.Value;
set => _takeProfitPct.Value = value;
}
public decimal StopLossPct
{
get => _stopLossPct.Value;
set => _stopLossPct.Value = value;
}
public decimal RecoveryMultiplier
{
get => _recoveryMultiplier.Value;
set => _recoveryMultiplier.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public MartinGaleBreakoutStrategy()
{
_requiredHistory = Param(nameof(RequiredHistory), 10)
.SetDisplay("Lookback", "Number of candles for average range", "General");
_breakoutFactor = Param(nameof(BreakoutFactor), 2.5m)
.SetDisplay("Breakout Factor", "Multiplier for abnormal candle detection", "General");
_takeProfitPct = Param(nameof(TakeProfitPct), 0.5m)
.SetDisplay("TP %", "Take profit percent of entry", "Trading");
_stopLossPct = Param(nameof(StopLossPct), 0.3m)
.SetDisplay("SL %", "Stop loss percent of entry", "Trading");
_recoveryMultiplier = Param(nameof(RecoveryMultiplier), 1.5m)
.SetDisplay("Recovery Mult", "TP multiplier during recovery", "Trading");
_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
.SetDisplay("Candle Type", "Candle series", "General");
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_ranges.Clear();
_entryPrice = 0;
_entrySide = null;
_recovering = 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 close = candle.ClosePrice;
var range = candle.HighPrice - candle.LowPrice;
// Check exit
if (Position != 0 && _entryPrice > 0)
{
var tpPct = _recovering ? TakeProfitPct * RecoveryMultiplier : TakeProfitPct;
if (_entrySide == Sides.Buy)
{
var pnl = (close - _entryPrice) / _entryPrice * 100m;
if (pnl >= tpPct || pnl <= -StopLossPct)
{
var wasLoss = pnl < 0;
SellMarket();
_entryPrice = 0;
_entrySide = null;
_recovering = wasLoss;
AddRange(range);
return;
}
}
else if (_entrySide == Sides.Sell)
{
var pnl = (_entryPrice - close) / _entryPrice * 100m;
if (pnl >= tpPct || pnl <= -StopLossPct)
{
var wasLoss = pnl < 0;
BuyMarket();
_entryPrice = 0;
_entrySide = null;
_recovering = wasLoss;
AddRange(range);
return;
}
}
}
// Entry - only when flat
if (Position == 0 && _ranges.Count >= RequiredHistory)
{
decimal sum = 0;
for (int i = 0; i < _ranges.Count; i++)
sum += _ranges[i];
var avgRange = sum / _ranges.Count;
if (avgRange > 0 && range > avgRange * BreakoutFactor)
{
var body = candle.ClosePrice - candle.OpenPrice;
if (body > 0 && body > range * 0.4m)
{
BuyMarket();
_entryPrice = close;
_entrySide = Sides.Buy;
}
else if (body < 0 && Math.Abs(body) > range * 0.4m)
{
SellMarket();
_entryPrice = close;
_entrySide = Sides.Sell;
}
}
}
AddRange(range);
}
private void AddRange(decimal range)
{
_ranges.Add(range);
while (_ranges.Count > RequiredHistory)
_ranges.RemoveAt(0);
}
/// <inheritdoc />
protected override void OnReseted()
{
_ranges.Clear();
_entryPrice = 0;
_entrySide = null;
_recovering = 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 martin_gale_breakout_strategy(Strategy):
def __init__(self):
super(martin_gale_breakout_strategy, self).__init__()
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(5)))
self._required_history = self.Param("RequiredHistory", 10)
self._breakout_factor = self.Param("BreakoutFactor", 2.5)
self._take_profit_pct = self.Param("TakeProfitPct", 0.5)
self._stop_loss_pct = self.Param("StopLossPct", 0.3)
self._recovery_multiplier = self.Param("RecoveryMultiplier", 1.5)
self._ranges = []
self._entry_price = 0.0
self._entry_side = 0
self._recovering = False
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
@property
def RequiredHistory(self):
return self._required_history.Value
@RequiredHistory.setter
def RequiredHistory(self, value):
self._required_history.Value = value
@property
def BreakoutFactor(self):
return self._breakout_factor.Value
@BreakoutFactor.setter
def BreakoutFactor(self, value):
self._breakout_factor.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
@property
def RecoveryMultiplier(self):
return self._recovery_multiplier.Value
@RecoveryMultiplier.setter
def RecoveryMultiplier(self, value):
self._recovery_multiplier.Value = value
def OnReseted(self):
super(martin_gale_breakout_strategy, self).OnReseted()
self._ranges = []
self._entry_price = 0.0
self._entry_side = 0
self._recovering = False
def OnStarted2(self, time):
super(martin_gale_breakout_strategy, self).OnStarted2(time)
self._ranges = []
self._entry_price = 0.0
self._entry_side = 0
self._recovering = False
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._process_candle).Start()
def _add_range(self, r):
self._ranges.append(r)
req = self.RequiredHistory
while len(self._ranges) > req:
self._ranges.pop(0)
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
r = float(candle.HighPrice) - float(candle.LowPrice)
# Check exit
if self.Position != 0 and self._entry_price > 0:
tp_pct = float(self.TakeProfitPct) * float(self.RecoveryMultiplier) if self._recovering else float(self.TakeProfitPct)
sl_pct = float(self.StopLossPct)
if self._entry_side == 1:
pnl = (close - self._entry_price) / self._entry_price * 100.0
if pnl >= tp_pct or pnl <= -sl_pct:
was_loss = pnl < 0
self.SellMarket()
self._entry_price = 0.0
self._entry_side = 0
self._recovering = was_loss
self._add_range(r)
return
elif self._entry_side == -1:
pnl = (self._entry_price - close) / self._entry_price * 100.0
if pnl >= tp_pct or pnl <= -sl_pct:
was_loss = pnl < 0
self.BuyMarket()
self._entry_price = 0.0
self._entry_side = 0
self._recovering = was_loss
self._add_range(r)
return
# Entry - only when flat
req = self.RequiredHistory
if self.Position == 0 and len(self._ranges) >= req:
total = sum(self._ranges)
avg_range = total / len(self._ranges)
if avg_range > 0 and r > avg_range * float(self.BreakoutFactor):
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._add_range(r)
def CreateClone(self):
return martin_gale_breakout_strategy()