Открыть на GitHub

Стратегия Sniper Jaw

Sniper Jaw Strategy переносит советник MetaTrader 4 SniperJawEA.mq4 на высокоуровневый API StockSharp. Система анализирует индикатор Аллигатор Билла Вильямса на медианной цене свечи. Сделка инициируется только тогда, когда три сглаженные скользящие средние (челюсть, зубы и губы) расположены в строгом бычьем или медвежьем порядке и одновременно движутся в ту же сторону, что и на предыдущей завершённой свече.

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

  1. Восстановление Аллигатора – три индикатора SmoothedMovingAverage рассчитывают линии челюсти, зубов и губ по медиане свечи (High + Low) / 2. Каждая линия сдвигается вперёд на своё количество баров, чтобы повторить отображение в MetaTrader.
  2. Подтверждение тренда – длинный сигнал появляется, когда выполнено условие jaw < teeth < lips и каждая линия выше, чем на предыдущей свече. Для короткого сигнала требуется jaw > teeth > lips, при этом все три линии должны снижаться относительно предыдущего бара.
  3. Управление входом – стратегия удерживает только одну позицию. Если включён параметр UseEntryToExit, при появлении сигнала противоположного направления сначала закрывается текущая позиция, а новая заявка отправляется на следующем сигнале.
  4. Защитные выходы – стоп-лосс и тейк-профит задаются в пунктах и пересчитываются через PriceStep инструмента. Открытые позиции проверяются на каждой завершённой свече и закрываются, как только цена достигает одного из уровней.
  5. Фильтр повторных сигналов – оригинальный советник не допускал повторных входов в пределах одной свечи. Порт сохраняет время последнего сигнального бара и пропускает дополнительные заявки в течение той же свечи.

Параметры

Параметр Значение по умолчанию Описание
OrderVolume 0.1 Объём сделки в лотах или контрактах, передаваемый в BuyMarket/SellMarket.
EnableTrading true Главный переключатель: разрешает или запрещает новые входы, сохраняя контроль рисков.
UseEntryToExit true Закрывает текущую позицию перед подготовкой противоположного сигнала (аналог флага «Entry to Exit»).
StopLossPips 20 Расстояние защитного стопа от цены входа. Ноль отключает стоп.
TakeProfitPips 50 Расстояние тейк-профита от цены входа. Ноль отключает цель.
MinimumBars 60 Минимальное число завершённых свечей перед первой проверкой сигналов.
JawPeriod / TeethPeriod / LipsPeriod 13 / 8 / 5 Длины сглаженных скользящих средних Аллигатора.
JawShift / TeethShift / LipsShift 8 / 5 / 3 Сдвиг вперёд (в барах) для каждой линии, совпадает с настройками MetaTrader.
CandleType таймфрейм 1 час Основная серия свечей. Измените при необходимости, чтобы совпасть с графиком из MetaTrader.

