GitHub で見る

スナイパージョー戦略

スナイパー ジョー戦略は、MetaTrader 4 エキスパート アドバイザー SniperJawEA.mq4 を StockSharp のハイレベル戦略 API に移植します。システムは、ローソク足の中央値でビル Williams の Alligator インジケーターを分析します。取引は、3 つの平滑化移動平均 (顎、歯、唇) が厳密に強気または弱気の順序で積み上げられ、そのすべてが以前に完成したローソク足と比較して同じ方向に進んだ場合にのみ開始されます。

取引ロジック

  1. Alligator 再構築 – 3 つの SmoothedMovingAverage インスタンスは、ローソク足中央値 (High + Low) / 2 上の顎、歯、唇を計算します。各線は、MetaTrader のプロットを反映するために、独自のバーの数だけ前方にシフトできます。
  2. トレンドの確認 – シフトされた値が jaw < teeth < lips を満たし、**各線が前のローソク足よりも高い場合、ロング バイアスが生成されます。ショート バイアスでは、3 つの線すべてが前のバーと比較して低くなっている jaw > teeth > lips が必要です。
  3. エントリー管理 – この戦略は一度に 1 つのポジションのみをオープンします。 UseEntryToExit が有効になっていて、新しい反対のシグナルが発火すると、現在のエクスポージャーが最初に平坦化され、次のシグナルで新しい注文が送信されます。
  4. 保護的出口 – ストップロスとテイクプロフィットの距離はピップ単位で定義され、セキュリティ PriceStep を使用して変換されます。ロングポジションとショートポジションの両方が、完成したローソク足ごとに監視され、いずれかのしきい値に達するとクローズされます。
  5. 信号スロットリング – 元の EA はバーのタイムスタンプをチェックすることで重複エントリを防止していました。ポートは最後のシグナルローソクタイムを保存し、同じ足の間に追加の注文をスキップします。

パラメーター

パラメータ デフォルト 説明
OrderVolume 0.1 BuyMarket/SellMarket に渡されるロットまたは契約の取引サイズ。
EnableTrading true リスク管理をアクティブに保ちながら、新しいエントリを無効にすることができるマスター スイッチ。
UseEntryToExit true 反対のシグナルを準備する前に、既存のポジションをクローズします。 EA の「Entry to Exit」フラグをミラーリングします。
StopLossPips 20 エントリー価格からの保護ストップまでの距離。ゼロは停止を無効にします。
TakeProfitPips 50 エントリー価格から利益目標までの距離。ゼロはターゲットを無効にします。
MinimumBars 60 最初のシグナルが評価される前に必要な完成したキャンドルの数。
JawPeriod / TeethPeriod / LipsPeriod 13 / 8 / 5 Alligator ラインを形成する平滑化移動平均の長さ。
JawShift / TeethShift / LipsShift 8 / 5 / 3 Alligator バッファを MetaTrader バージョンに合わせるために使用される前方シフト (バー単位)。
CandleType 1 hour time frame プライマリーキャンドルシリーズの定期購読。 MetaTrader で使用されているグラフと一致するように調整します。

使用上の注意

  • この実装では、部分的に形成された値を避けるために、完成したローソク足 (CandleStates.Finished) のみを評価します。
  • ストップレベルとターゲットレベルは内部的に追跡されます。この戦略は、レベルが違反されたときにポジションをフラットにするために成行注文を発行します。
  • 価格ステップ変換は一般的な外国為替の規則に従います。つまり、5 桁と 3 桁の小数記号は 1 ピップを 10 の価格ステップとして扱います。
  • コネクタ、ポートフォリオ、セキュリティ構成とともに戦略をスキームに追加します。戦略を開始すると、チャート パネルにローソク足シリーズと再構築された Alligator ラインが表示され、視覚的に簡単に検証できます。
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;
	}
}