Precipice Martin — это механическая сеточная стратегия, которая на закрытии каждого обработанного бара открывает рыночную сделку. В оригинальном советнике MetaTrader 5 при появлении новой свечи одновременно создавались длинная и короткая позиции с фиксированными стоп-лоссом и тейк-профитом, выраженными в пунктах. После убыточных выходов лот умножалcя на коэффициент мартингейла, после прибыльных — возвращался к минимальному значению.
Порт на C# сохраняет ту же логику и использует высокоуровневый API StockSharp. Для каждой завершённой свечи стратегия:
Проверяет активные позиции и закрывает их, если диапазон свечи пробил заданные уровни стоп-лосса или тейк-профита.
В состоянии "без позиции" поочерёдно открывает длинную или короткую сделку (если обе стороны включены), что позволяет воспроизвести поведение исходного робота и при этом не конфликтовать с неттинговым учётом позиций в StockSharp.
Применяет опциональный мартингейл для увеличения объёма после серий убыточных сделок.
Переводит пользовательские значения стопа и профита из пунктов в абсолютные ценовые отступы на основе шага цены инструмента.
Особенности конвертации
В MT5 одновременно открывались buy и sell. В StockSharp мы чередуем направления при каждом новом входе, чтобы избежать мгновенного закрытия нетто-позиции и всё же обеспечить торговлю в обе стороны.
Стоп-лосс и тейк-профит контролируются внутри стратегии. Как только минимум или максимум свечи достигает уровня, позиция закрывается рыночным ордером, а результат фиксируется для блока мартингейла.
Проверка объёма повторяет функцию LotCheck: рассчитанный лот округляется к шагу объёма, при необходимости обрезается по минимуму и максимуму, и если после округления он становится нулём, новая сделка пропускается.
Расчёт множителя мартингейла соответствует функции CalculateLot: любая неприбыльная сделка умножает текущий множитель на MartingaleCoefficient, прибыльная — сбрасывает его на единицу.
Параметры
Параметр
Описание
Use Buy
Разрешение на открытие длинных позиций.
Buy SL/TP (pips)
Расстояние в пунктах до стоп-лосса и тейк-профита длинных сделок. Значение 0 отключает выходы для лонга.
Use Sell
Разрешение на открытие коротких позиций.
Sell SL/TP (pips)
Расстояние в пунктах до стоп-лосса и тейк-профита коротких сделок.
Use Martingale
Включает мартингейл. При отключении используется минимальный лот.
Martingale Coefficient
Множитель, которым умножается лот после убыточной сделки.
Candle Type
Тип свечей (таймфрейм), обрабатываемых стратегией. По умолчанию — минутные бары, но можно выбрать любой доступный интервал.
Логика торговли
Размер пункта — рассчитывается из шага цены инструмента. Для пятизнаков пункт равен десяти тикам, как и в оригинальном советнике.
Выбор направления — при включённых Use Buy и Use Sell стратегия после закрытия позиции чередует лонг и шорт. Если активна только одна сторона, сделки выполняются только в этом направлении.
Установка целей — при открытии позиции фиксируются абсолютные уровни стоп-лосса и тейк-профита, вычисленные из заданного расстояния в пунктах. Нулевое значение отключает защитные уровни для соответствующей стороны.
Выход — на каждом закрытом баре проверяется, пересёк ли минимум/максимум свечи соответствующий уровень. При срабатывании позиция закрывается маркет-ордером с объёмом последнего входа.
Мартингейл — следующий объём равен минимальному лоту инструмента, умноженному на текущий множитель. Убыточный результат (включая нулевой) умножает множитель на MartingaleCoefficient, прибыльный сбрасывает его на 1. Перед отправкой объём округляется по шагу.
Контроль ограничений — если после округления объём стал меньше минимального лота, новый ордер не отправляется, что предотвращает ошибки "недостаточно средств".
Рекомендации по использованию
Настройте таймфрейм параметром Candle Type в соответствии с периодом графика, использовавшимся в MT5.
Подберите расстояния стопа и профита под волатильность инструмента. Помните, что отступы задаются в абсолютной цене.
Используйте мартингейл только при чётком понимании рисков. Рост объёма после каждой потери может быстро увеличить нагрузку на депозит.
Запускайте стратегию на инструменте с поступающими свечами. Алгоритм работает только по завершённым барам.
Следите за маржинальными требованиями: в этой реализации всегда открыта только одна нетто-позиция, но при больших множителях объём может сильно увеличиваться.
Отличия от версии MT5
Неттинг — чередование направлений заменяет одновременное открытие buy и sell. Для истинного хеджирования можно запустить две копии стратегии с разными настройками направления.
Защитные ордера — стопы и тейки не выставляются в стакан, а контролируются внутренней логикой и закрываются рынком.
История сделок — вместо сканирования всей истории при каждом тике множитель мартингейла обновляется инкрементно после каждой сделки, что снижает нагрузку.
Предупреждение о рисках
Мартингейл способен очень быстро наращивать объём позиции в серии убыточных сделок. Перед запуском на реальном счёте протестируйте стратегию на истории и убедитесь, что выбранный коэффициент и расстояния стопов соответствуют волатильности инструмента и размеру капитала.
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()