Стратегия воспроизводит логику оригинального советника "Martin" из MQL, формируя хеджированную мартингейл-сетку вокруг текущей цены. Позиции постоянно чередуются между покупкой и продажей, а объём сделки удваивается при каждом развороте до тех пор, пока суммарная прибыль всего портфеля заявок не достигнет заданной цели. Свечи используются только как источник сигналов, а исполнение заявок полностью реализовано через высокоуровневые методы StockSharp.
Алгоритм работы
При запуске стратегия читает PriceStep инструмента и переводит параметры EntryOffsetPoints и StepPoints в абсолютные ценовые расстояния. Если шаг цены недоступен, используется значение 1.
Когда нет открытых позиций и активного цикла мартингейла, создаются симметричные стоп-заявки Buy Stop и Sell Stop на расстоянии EntryOffsetPoints * PriceStep от последней цены закрытия. Это соответствует 10 пунктам в исходном коде MQL.
После исполнения одной из стоп-заявок противоположная отменяется. Сделка фиксируется как первая в цепочке мартингейла: сохраняются её цена, направление и объём, а внутренний счётчик уровня устанавливается в 1.
На каждом закрытии свечи текущая цена сравнивается с ценой последней сделки. Если рынок прошёл против неё не менее чем на martingaleLevel * StepPoints * PriceStep, регистрируется рыночная заявка в противоположную сторону с объёмом, удвоенным относительно предыдущей сделки. После исполнения обновляются данные о последней заявке.
Нереализованная прибыль вычисляется по формуле PnL + Position * (closePrice - PositionPrice). Как только агрегированная прибыль превышает параметр ProfitTarget, вызывается CloseAll() для полного закрытия корзины, отменяются оставшиеся заявки и цикл сбрасывается, чтобы можно было разместить новую пару стопов.
Аналогичный сброс происходит и при ручном закрытии всех позиций: внутренние счётчики очищаются, и на следующей свече появится новая пара заявок.
Такой подход сохраняет чередование сделок, характерное для оригинального советника, и полностью укладывается в рекомендованное использование высокоуровневого API StockSharp.
Параметры
StepPoints – количество шагов цены, определяющее порог разворота для следующей усредняющей сделки. Значение по умолчанию 10 и допускает оптимизацию.
EntryOffsetPoints – отступ для исходных стоп-заявок в шагах цены. По умолчанию также равен 10 пунктам, как в версии MQL.
ProfitTarget – абсолютная величина прибыли в валюте депозита, необходимая для закрытия всей мартингейл-сетки. После превышения этого уровня по сумме реализованной и нереализованной прибыли все позиции ликвидируются.
CandleType – тип свечей, используемый для генерации сигналов. По умолчанию установлен таймфрейм 1 минута, но допускается любой доступный DataType.
Базовый объём берётся из свойства Volume стратегии. Каждый новый разворот умножает его на два, формируя классическую мартингейл-последовательность.
Практические замечания
Настраивайте Volume с учётом минимального шага объёма брокера. Из-за удвоения объёмов экспозиция растёт очень быстро, поэтому необходимо ограничивать риск на уровне портфеля.
Поскольку решения принимаются по закрытиям свечей, резкие движения внутри свечи могут привести к более позднему срабатыванию по сравнению с тиковым вариантом на MQL. Тем не менее стоп-заявки позволяют сохранять уровни входа, близкие к исходной логике.
На график выводятся свечи и собственные сделки стратегии для визуального контроля.
Автоматический стоп-лосс не используется. Единственным условием выхода является достижение ProfitTarget, поэтому при выборе инструмента и таймфрейма необходимо учитывать риск затяжных трендов против позиции.
Отличия от MQL-версии
В StockSharp используется неттированная позиция, поэтому каждый разворот выполняется одной рыночной заявкой, которая одновременно закрывает прежнюю экспозицию и открывает новую. Совокупный результат совпадает с хеджированной реализацией.
Тиковая обработка заменена анализом закрытий свечей, что соответствует рекомендациям по работе с высокоуровневым API.
Для исключения повторной обработки частичных исполнений отслеживаются идентификаторы заявок, что обеспечивает корректное удвоение объёмов.
Благодаря этим изменениям стратегия сохраняет торговое поведение исходного советника и адаптирована для инфраструктуры StockSharp.
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>
/// Martingale grid that alternates long and short entries while doubling volume.
/// </summary>
public class MartinMartingaleStrategy : Strategy
{
private readonly StrategyParam<int> _stepPoints;
private readonly StrategyParam<int> _entryOffsetPoints;
private readonly StrategyParam<decimal> _profitTarget;
private readonly StrategyParam<int> _maxLevel;
private readonly StrategyParam<DataType> _candleType;
private decimal _stepSize;
private decimal _entryOffset;
private decimal _lastTradePrice;
private decimal _lastTradeVolume;
private int _martingaleLevel;
private Sides? _lastTradeSide;
private bool _isClosing;
private decimal? _initialPrice;
/// <summary>
/// Distance in points that defines when the next reversal is triggered.
/// </summary>
public int StepPoints
{
get => _stepPoints.Value;
set => _stepPoints.Value = value;
}
/// <summary>
/// Offset in points for the initial breakout entry.
/// </summary>
public int EntryOffsetPoints
{
get => _entryOffsetPoints.Value;
set => _entryOffsetPoints.Value = value;
}
/// <summary>
/// Aggregated profit required to close the entire martingale cycle.
/// </summary>
public decimal ProfitTarget
{
get => _profitTarget.Value;
set => _profitTarget.Value = value;
}
/// <summary>
/// Maximum martingale doubling level before resetting.
/// </summary>
public int MaxLevel
{
get => _maxLevel.Value;
set => _maxLevel.Value = value;
}
/// <summary>
/// Candle type used to monitor the price.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <summary>
/// Initializes a new instance of <see cref="MartinMartingaleStrategy"/>.
/// </summary>
public MartinMartingaleStrategy()
{
_stepPoints = Param(nameof(StepPoints), 10)
.SetGreaterThanZero()
.SetDisplay("Step (points)", "Distance multiplier for reversals", "General")
;
_entryOffsetPoints = Param(nameof(EntryOffsetPoints), 10)
.SetGreaterThanZero()
.SetDisplay("Entry Offset (points)", "Offset for initial breakout entry", "General")
;
_profitTarget = Param(nameof(ProfitTarget), 5m)
.SetGreaterThanZero()
.SetDisplay("Profit Target", "Total profit to close all positions", "Risk")
;
_maxLevel = Param(nameof(MaxLevel), 5)
.SetGreaterThanZero()
.SetDisplay("Max Level", "Maximum martingale levels", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Candles for price monitoring", "Data");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
ResetCycle();
_isClosing = false;
_initialPrice = null;
_stepSize = 0;
_entryOffset = 0;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
UpdateStepSettings();
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;
UpdateStepSettings();
if (_stepSize <= 0m || Volume <= 0m)
return;
var price = candle.ClosePrice;
// If closing, flatten and wait
if (_isClosing)
{
if (Position == 0)
{
_isClosing = false;
ResetCycle();
}
return;
}
// If flat after a cycle, reset
if (Position == 0 && _martingaleLevel > 0)
{
ResetCycle();
}
// Check profit target
if (ProfitTarget > 0m && PnL >= ProfitTarget && Position != 0)
{
_isClosing = true;
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
return;
}
// Max level reached -> close and reset
if (_martingaleLevel >= MaxLevel && Position != 0)
{
_isClosing = true;
if (Position > 0)
SellMarket();
else if (Position < 0)
BuyMarket();
return;
}
// Initial entry: wait for breakout from first candle
if (_martingaleLevel == 0 && Position == 0)
{
if (!_initialPrice.HasValue)
{
_initialPrice = price;
return;
}
if (_entryOffset <= 0m)
return;
if (price >= _initialPrice.Value + _entryOffset)
{
BuyMarket();
_lastTradePrice = price;
_lastTradeVolume = Volume;
_lastTradeSide = Sides.Buy;
_martingaleLevel = 1;
_initialPrice = null;
}
else if (price <= _initialPrice.Value - _entryOffset)
{
SellMarket();
_lastTradePrice = price;
_lastTradeVolume = Volume;
_lastTradeSide = Sides.Sell;
_martingaleLevel = 1;
_initialPrice = null;
}
return;
}
if (_lastTradeSide is null || _martingaleLevel == 0)
return;
var threshold = _stepSize;
if (_lastTradeSide == Sides.Buy)
{
if (price <= _lastTradePrice - threshold)
{
var nextVolume = _lastTradeVolume * 2m;
var totalVolume = nextVolume + Math.Abs(Position);
SellMarket();
_lastTradePrice = price;
_lastTradeVolume = nextVolume;
_lastTradeSide = Sides.Sell;
_martingaleLevel++;
}
}
else
{
if (price >= _lastTradePrice + threshold)
{
var nextVolume = _lastTradeVolume * 2m;
var totalVolume = nextVolume + Math.Abs(Position);
BuyMarket();
_lastTradePrice = price;
_lastTradeVolume = nextVolume;
_lastTradeSide = Sides.Buy;
_martingaleLevel++;
}
}
}
private void UpdateStepSettings()
{
var priceStep = Security?.PriceStep ?? 0m;
if (priceStep <= 0m)
{
priceStep = 1m;
}
_stepSize = StepPoints * priceStep;
_entryOffset = EntryOffsetPoints * priceStep;
}
private void ResetCycle()
{
_martingaleLevel = 0;
_lastTradePrice = 0m;
_lastTradeVolume = 0m;
_lastTradeSide = null;
}
}
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_martingale_strategy(Strategy):
"""Martingale grid that alternates long/short entries while doubling volume."""
def __init__(self):
super(martin_martingale_strategy, self).__init__()
self._step_points = self.Param("StepPoints", 10) \
.SetGreaterThanZero() \
.SetDisplay("Step (points)", "Distance multiplier for reversals", "General")
self._entry_offset_points = self.Param("EntryOffsetPoints", 10) \
.SetGreaterThanZero() \
.SetDisplay("Entry Offset (points)", "Offset for initial breakout entry", "General")
self._profit_target = self.Param("ProfitTarget", 5.0) \
.SetGreaterThanZero() \
.SetDisplay("Profit Target", "Total profit to close all positions", "Risk")
self._max_level = self.Param("MaxLevel", 5) \
.SetGreaterThanZero() \
.SetDisplay("Max Level", "Maximum martingale levels", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Candles for price monitoring", "Data")
self._step_size = 0.0
self._entry_offset = 0.0
self._last_trade_price = 0.0
self._last_trade_volume = 0.0
self._martingale_level = 0
self._last_trade_side = 0 # 0=none, 1=buy, -1=sell
self._is_closing = False
self._initial_price = None
@property
def StepPoints(self):
return int(self._step_points.Value)
@property
def EntryOffsetPoints(self):
return int(self._entry_offset_points.Value)
@property
def ProfitTarget(self):
return self._profit_target.Value
@property
def MaxLevel(self):
return int(self._max_level.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _update_step_settings(self):
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None and float(sec.PriceStep) > 0 else 1.0
self._step_size = self.StepPoints * step
self._entry_offset = self.EntryOffsetPoints * step
def _reset_cycle(self):
self._martingale_level = 0
self._last_trade_price = 0.0
self._last_trade_volume = 0.0
self._last_trade_side = 0
def OnStarted2(self, time):
super(martin_martingale_strategy, self).OnStarted2(time)
self._update_step_settings()
self._reset_cycle()
self._is_closing = False
self._initial_price = None
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawOwnTrades(area)
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
self._update_step_settings()
if self._step_size <= 0 or float(self.Volume) <= 0:
return
price = float(candle.ClosePrice)
# If closing, flatten and wait
if self._is_closing:
if self.Position == 0:
self._is_closing = False
self._reset_cycle()
return
# If flat after a cycle, reset
if self.Position == 0 and self._martingale_level > 0:
self._reset_cycle()
# Check profit target
if float(self.ProfitTarget) > 0 and float(self.PnL) >= float(self.ProfitTarget) and self.Position != 0:
self._is_closing = True
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
return
# Max level reached
if self._martingale_level >= self.MaxLevel and self.Position != 0:
self._is_closing = True
if self.Position > 0:
self.SellMarket()
elif self.Position < 0:
self.BuyMarket()
return
# Initial entry: wait for breakout from first candle
if self._martingale_level == 0 and self.Position == 0:
if self._initial_price is None:
self._initial_price = price
return
if self._entry_offset <= 0:
return
if price >= self._initial_price + self._entry_offset:
self.BuyMarket()
self._last_trade_price = price
self._last_trade_volume = float(self.Volume)
self._last_trade_side = 1
self._martingale_level = 1
self._initial_price = None
elif price <= self._initial_price - self._entry_offset:
self.SellMarket()
self._last_trade_price = price
self._last_trade_volume = float(self.Volume)
self._last_trade_side = -1
self._martingale_level = 1
self._initial_price = None
return
if self._last_trade_side == 0 or self._martingale_level == 0:
return
threshold = self._step_size
if self._last_trade_side == 1:
if price <= self._last_trade_price - threshold:
self.SellMarket()
self._last_trade_price = price
self._last_trade_volume *= 2.0
self._last_trade_side = -1
self._martingale_level += 1
else:
if price >= self._last_trade_price + threshold:
self.BuyMarket()
self._last_trade_price = price
self._last_trade_volume *= 2.0
self._last_trade_side = 1
self._martingale_level += 1
def OnReseted(self):
super(martin_martingale_strategy, self).OnReseted()
self._reset_cycle()
self._is_closing = False
self._initial_price = None
self._step_size = 0.0
self._entry_offset = 0.0
def CreateClone(self):
return martin_martingale_strategy()