Открыть на GitHub

Стратегия Precipice Martin (C#)

Обзор

Precipice Martin — это механическая сеточная стратегия, которая на закрытии каждого обработанного бара открывает рыночную сделку. В оригинальном советнике MetaTrader 5 при появлении новой свечи одновременно создавались длинная и короткая позиции с фиксированными стоп-лоссом и тейк-профитом, выраженными в пунктах. После убыточных выходов лот умножалcя на коэффициент мартингейла, после прибыльных — возвращался к минимальному значению.

Порт на C# сохраняет ту же логику и использует высокоуровневый API StockSharp. Для каждой завершённой свечи стратегия:

  1. Проверяет активные позиции и закрывает их, если диапазон свечи пробил заданные уровни стоп-лосса или тейк-профита.
  2. В состоянии "без позиции" поочерёдно открывает длинную или короткую сделку (если обе стороны включены), что позволяет воспроизвести поведение исходного робота и при этом не конфликтовать с неттинговым учётом позиций в StockSharp.
  3. Применяет опциональный мартингейл для увеличения объёма после серий убыточных сделок.
  4. Переводит пользовательские значения стопа и профита из пунктов в абсолютные ценовые отступы на основе шага цены инструмента.

Особенности конвертации

  • В 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 Тип свечей (таймфрейм), обрабатываемых стратегией. По умолчанию — минутные бары, но можно выбрать любой доступный интервал.

Логика торговли

  1. Размер пункта — рассчитывается из шага цены инструмента. Для пятизнаков пункт равен десяти тикам, как и в оригинальном советнике.
  2. Выбор направления — при включённых Use Buy и Use Sell стратегия после закрытия позиции чередует лонг и шорт. Если активна только одна сторона, сделки выполняются только в этом направлении.
  3. Установка целей — при открытии позиции фиксируются абсолютные уровни стоп-лосса и тейк-профита, вычисленные из заданного расстояния в пунктах. Нулевое значение отключает защитные уровни для соответствующей стороны.
  4. Выход — на каждом закрытом баре проверяется, пересёк ли минимум/максимум свечи соответствующий уровень. При срабатывании позиция закрывается маркет-ордером с объёмом последнего входа.
  5. Мартингейл — следующий объём равен минимальному лоту инструмента, умноженному на текущий множитель. Убыточный результат (включая нулевой) умножает множитель на MartingaleCoefficient, прибыльный сбрасывает его на 1. Перед отправкой объём округляется по шагу.
  6. Контроль ограничений — если после округления объём стал меньше минимального лота, новый ордер не отправляется, что предотвращает ошибки "недостаточно средств".

Рекомендации по использованию

  1. Настройте таймфрейм параметром Candle Type в соответствии с периодом графика, использовавшимся в MT5.
  2. Подберите расстояния стопа и профита под волатильность инструмента. Помните, что отступы задаются в абсолютной цене.
  3. Используйте мартингейл только при чётком понимании рисков. Рост объёма после каждой потери может быстро увеличить нагрузку на депозит.
  4. Запускайте стратегию на инструменте с поступающими свечами. Алгоритм работает только по завершённым барам.
  5. Следите за маржинальными требованиями: в этой реализации всегда открыта только одна нетто-позиция, но при больших множителях объём может сильно увеличиваться.

Отличия от версии 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;
	}
}