A estratégia reproduz o comportamento do Consultor Especializado original "Martin" do MQL executando uma grade martingale com cobertura em torno do preço atual. Ela alterna continuamente posições compradas e vendidas, dobrando o volume negociado a cada reversão até que o lucro acumulado de toda a cesta atinja o alvo configurado. Os candles são usados apenas como motor para a lógica de decisão, enquanto as execuções reais dependem de ordens a mercado e ordens stop expostas pela API de alto nível do StockSharp.
Como funciona
Na inicialização, a estratégia lê o PriceStep do instrumento para converter os parâmetros EntryOffsetPoints e StepPoints em distâncias de preço absolutas. Se o passo de preço não estiver disponível, o valor 1 é assumido.
Quando não há posição aberta nem ciclo martingale ativo, a estratégia coloca uma ordem stop de compra e uma ordem stop de venda em torno do último fechamento. Os offsets são EntryOffsetPoints * PriceStep, o que coincide com a distância de 10 pontos usada no código MQL original.
Quando uma das ordens stop é executada, a ordem pendente oposta é cancelada. A execução define o primeiro trade da sequência martingale: a estratégia armazena seu preço, volume e direção, e define o contador de nível interno como 1.
A cada fechamento de candle subsequente, o preço de fechamento atual é comparado ao preço da última ordem executada. Se o mercado se moveu contra essa ordem pelo menos martingaleLevel * StepPoints * PriceStep, uma ordem a mercado é enviada na direção oposta com volume dobrado em relação ao trade anterior. As informações do último trade são atualizadas após cada execução.
O lucro não realizado é avaliado como PnL + Position * (closePrice - PositionPrice). Quando esse lucro agregado supera o parâmetro ProfitTarget, a estratégia envia CloseAll() para nivelar cada posição na cesta, cancela todas as ordens restantes e reinicia o ciclo para que um novo par de ordens stop possa ser colocado.
O mesmo reinício também ocorre automaticamente quando todas as posições são fechadas manualmente: os contadores internos são limpos e novas ordens stop serão criadas no próximo candle.
Este fluxo de trabalho espelha a lógica de compra/venda alternada do Consultor Especializado original enquanto mantém a implementação totalmente dentro da API de alto nível do StockSharp.
Parâmetros
StepPoints – número de passos de preço usados para calcular o limite de reversão para a próxima ordem de média. Padrão 10 e pode ser otimizado.
EntryOffsetPoints – offset para as ordens stop iniciais de compra/venda em passos de preço. Também padrão 10 pontos como a versão MQL.
ProfitTarget – lucro absoluto em moeda necessário para fechar toda a cesta martingale. Uma vez que o PnL combinado realizado e não realizado supera esse valor, todas as posições são liquidadas.
CandleType – assinatura de candles usada para impulsionar a lógica da estratégia. O padrão é o período de um minuto, mas qualquer DataType suportado pelo local pode ser selecionado.
O tamanho base do trade é retirado da propriedade Volume da estratégia. Cada nova reversão multiplica essa base por potências de dois da maneira martingale clássica.
Notas práticas
Sempre configure Volume para corresponder ao tamanho mínimo de lote do broker. O esquema de duplicação aumenta rapidamente a exposição, portanto os limites de risco devem ser aplicados externamente.
Como a colocação de ordens é impulsionada por fechamentos de candles, movimentos de preço rápidos dentro do candle podem acionar entradas ligeiramente mais tarde do que a versão MQL baseada em ticks. No entanto, as ordens stop mantêm os preços de entrada alinhados com a lógica original.
A estratégia desenha candles de preço e trades próprios na área de gráfico padrão para rastreamento visual mais fácil.
Nenhum stop-loss automático é usado. A única condição de saída é o ProfitTarget, portanto o instrumento e o período devem ser escolhidos cuidadosamente para controlar o risco de grandes tendências adversas.
Diferenças em relação ao Especialista MQL
O StockSharp usa posições líquidas, portanto cada reversão é executada com uma ordem a mercado que fecha a exposição anterior e abre a nova em um único trade. O PnL cumulativo da cesta permanece idêntico à implementação com cobertura.
A lógica tick a tick foi substituída por fechamentos de candles para avaliação de sinais, a fim de permanecer dentro do uso recomendado da API de alto nível.
Os identificadores de ordens são rastreados para evitar processar execuções parciais múltiplas vezes, garantindo que a lógica de duplicação de volume permaneça consistente.
Essas mudanças mantêm o comportamento de negociação fiel à estratégia fonte enquanto a adaptam ao framework do 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()