Примечания по эксплуатации

  • Обрабатываются только завершённые свечи (CandleStates.Finished), чтобы избегать частичных значений.
  • Уровни стопа и цели отслеживаются внутри стратегии; при срабатывании отправляется рыночная заявка на закрытие позиции.
  • Пересчёт пунктов учитывает стандарт форекс-инструментов: для 5- и 3-знаковых котировок один пункт равен десяти шагам цены.
  • Для визуальной проверки подключите стратегию к схеме вместе с коннектором, портфелем и инструментом. В графической панели будут отображаться свечи и линии Аллигатора, что упрощает сравнение с шаблоном MetaTrader.
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>
/// Trend-following system converted from the MetaTrader expert advisor "SniperJawEA.mq4".
/// The strategy aligns the Alligator jaw, teeth, and lips smoothed moving averages on the median price.
/// A long signal appears when all three lines stack upward and each line rises compared with the previous candle.
/// A short signal requires the inverse stacking and downward slope. Optional settings mirror the original EA: pip-based
/// stop-loss and take-profit distances plus an "entry-to-exit" switch that liquidates the opposite position before opening a new trade.
/// </summary>
public class SniperJawStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<bool> _enableTrading;
	private readonly StrategyParam<bool> _useEntryToExit;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _minimumBars;
	private readonly StrategyParam<int> _jawPeriod;
	private readonly StrategyParam<int> _jawShift;
	private readonly StrategyParam<int> _teethPeriod;
	private readonly StrategyParam<int> _teethShift;
	private readonly StrategyParam<int> _lipsPeriod;
	private readonly StrategyParam<int> _lipsShift;
	private readonly StrategyParam<DataType> _candleType;

	private SmoothedMovingAverage _jaw;
	private SmoothedMovingAverage _teeth;
	private SmoothedMovingAverage _lips;

	private decimal?[] _jawHistory;
	private decimal?[] _teethHistory;
	private decimal?[] _lipsHistory;

	private decimal _pipSize;
	private decimal? _longStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortStopPrice;
	private decimal? _shortTakePrice;
	private bool _longExitRequested;
	private bool _shortExitRequested;
	private int _finishedCandles;
	private DateTimeOffset? _lastSignalTime;

	/// <summary>
	/// Initializes <see cref="SniperJawStrategy"/> parameters.
	/// </summary>
	public SniperJawStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Trade size in lots or contracts", "Trading");

		_enableTrading = Param(nameof(EnableTrading), true)
			.SetDisplay("Enable Trading", "Master switch for signal execution", "Trading");

		_useEntryToExit = Param(nameof(UseEntryToExit), true)
			.SetDisplay("Use Entry To Exit", "Close opposite exposure before opening a new trade", "Trading");

		_stopLossPips = Param(nameof(StopLossPips), 20)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance converted with the price step", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 50)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Optional profit target distance; zero disables it", "Risk");

		_minimumBars = Param(nameof(MinimumBars), 1)
			.SetGreaterThanZero()
			.SetDisplay("Minimum Bars", "Required number of finished candles before trading", "Filters");

		_jawPeriod = Param(nameof(JawPeriod), 13)
			.SetGreaterThanZero()
			.SetDisplay("Jaw Period", "Smoothed moving average length for the jaw line", "Alligator");

		_jawShift = Param(nameof(JawShift), 0)
			.SetNotNegative()
			.SetDisplay("Jaw Shift", "Forward shift applied to jaw readings", "Alligator");

		_teethPeriod = Param(nameof(TeethPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("Teeth Period", "Smoothed moving average length for the teeth line", "Alligator");

		_teethShift = Param(nameof(TeethShift), 0)
			.SetNotNegative()
			.SetDisplay("Teeth Shift", "Forward shift applied to teeth readings", "Alligator");

		_lipsPeriod = Param(nameof(LipsPeriod), 5)
			.SetGreaterThanZero()
			.SetDisplay("Lips Period", "Smoothed moving average length for the lips line", "Alligator");

		_lipsShift = Param(nameof(LipsShift), 0)
			.SetNotNegative()
			.SetDisplay("Lips Shift", "Forward shift applied to lips readings", "Alligator");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary candle series used for signals", "Data");
	}

	/// <summary>
	/// Trade volume expressed in lots or contracts.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Master switch for enabling or disabling signal execution.
	/// </summary>
	public bool EnableTrading
	{
		get => _enableTrading.Value;
		set => _enableTrading.Value = value;
	}

	/// <summary>
	/// Close the opposite position before opening a new trade when a fresh signal arrives.
	/// </summary>
	public bool UseEntryToExit
	{
		get => _useEntryToExit.Value;
		set => _useEntryToExit.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips; zero disables the protective stop.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips; zero disables the target.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Minimum number of finished candles required before the system evaluates signals.
	/// </summary>
	public int MinimumBars
	{
		get => _minimumBars.Value;
		set => _minimumBars.Value = value;
	}

	/// <summary>
	/// Length of the jaw smoothed moving average.
	/// </summary>
	public int JawPeriod
	{
		get => _jawPeriod.Value;
		set => _jawPeriod.Value = value;
	}

	/// <summary>
	/// Forward shift applied to jaw readings when aligning them with candles.
	/// </summary>
	public int JawShift
	{
		get => _jawShift.Value;
		set => _jawShift.Value = value;
	}

	/// <summary>
	/// Length of the teeth smoothed moving average.
	/// </summary>
	public int TeethPeriod
	{
		get => _teethPeriod.Value;
		set => _teethPeriod.Value = value;
	}

	/// <summary>
	/// Forward shift applied to teeth readings when aligning them with candles.
	/// </summary>
	public int TeethShift
	{
		get => _teethShift.Value;
		set => _teethShift.Value = value;
	}

	/// <summary>
	/// Length of the lips smoothed moving average.
	/// </summary>
	public int LipsPeriod
	{
		get => _lipsPeriod.Value;
		set => _lipsPeriod.Value = value;
	}

	/// <summary>
	/// Forward shift applied to lips readings when aligning them with candles.
	/// </summary>
	public int LipsShift
	{
		get => _lipsShift.Value;
		set => _lipsShift.Value = value;
	}

	/// <summary>
	/// Candle type used for the primary signal series.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_jaw = null;
		_teeth = null;
		_lips = null;
		_jawHistory = null;
		_teethHistory = null;
		_lipsHistory = null;

		_pipSize = 0m;
		_longStopPrice = null;
		_longTakePrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_longExitRequested = false;
		_shortExitRequested = false;
		_finishedCandles = 0;
		_lastSignalTime = null;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_jaw = new SmoothedMovingAverage { Length = JawPeriod };
		_teeth = new SmoothedMovingAverage { Length = TeethPeriod };
		_lips = new SmoothedMovingAverage { Length = LipsPeriod };

		_jawHistory = CreateHistoryBuffer(JawShift);
		_teethHistory = CreateHistoryBuffer(TeethShift);
		_lipsHistory = CreateHistoryBuffer(LipsShift);

		_pipSize = CalculatePipSize();
		_finishedCandles = 0;
		_lastSignalTime = null;

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _jaw);
			DrawIndicator(area, _teeth);
			DrawIndicator(area, _lips);
			DrawOwnTrades(area);
		}
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Order?.Security != Security)
			return;

		var entryPrice = trade.Trade.Price;

		if (Position > 0)
		{
			_longStopPrice = StopLossPips > 0 ? entryPrice - StopLossPips * _pipSize : (decimal?)null;
			_longTakePrice = TakeProfitPips > 0 ? entryPrice + TakeProfitPips * _pipSize : (decimal?)null;
			_longExitRequested = false;
			_shortExitRequested = false;
			_shortStopPrice = null;
			_shortTakePrice = null;
		}
		else if (Position < 0)
		{
			_shortStopPrice = StopLossPips > 0 ? entryPrice + StopLossPips * _pipSize : (decimal?)null;
			_shortTakePrice = TakeProfitPips > 0 ? entryPrice - TakeProfitPips * _pipSize : (decimal?)null;
			_shortExitRequested = false;
			_longExitRequested = false;
			_longStopPrice = null;
			_longTakePrice = null;
		}
		else
		{
			_longStopPrice = null;
			_longTakePrice = null;
			_shortStopPrice = null;
			_shortTakePrice = null;
			_longExitRequested = false;
			_shortExitRequested = false;
		}
	}

	private void ProcessCandle(ICandleMessage candle)
	{
		if (candle.State != CandleStates.Finished)
			return;

		_finishedCandles++;

		if (Position > 0)
		{
			ManageLong(candle);
		}
		else if (Position < 0)
		{
			ManageShort(candle);
		}

		var median = (candle.HighPrice + candle.LowPrice) / 2m;

		var jawValue = _jaw.Process(new DecimalIndicatorValue(_jaw, median, candle.OpenTime) { IsFinal = true });
		var teethValue = _teeth.Process(new DecimalIndicatorValue(_teeth, median, candle.OpenTime) { IsFinal = true });
		var lipsValue = _lips.Process(new DecimalIndicatorValue(_lips, median, candle.OpenTime) { IsFinal = true });

		if (!_jaw.IsFormed || !_teeth.IsFormed || !_lips.IsFormed)
			return;

		var jawCurrent = jawValue.ToDecimal();
		var teethCurrent = teethValue.ToDecimal();
		var lipsCurrent = lipsValue.ToDecimal();

		if (_finishedCandles < MinimumBars)
			return;

		var isUptrend = jawCurrent < teethCurrent && teethCurrent < lipsCurrent;

		var isDowntrend = jawCurrent > teethCurrent && teethCurrent > lipsCurrent;

		if (!EnableTrading)
			return;

		// removed IsOnline guard

		if (isUptrend)
		{
			if (Position < 0 && UseEntryToExit)
			{
				RequestShortExit();
				return;
			}

			if (Position != 0)
				return;

			if (_lastSignalTime == candle.OpenTime)
				return;

			BuyMarket(volume: OrderVolume);
			_lastSignalTime = candle.OpenTime;
		}
		else if (isDowntrend)
		{
			if (Position > 0 && UseEntryToExit)
			{
				RequestLongExit();
				return;
			}

			if (Position != 0)
				return;

			if (_lastSignalTime == candle.OpenTime)
				return;

			SellMarket(volume: OrderVolume);
			_lastSignalTime = candle.OpenTime;
		}
	}

	private void ManageLong(ICandleMessage candle)
	{
		if (_longTakePrice is decimal take && candle.HighPrice >= take)
		{
			RequestLongExit();
			return;
		}

		if (_longStopPrice is decimal stop && candle.LowPrice <= stop)
		{
			RequestLongExit();
		}
	}

	private void ManageShort(ICandleMessage candle)
	{
		if (_shortTakePrice is decimal take && candle.LowPrice <= take)
		{
			RequestShortExit();
			return;
		}

		if (_shortStopPrice is decimal stop && candle.HighPrice >= stop)
		{
			RequestShortExit();
		}
	}

	private void RequestLongExit()
	{
		if (_longExitRequested || Position <= 0)
			return;

		_longExitRequested = true;
		SellMarket(volume: Position);
	}

	private void RequestShortExit()
	{
		if (_shortExitRequested || Position >= 0)
			return;

		_shortExitRequested = true;
		BuyMarket(volume: Math.Abs(Position));
	}

	private static decimal?[] CreateHistoryBuffer(int shift)
	{
		var size = Math.Max(shift + 3, 3);
		return new decimal?[size];
	}

	private static void UpdateHistory(decimal?[] buffer, decimal value)
	{
		if (buffer.Length == 0)
			return;

		Array.Copy(buffer, 1, buffer, 0, buffer.Length - 1);
		buffer[^1] = value;
	}

	private static bool TryGetShiftedValue(decimal?[] buffer, int offsetFromEnd, out decimal value)
	{
		value = 0m;

		if (buffer.Length < offsetFromEnd)
			return false;

		var index = buffer.Length - offsetFromEnd;
		if (index < 0)
			return false;

		if (buffer[index] is not decimal stored)
			return false;

		value = stored;
		return true;
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			return 1m;

		var decimals = Security?.Decimals ?? 0;
		if (decimals == 3 || decimals == 5)
			return step * 10m;

		return step;
	}
}