Estratégia Precipice Martin (C#)
Visão Geral
A estratégia Precipice Martin é uma abordagem de grid mecânica que abre uma ordem a mercado no fechamento de cada vela processada. O consultor especializado original do MetaTrader 5 criava uma posição de compra e venda simétrica em cada nova barra e gerenciava as saídas usando offsets estáticos de stop-loss e take-profit expressos em pips. Operações perdedoras aumentavam o tamanho da próxima ordem por um multiplicador martingale, enquanto operações lucrativas reajustavam o tamanho da posição ao lote mínimo.
Este port em C# segue a mesma lógica de alto nível usando a API de alto nível do StockSharp. Para cada vela terminada a estratégia:
- Atualiza as posições compradas e vendidas existentes e as fecha se o intervalo da vela penetrou o nível de stop-loss ou take-profit configurado.
- Quando plana, alterna entre abrir uma posição de mercado comprada ou vendida (quando ambas as direções estão habilitadas) para emular o comportamento de entrada dupla do robô fonte enquanto permanece compatível com a contabilidade de posição líquida do StockSharp.
- Aplica dimensionamento martingale opcional para que operações perdedoras consecutivas aumentem o volume pelo multiplicador configurado.
- Calcula os alvos de stop-loss e take-profit a partir de distâncias de pips definidas pelo usuário que são traduzidas em offsets de preço absolutos com base no tamanho do tick do instrumento.
Notas de Conversão
- O EA original abria uma posição comprada e vendida em cada nova barra quando ambos os toggles estavam habilitados. Como o StockSharp usa posições líquidas por padrão, a versão em C# alterna entre direções em oportunidades consecutivas para evitar aplanar instantaneamente a posição líquida. Isso ainda garante que ambos os lados do mercado sejam negociados ao longo do tempo.
- O gerenciamento de stop-loss e take-profit é realizado internamente verificando se o máximo/mínimo de uma vela teria acionado o nível correspondente. Quando um nível é atingido, a estratégia fecha a posição usando uma ordem a mercado e registra o lucro ou perda realizado para a lógica martingale.
- A validação de lotes replica a rotina
LotCheck do MQL5 arredondando o volume calculado para o VolumeStep do exchange, aplicando os limites mínimo e máximo, e cancelando a ordem se o valor arredondado se tornar zero.
- A rotina martingale reflete
CalculateLot: qualquer saída não lucrativa multiplica o tamanho da próxima ordem por MartingaleCoefficient, enquanto uma saída lucrativa reajusta o multiplicador para um.
Parâmetros
| Parâmetro |
Descrição |
| Use Buy |
Habilita a abertura de posições compradas. |
| Buy SL/TP (pips) |
Distância (em pips) usada tanto para o stop-loss como para o take-profit das operações compradas. Um valor de 0 desabilita as saídas para aquele lado. |
| Use Sell |
Habilita a abertura de posições vendidas. |
| Sell SL/TP (pips) |
Distância (em pips) usada tanto para o stop-loss como para o take-profit das operações vendidas. |
| Use Martingale |
Alterna o dimensionamento de posição martingale. Quando desabilitado, cada ordem usa o tamanho de lote mínimo. |
| Martingale Coefficient |
Multiplicador aplicado ao lote mínimo após cada operação não lucrativa. |
| Candle Type |
Período das velas processadas pela estratégia. Por padrão a estratégia trabalha em barras de um minuto, mas qualquer período disponível pode ser selecionado. |
Lógica de Trading
- Cálculo do Tamanho do Pip – a estratégia deriva o valor do pip do tamanho do tick do instrumento. Para instrumentos cotados com pips fracionários (símbolos FX de 5 dígitos) o pip é considerado 10 ticks, correspondendo à implementação MT5.
- Seleção de Entrada – se tanto
Use Buy como Use Sell estão habilitados, a estratégia alterna entre entradas compradas e vendidas sempre que estiver plana. Se apenas uma direção estiver habilitada, todas as operações seguem essa direção. As entradas são acionadas imediatamente após a conclusão de uma vela e a estratégia estar online.
- Níveis de Stop/Take – quando uma operação é aberta, o stop-loss e take-profit são armazenados como preços absolutos relativos à entrada usando a distância de pips selecionada. Um valor de zero desabilita ambos os níveis para aquela direção.
- Tratamento de Saídas – em cada vela terminada os valores de máximo/mínimo são verificados. Se o mínimo viola o stop comprado ou o máximo viola o alvo comprado, a posição comprada é fechada. Para vendidos, a lógica é espelhada. As saídas são executadas com ordens a mercado usando o último volume registrado para aquela posição.
- Dimensionamento Martingale – o volume da próxima ordem é igual ao lote mínimo do instrumento multiplicado pelo multiplicador martingale atual. Operações perdedoras (incluindo resultados de empate) multiplicam o multiplicador por
MartingaleCoefficient; operações lucrativas o reajustam para um. O arredondamento de volume para o passo do exchange é aplicado antes de enviar a ordem.
- Verificações de Segurança – se o volume arredondado estiver abaixo do lote mínimo do exchange, a ordem é ignorada, evitando erros de "fundos insuficientes" que o EA original tratava via
CheckVolume.
Diretrizes de Uso
- Configure o período desejado em Candle Type para corresponder ao período do gráfico usado no MT5.
- Ajuste as distâncias em pips para corresponder ao comportamento desejado de stop-loss e take-profit. Lembre-se de que os offsets são preços absolutos, então o stop real em moeda depende do símbolo.
- Habilite ou desabilite o dimensionamento martingale de acordo com sua tolerância ao risco. Como o volume cresce exponencialmente após perdas consecutivas, aplique multiplicadores conservadores.
- Implante a estratégia em um instrumento que forneça velas em tempo real. A estratégia requer barras completas para operar e não negociará em velas incompletas.
- Monitore o uso de margem quando o martingale estiver ativo. A versão StockSharp alterna intencionalmente direções quando ambos os lados estão habilitados, de modo que apenas uma posição líquida esteja aberta em qualquer momento.
Diferenças da Implementação MT5
- Posições Líquidas – a lógica de alternância substitui as entradas hedgeadas simultâneas do algoritmo original. Se uma conta de hedge verdadeiro for necessária, você pode executar duas instâncias da estratégia (uma com
Use Buy, outra com Use Sell).
- Colocação de Ordens – as ordens protetoras não são colocadas no livro do exchange. Em vez disso, as saídas são executadas via ordens a mercado quando a estratégia detecta que o nível de stop ou take foi cruzado.
- Varredura de Histórico – o script MT5 recalculava o coeficiente martingale varrendo todo o histórico de negociações a cada tick. A versão em C# mantém o multiplicador incrementalmente para reduzir a sobrecarga enquanto preserva o comportamento.
Aviso de Risco
Estratégias baseadas em martingale podem gerar posições muito grandes durante sequências de perdas, que podem exceder os limites de risco da conta. Sempre teste a estratégia em dados simulados antes da implantação ao vivo e certifique-se de que o multiplicador selecionado e as distâncias em pips sejam adequados para a volatilidade do instrumento negociado.
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>
/// Grid style strategy that opens a position on every new bar with optional martingale sizing.
/// </summary>
public class PrecipiceMartinStrategy : Strategy
{
private readonly StrategyParam<bool> _useBuy;
private readonly StrategyParam<int> _buyStepPips;
private readonly StrategyParam<bool> _useSell;
private readonly StrategyParam<int> _sellStepPips;
private readonly StrategyParam<bool> _useMartingale;
private readonly StrategyParam<decimal> _martingaleCoefficient;
private readonly StrategyParam<DataType> _candleType;
private decimal _pipSize;
private decimal _martingaleMultiplier;
private decimal? _longEntryPrice;
private decimal? _longStopPrice;
private decimal? _longTakePrice;
private decimal? _shortEntryPrice;
private decimal? _shortStopPrice;
private decimal? _shortTakePrice;
private decimal _lastLongVolume;
private decimal _lastShortVolume;
private bool _preferLongEntry;
public bool UseBuy
{
get => _useBuy.Value;
set => _useBuy.Value = value;
}
public int BuyStepPips
{
get => _buyStepPips.Value;
set => _buyStepPips.Value = value;
}
public bool UseSell
{
get => _useSell.Value;
set => _useSell.Value = value;
}
public int SellStepPips
{
get => _sellStepPips.Value;
set => _sellStepPips.Value = value;
}
public bool UseMartingale
{
get => _useMartingale.Value;
set => _useMartingale.Value = value;
}
public decimal MartingaleCoefficient
{
get => _martingaleCoefficient.Value;
set => _martingaleCoefficient.Value = value;
}
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
public PrecipiceMartinStrategy()
{
_useBuy = Param(nameof(UseBuy), true)
.SetDisplay("Use Buy", "Enable opening long positions", "Trading");
_buyStepPips = Param(nameof(BuyStepPips), 89)
.SetDisplay("Buy SL/TP (pips)", "Stop loss and take profit distance for longs", "Trading");
_useSell = Param(nameof(UseSell), true)
.SetDisplay("Use Sell", "Enable opening short positions", "Trading");
_sellStepPips = Param(nameof(SellStepPips), 89)
.SetDisplay("Sell SL/TP (pips)", "Stop loss and take profit distance for shorts", "Trading");
_useMartingale = Param(nameof(UseMartingale), true)
.SetDisplay("Use Martingale", "Increase volume after losing trades", "Position sizing");
_martingaleCoefficient = Param(nameof(MartingaleCoefficient), 1.6m)
.SetDisplay("Martingale Coefficient", "Multiplier applied after losses", "Position sizing")
.SetGreaterThanZero();
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
.SetDisplay("Candle Type", "Timeframe used to generate trading bars", "General");
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_pipSize = 0m;
_martingaleMultiplier = 1m;
_longEntryPrice = null;
_longStopPrice = null;
_longTakePrice = null;
_shortEntryPrice = null;
_shortStopPrice = null;
_shortTakePrice = null;
_lastLongVolume = 0m;
_lastShortVolume = 0m;
_preferLongEntry = true;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
// Calculate the pip size based on the instrument tick size.
_pipSize = (Security?.PriceStep ?? 1m) * 10m;
if (_pipSize <= 0m)
_pipSize = Security?.PriceStep ?? 1m;
if (_pipSize <= 0m)
_pipSize = 1m;
_martingaleMultiplier = 1m;
// Subscribe to candle data and process every completed bar.
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(ProcessCandle)
.Start();
}
private void ProcessCandle(ICandleMessage candle)
{
// Ignore unfinished candles because the original strategy trades on bar close.
if (candle.State != CandleStates.Finished)
return;
// Manage exits before looking for new entries.
var closedLong = TryCloseLong(candle);
var closedShort = TryCloseShort(candle);
// Do not open new trades while any position is still active.
if (Position != 0)
return;
// Avoid immediate re-entry for a direction that has just closed on this bar.
if (closedLong)
return;
if (closedShort)
return;
if (_longEntryPrice.HasValue || _shortEntryPrice.HasValue)
return;
if (UseBuy && UseSell)
{
if (_preferLongEntry)
{
if (TryEnterLong(candle))
{
_preferLongEntry = false;
return;
}
if (TryEnterShort(candle))
{
_preferLongEntry = false;
}
}
else
{
if (TryEnterShort(candle))
{
_preferLongEntry = true;
return;
}
if (TryEnterLong(candle))
{
_preferLongEntry = true;
}
}
}
else
{
if (UseBuy)
{
TryEnterLong(candle);
}
if (UseSell)
{
TryEnterShort(candle);
}
}
}
private bool TryEnterLong(ICandleMessage candle)
{
// Prevent duplicate long entries.
if (_longEntryPrice.HasValue)
return false;
// Ensure no net position exists before opening a new long.
if (Position != 0)
return false;
var volume = CalculateOrderVolume();
if (volume <= 0m)
return false;
var entryPrice = candle.ClosePrice;
Volume = volume;
BuyMarket();
_longEntryPrice = entryPrice;
_lastLongVolume = volume;
if (BuyStepPips > 0)
{
var offset = BuyStepPips * _pipSize;
_longStopPrice = entryPrice - offset;
_longTakePrice = entryPrice + offset;
}
else
{
_longStopPrice = null;
_longTakePrice = null;
}
return true;
}
private bool TryEnterShort(ICandleMessage candle)
{
// Prevent duplicate short entries.
if (_shortEntryPrice.HasValue)
return false;
// Ensure no net position exists before opening a new short.
if (Position != 0)
return false;
var volume = CalculateOrderVolume();
if (volume <= 0m)
return false;
var entryPrice = candle.ClosePrice;
Volume = volume;
SellMarket();
_shortEntryPrice = entryPrice;
_lastShortVolume = volume;
if (SellStepPips > 0)
{
var offset = SellStepPips * _pipSize;
_shortStopPrice = entryPrice + offset;
_shortTakePrice = entryPrice - offset;
}
else
{
_shortStopPrice = null;
_shortTakePrice = null;
}
return true;
}
private bool TryCloseLong(ICandleMessage candle)
{
if (!_longEntryPrice.HasValue)
return false;
var volume = Position;
if (volume <= 0m)
volume = _lastLongVolume;
if (volume <= 0m)
return false;
var stopHit = _longStopPrice.HasValue && candle.LowPrice <= _longStopPrice.Value;
var takeHit = _longTakePrice.HasValue && candle.HighPrice >= _longTakePrice.Value;
if (!stopHit && !takeHit)
return false;
var exitPrice = stopHit ? _longStopPrice!.Value : _longTakePrice!.Value;
SellMarket();
var pnl = (exitPrice - _longEntryPrice.Value) * volume;
UpdateMartingale(pnl);
ResetLongState();
return true;
}
private bool TryCloseShort(ICandleMessage candle)
{
if (!_shortEntryPrice.HasValue)
return false;
var volume = Math.Abs(Position);
if (volume <= 0m)
volume = _lastShortVolume;
if (volume <= 0m)
return false;
var stopHit = _shortStopPrice.HasValue && candle.HighPrice >= _shortStopPrice.Value;
var takeHit = _shortTakePrice.HasValue && candle.LowPrice <= _shortTakePrice.Value;
if (!stopHit && !takeHit)
return false;
var exitPrice = stopHit ? _shortStopPrice!.Value : _shortTakePrice!.Value;
BuyMarket();
var pnl = (_shortEntryPrice.Value - exitPrice) * volume;
UpdateMartingale(pnl);
ResetShortState();
return true;
}
private decimal CalculateOrderVolume()
{
var minVolume = Security?.MinVolume ?? Volume;
if (minVolume <= 0m)
minVolume = 1m;
var multiplier = UseMartingale ? _martingaleMultiplier : 1m;
var volume = minVolume * multiplier;
return AdjustVolume(volume);
}
private decimal AdjustVolume(decimal volume)
{
var step = Security?.VolumeStep;
if (step.HasValue && step.Value > 0m)
{
var steps = Math.Truncate(volume / step.Value);
volume = steps * step.Value;
}
var min = Security?.MinVolume;
if (min.HasValue && min.Value > 0m && volume < min.Value)
volume = 0m;
var max = Security?.MaxVolume;
if (max.HasValue && max.Value > 0m && volume > max.Value)
volume = max.Value;
return volume;
}
private void UpdateMartingale(decimal realizedPnl)
{
if (!UseMartingale)
{
_martingaleMultiplier = 1m;
return;
}
// Reset the multiplier after profitable trades and scale up after losses.
_martingaleMultiplier = realizedPnl > 0m
? 1m
: _martingaleMultiplier * MartingaleCoefficient;
}
private void ResetLongState()
{
_longEntryPrice = null;
_longStopPrice = null;
_longTakePrice = null;
_lastLongVolume = 0m;
}
private void ResetShortState()
{
_shortEntryPrice = null;
_shortStopPrice = null;
_shortTakePrice = null;
_lastShortVolume = 0m;
}
}
import clr
import math
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 precipice_martin_strategy(Strategy):
"""Precipice Martin: grid strategy with alternating long/short entries and martingale sizing."""
def __init__(self):
super(precipice_martin_strategy, self).__init__()
self._use_buy = self.Param("UseBuy", True) \
.SetDisplay("Use Buy", "Enable opening long positions", "Trading")
self._buy_step_pips = self.Param("BuyStepPips", 89) \
.SetDisplay("Buy SL/TP (pips)", "Stop loss and take profit distance for longs", "Trading")
self._use_sell = self.Param("UseSell", True) \
.SetDisplay("Use Sell", "Enable opening short positions", "Trading")
self._sell_step_pips = self.Param("SellStepPips", 89) \
.SetDisplay("Sell SL/TP (pips)", "Stop loss and take profit distance for shorts", "Trading")
self._use_martingale = self.Param("UseMartingale", True) \
.SetDisplay("Use Martingale", "Increase volume after losing trades", "Position sizing")
self._martingale_coefficient = self.Param("MartingaleCoefficient", 1.6) \
.SetGreaterThanZero() \
.SetDisplay("Martingale Coefficient", "Multiplier applied after losses", "Position sizing")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(4))) \
.SetDisplay("Candle Type", "Timeframe used to generate trading bars", "General")
self._pip_size = 0.0
self._martingale_multiplier = 1.0
self._long_entry_price = None
self._long_stop_price = None
self._long_take_price = None
self._short_entry_price = None
self._short_stop_price = None
self._short_take_price = None
self._last_long_volume = 0.0
self._last_short_volume = 0.0
self._prefer_long_entry = True
@property
def UseBuy(self):
return self._use_buy.Value
@property
def BuyStepPips(self):
return int(self._buy_step_pips.Value)
@property
def UseSell(self):
return self._use_sell.Value
@property
def SellStepPips(self):
return int(self._sell_step_pips.Value)
@property
def UseMartingale(self):
return self._use_martingale.Value
@property
def MartingaleCoefficient(self):
return float(self._martingale_coefficient.Value)
@property
def CandleType(self):
return self._candle_type.Value
def _adjust_volume(self, volume):
sec = self.Security
if sec is not None and sec.VolumeStep is not None:
step = float(sec.VolumeStep)
if step > 0:
volume = math.floor(volume / step) * step
if sec is not None and sec.MinVolume is not None:
min_v = float(sec.MinVolume)
if min_v > 0 and volume < min_v:
volume = 0.0
if sec is not None and sec.MaxVolume is not None:
max_v = float(sec.MaxVolume)
if max_v > 0 and volume > max_v:
volume = max_v
return volume
def _calculate_order_volume(self):
sec = self.Security
min_volume = float(sec.MinVolume) if sec is not None and sec.MinVolume is not None else self.Volume
if min_volume <= 0:
min_volume = 1.0
multiplier = self._martingale_multiplier if self.UseMartingale else 1.0
volume = min_volume * multiplier
return self._adjust_volume(volume)
def _update_martingale(self, realized_pnl):
if not self.UseMartingale:
self._martingale_multiplier = 1.0
return
if realized_pnl > 0:
self._martingale_multiplier = 1.0
else:
self._martingale_multiplier *= self.MartingaleCoefficient
def OnStarted2(self, time):
super(precipice_martin_strategy, self).OnStarted2(time)
sec = self.Security
step = float(sec.PriceStep) if sec is not None and sec.PriceStep is not None else 1.0
self._pip_size = step * 10.0
if self._pip_size <= 0:
self._pip_size = step if step > 0 else 1.0
self._martingale_multiplier = 1.0
self._long_entry_price = None
self._long_stop_price = None
self._long_take_price = None
self._short_entry_price = None
self._short_stop_price = None
self._short_take_price = None
self._last_long_volume = 0.0
self._last_short_volume = 0.0
self._prefer_long_entry = True
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self.process_candle).Start()
def process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
close = float(candle.ClosePrice)
closed_long = self._try_close_long(candle)
closed_short = self._try_close_short(candle)
if self.Position != 0:
return
if closed_long or closed_short:
return
if self._long_entry_price is not None or self._short_entry_price is not None:
return
if self.UseBuy and self.UseSell:
if self._prefer_long_entry:
if self._try_enter_long(candle):
self._prefer_long_entry = False
return
if self._try_enter_short(candle):
self._prefer_long_entry = False
else:
if self._try_enter_short(candle):
self._prefer_long_entry = True
return
if self._try_enter_long(candle):
self._prefer_long_entry = True
else:
if self.UseBuy:
self._try_enter_long(candle)
if self.UseSell:
self._try_enter_short(candle)
def _try_enter_long(self, candle):
if self._long_entry_price is not None:
return False
if self.Position != 0:
return False
volume = self._calculate_order_volume()
if volume <= 0:
return False
entry_price = float(candle.ClosePrice)
self.BuyMarket()
self._long_entry_price = entry_price
self._last_long_volume = volume
if self.BuyStepPips > 0:
offset = self.BuyStepPips * self._pip_size
self._long_stop_price = entry_price - offset
self._long_take_price = entry_price + offset
else:
self._long_stop_price = None
self._long_take_price = None
return True
def _try_enter_short(self, candle):
if self._short_entry_price is not None:
return False
if self.Position != 0:
return False
volume = self._calculate_order_volume()
if volume <= 0:
return False
entry_price = float(candle.ClosePrice)
self.SellMarket()
self._short_entry_price = entry_price
self._last_short_volume = volume
if self.SellStepPips > 0:
offset = self.SellStepPips * self._pip_size
self._short_stop_price = entry_price + offset
self._short_take_price = entry_price - offset
else:
self._short_stop_price = None
self._short_take_price = None
return True
def _try_close_long(self, candle):
if self._long_entry_price is None:
return False
volume = self.Position
if volume <= 0:
volume = self._last_long_volume
if volume <= 0:
return False
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
stop_hit = self._long_stop_price is not None and lo <= self._long_stop_price
take_hit = self._long_take_price is not None and h >= self._long_take_price
if not stop_hit and not take_hit:
return False
exit_price = self._long_stop_price if stop_hit else self._long_take_price
self.SellMarket()
pnl = (exit_price - self._long_entry_price) * volume
self._update_martingale(pnl)
self._reset_long_state()
return True
def _try_close_short(self, candle):
if self._short_entry_price is None:
return False
volume = abs(self.Position)
if volume <= 0:
volume = self._last_short_volume
if volume <= 0:
return False
h = float(candle.HighPrice)
lo = float(candle.LowPrice)
stop_hit = self._short_stop_price is not None and h >= self._short_stop_price
take_hit = self._short_take_price is not None and lo <= self._short_take_price
if not stop_hit and not take_hit:
return False
exit_price = self._short_stop_price if stop_hit else self._short_take_price
self.BuyMarket()
pnl = (self._short_entry_price - exit_price) * volume
self._update_martingale(pnl)
self._reset_short_state()
return True
def _reset_long_state(self):
self._long_entry_price = None
self._long_stop_price = None
self._long_take_price = None
self._last_long_volume = 0.0
def _reset_short_state(self):
self._short_entry_price = None
self._short_stop_price = None
self._short_take_price = None
self._last_short_volume = 0.0
def OnReseted(self):
super(precipice_martin_strategy, self).OnReseted()
self._pip_size = 0.0
self._martingale_multiplier = 1.0
self._long_entry_price = None
self._long_stop_price = None
self._long_take_price = None
self._short_entry_price = None
self._short_stop_price = None
self._short_take_price = None
self._last_long_volume = 0.0
self._last_short_volume = 0.0
self._prefer_long_entry = True
def CreateClone(self):
return precipice_martin_strategy()