Precipice Martin Strategy (C#)
Overview
The Precipice Martin strategy is a mechanical grid approach that opens one market order at the close of every processed candle. The original MetaTrader 5 expert advisor created a symmetrical buy and sell position on every new bar and managed exits using static stop-loss and take-profit offsets that were expressed in pips. Losing trades increased the next order size by a martingale multiplier, while profitable trades reset the position size to the minimum lot.
This C# port follows the same high-level logic using the StockSharp high-level API. For each finished candle the strategy:
- Updates existing long and short positions and closes them if the candle range pierced the configured stop-loss or take-profit level.
- When flat, alternates between opening a long or a short market position (when both directions are enabled) to emulate the dual-entry behaviour of the source robot while remaining compatible with StockSharp's net-position accounting.
- Applies optional martingale sizing so that consecutive losing trades increase volume by the configured multiplier.
- Computes stop-loss and take-profit targets from user-defined pip distances that are translated to absolute price offsets based on the security tick size.
Conversion Notes
- The original EA opened a long and a short position on every new bar when both toggles were enabled. Because StockSharp uses net positions by default, the C# version alternates between directions on consecutive opportunities to avoid instantly flattening the net position. This still ensures both sides of the market are traded over time.
- Stop-loss and take-profit management is performed internally by checking whether a candle's high/low would have triggered the corresponding level. When a level is hit the strategy closes the position using a market order and records the realised profit or loss for the martingale logic.
- Lot validation replicates the
LotCheck routine from MQL5 by rounding the calculated volume to the exchange VolumeStep, enforcing the minimum and maximum bounds, and cancelling the order if the rounded value becomes zero.
- The martingale routine mirrors
CalculateLot: any non-profitable exit multiplies the next order size by MartingaleCoefficient, while a profitable exit resets the multiplier to one.
Parameters
| Parameter |
Description |
| Use Buy |
Enables opening long positions. |
| Buy SL/TP (pips) |
Distance (in pips) used for both the stop-loss and take-profit of long trades. A value of 0 disables exits for that side. |
| Use Sell |
Enables opening short positions. |
| Sell SL/TP (pips) |
Distance (in pips) used for both the stop-loss and take-profit of short trades. |
| Use Martingale |
Toggles martingale position sizing. When disabled every order uses the minimum lot size. |
| Martingale Coefficient |
Multiplier applied to the minimum lot after each non-profitable trade. |
| Candle Type |
Timeframe of the candles processed by the strategy. By default the strategy works on one-minute bars but any available timeframe can be selected. |
Trading Logic
- Pip Size Calculation – the strategy derives the pip value from the security tick size. For instruments quoted with fractional pips (5-digit FX symbols) the pip is considered 10 ticks, matching the MT5 implementation.
- Entry Selection – if both
Use Buy and Use Sell are enabled, the strategy alternates between long and short entries whenever it is flat. If only one direction is enabled, all trades follow that direction. Entries are triggered immediately after a candle is completed and the strategy is online.
- Stop/Take Levels – when a trade is opened, the stop-loss and take-profit are stored as absolute prices relative to the entry using the selected pip distance. A value of zero disables both levels for that direction.
- Exit Handling – on each finished candle the high/low values are checked. If the low breaches the long stop or the high breaches the long target, the long position is closed. For shorts the logic is mirrored. Exits are executed with market orders using the last recorded volume for that position.
- Martingale Sizing – the next order volume equals the instrument minimum lot multiplied by the current martingale multiplier. Losing trades (including break-even results) multiply the multiplier by
MartingaleCoefficient; profitable trades reset it to one. Volume rounding to the exchange step is applied before the order is submitted.
- Safety Checks – if the rounded volume is below the exchange minimum lot the order is skipped, preventing "not enough money" errors that the original EA handled via
CheckVolume.
Usage Guidelines
- Configure the desired timeframe in Candle Type to match the chart period used in MT5.
- Adjust the pip distances to match the desired stop-loss and take-profit behaviour. Remember that the offsets are absolute prices, so the actual stop in currency depends on the symbol.
- Enable or disable martingale sizing according to your risk tolerance. Because volume grows exponentially after consecutive losses, apply conservative multipliers.
- Deploy the strategy on a security that provides real-time candles. The strategy requires completed bars to operate and will not trade on incomplete candles.
- Monitor margin usage when martingale is active. The StockSharp version intentionally alternates directions when both sides are enabled, so only one net position is open at any moment.
Differences from the MT5 Implementation
- Net Positions – the alternation logic replaces the simultaneous hedged entries of the original algorithm. If a true hedging account is required you can run two instances of the strategy (one with
Use Buy, another with Use Sell).
- Order Placement – protective orders are not placed on the exchange book. Instead, exits are executed via market orders when the strategy detects that the stop or take level was crossed.
- History Scan – the MT5 script recalculated the martingale coefficient by scanning the entire trade history on every tick. The C# version maintains the multiplier incrementally to reduce overhead while preserving behaviour.
Risk Disclaimer
Martingale-based strategies can generate very large positions during losing streaks, which can exceed account risk limits. Always test the strategy on simulated data before live deployment and ensure that the selected multiplier and pip distances suit the volatility of the traded instrument.
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()