GitHub で見る

Parabolic SAR 最初のドット戦略

概要

Parabolic SAR ファースト ドット戦略 は、フォルダ MQL/9954 からの MetaTrader エキスパート アドバイザー pSAR_bug_4 の StockSharp の高レベルの変換です。システムは、価格の反対側に表示される Parabolic SAR の最初のドットに反応します。 SAR が終値を下回ると、ロング取引が開始されます。 SAR が終値を超えて上昇すると、ショートトレードが実行されます。オリジナルの MQL バージョンと同様に、すべてのポジションは、Parabolic SAR 「ポイント」で表される固定のストップロスとテイクプロフィットの距離で保護されます。

取引ロジック

  1. データとインジケーターの準備。このストラテジーは、構成可能なローソク足タイプ (デフォルトでは 15 分足ローソク足) をサブスクライブし、ユーザー定義の加速ステップと最大加速度を使用して Parabolic SAR インジケーターをバインドします。
  2. 状態追跡。最初に完成したローソク足で、戦略は SAR が終値の上にあるか下にあるかを記憶します。その後のローソク足は、新しい SAR の位置を前の状態と比較します。
  3. エントリールール
    • ロングエントリー: SAR が終値の上から終値の下に切り替わります。既存のショート ポジションはすべてクローズされ、設定されたボリュームで新しいロング ポジションが市場でオープンされます。
    • 簡単なエントリー: SAR は終値の下から終値の上に切り替わります。既存のロング ポジションは、新しいショート ポジションをオープンする前にクローズされます。
  4. 保護命令。エントリー直後、ストラテジーには、StopLossPoints または TakeProfitPoints に証券 PriceStep を乗算してローソク足の終値から計算されたストップロスとテイクプロフィットのレベルが保存されます。 UseStopMultiplier が有効な場合 (MetaTrader からコピーされたデフォルト動作)、ブローカーが小数ピップでクオートすることを考慮して、距離は 10 倍されます。
  5. 終了ルール。戦略は、完成したローソク足ごとに、保存されているストップロスとテイクプロフィットのレベルに対して高値と安値をチェックします。高値または安値がそのレベルを突破した場合、ポジションは市場でクローズされます。反対の SAR シグナルが到着すると、現在のエクスポージャーをフラットにして新しい取引を開始するサイズの注文を送信することにより、ポジションも逆転します。

リスク管理

  • ストップロスとテイクプロフィットの距離は、新しいポジションごとに再計算されます。
  • コードは保守的なフォールバックを実行します。証券が価格ステップを提供しない場合、ゼロ距離を避けるために値 0.0001 が使用されます。
  • すべての取引決定では、IsFormedAndOnlineAndAllowTrading() ヘルパーを使用して、サブスクリプションがアクティブでライブであることを確認します。

パラメーター

名前 デフォルト 説明
TradeVolume 0.1 新しいポジションに使用される注文量。このパラメータは、ベースの Strategy.Volume プロパティも更新します。
StopLossPoints 90 ストップロス距離は Parabolic SAR ポイントで表されます。この値にはセキュリティ PriceStep が乗算されます (UseStopMultiplier が true の場合はオプションで 10 が乗算されます)。
TakeProfitPoints 20 価格ステップを通じて換算された Parabolic SAR ポイント単位の利食い距離。
UseStopMultiplier true 有効にすると、ストップロスとテイクプロフィットの距離を 10 倍して、MetaTrader エキスパートの StopMult スイッチを模倣します。
SarAccelerationStep 0.02 Parabolic SAR インジケーターに供給される初期加速係数。
SarAccelerationMax 0.2 Parabolic SAR インジケーターの最大加速係数。
CandleType 15m time-frame インジケーターとシグナルの計算に使用されるローソクのタイプ。

変換時の注意点

  • MetaTrader のストップロス注文とテイクプロフィット注文はブローカー側の保護注文でした。 StockSharp は、ローソク足の高値と安値を監視し、しきい値を超えたときに市場退場を送信することでそれらを再現します。
  • MetaTrader の専門家は、StopMult が true の場合は常にストップ距離を 10 倍にして、端数ピップでクオートするブローカーとの互換性を向上させました。 UseStopMultiplier パラメータは同じ動作を実装します。
  • 変換では、プロジェクト ガイドラインの要求に応じて、StockSharp の高レベルの API (SubscribeCandlesBindBuyMarketSellMarket) を使用します。タスクのリクエストに一致する追加の Python バージョンはまだ提供されていません。
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>
/// Parabolic SAR first dot reversal strategy.
/// Opens a position when Parabolic SAR flips relative to the close and protects it with classic stops.
/// </summary>
public class ParabolicSarFirstDotStrategy : Strategy
{
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<bool> _useStopMultiplier;
	private readonly StrategyParam<decimal> _sarAccelerationStep;
	private readonly StrategyParam<decimal> _sarAccelerationMax;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _longStop;
	private decimal? _longTake;
	private decimal? _shortStop;
	private decimal? _shortTake;
	private bool? _prevIsSarAbovePrice;
	private decimal _priceStep;
	private DateTimeOffset _lastTradeTime;

	/// <summary>
	/// Trading volume in lots.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set
		{
			_tradeVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Stop-loss distance expressed in Parabolic SAR points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in Parabolic SAR points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Multiply stop distances by ten to mirror the MetaTrader implementation.
	/// </summary>
	public bool UseStopMultiplier
	{
		get => _useStopMultiplier.Value;
		set => _useStopMultiplier.Value = value;
	}

	/// <summary>
	/// Initial acceleration factor for Parabolic SAR.
	/// </summary>
	public decimal SarAccelerationStep
	{
		get => _sarAccelerationStep.Value;
		set => _sarAccelerationStep.Value = value;
	}

	/// <summary>
	/// Maximum acceleration factor for Parabolic SAR.
	/// </summary>
	public decimal SarAccelerationMax
	{
		get => _sarAccelerationMax.Value;
		set => _sarAccelerationMax.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="ParabolicSarFirstDotStrategy"/>.
	/// </summary>
	public ParabolicSarFirstDotStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetDisplay("Volume", "Order volume in lots", "General")
			.SetGreaterThanZero();

		_stopLossPoints = Param(nameof(StopLossPoints), 90)
			.SetDisplay("Stop-Loss Points", "Stop-loss distance converted through the instrument price step", "Risk Management")
			.SetGreaterThanZero()
			
			.SetOptimize(30, 150, 10);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 20)
			.SetDisplay("Take-Profit Points", "Take-profit distance converted through the instrument price step", "Risk Management")
			.SetGreaterThanZero()
			
			.SetOptimize(10, 80, 10);

		_useStopMultiplier = Param(nameof(UseStopMultiplier), true)
			.SetDisplay("Use Stop Multiplier", "Multiply distances by ten to reproduce MetaTrader stop handling", "Risk Management");

		_sarAccelerationStep = Param(nameof(SarAccelerationStep), 0.02m)
			.SetDisplay("SAR Step", "Initial acceleration factor for Parabolic SAR", "Indicator")
			.SetRange(0.01m, 0.05m)
			
			.SetOptimize(0.01m, 0.05m, 0.01m);

		_sarAccelerationMax = Param(nameof(SarAccelerationMax), 0.2m)
			.SetDisplay("SAR Max", "Maximum acceleration factor for Parabolic SAR", "Indicator")
			.SetRange(0.1m, 0.4m)
			
			.SetOptimize(0.1m, 0.4m, 0.05m);

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Candle type used for calculations", "General");

		Volume = _tradeVolume.Value;
	}

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

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

		_prevIsSarAbovePrice = null;
		_longStop = null;
		_longTake = null;
		_shortStop = null;
		_shortTake = null;
		Volume = _tradeVolume.Value;
		_priceStep = 0;
		_lastTradeTime = default;
	}

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

		_priceStep = GetPriceStep();

		var parabolicSar = new ParabolicSar
		{
			Acceleration = SarAccelerationStep,
			AccelerationMax = SarAccelerationMax
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(parabolicSar, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal sarValue)
	{
		// Work only with completed candles to match MetaTrader logic.
		if (candle.State != CandleStates.Finished)
			return;

		// Wait for Parabolic SAR indicator to be ready.

		// Check whether existing positions should be closed by protective levels.
		CheckProtectiveLevels(candle);

		var isSarAbovePrice = sarValue > candle.ClosePrice;

		// Initialize state on the first value.
		if (_prevIsSarAbovePrice == null)
		{
			_prevIsSarAbovePrice = isSarAbovePrice;
			return;
		}

		var sarSwitchedBelow = _prevIsSarAbovePrice.Value && !isSarAbovePrice;
		var sarSwitchedAbove = !_prevIsSarAbovePrice.Value && isSarAbovePrice;

		if (sarSwitchedBelow)
			TryEnterLong(candle, sarValue);
		else if (sarSwitchedAbove)
			TryEnterShort(candle, sarValue);

		_prevIsSarAbovePrice = isSarAbovePrice;
	}

	private void TryEnterLong(ICandleMessage candle, decimal sarValue)
	{
		// Prevent duplicate long entries.
		if (Position > 0m)
			return;

		// Cooldown: at least 2 days between trades to avoid over-trading.
		if (_lastTradeTime != default && (candle.OpenTime - _lastTradeTime).TotalHours < 48)
			return;

		var volume = Volume + Math.Abs(Position);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
		_lastTradeTime = candle.OpenTime;

		var entryPrice = candle.ClosePrice;
		var stopDistance = GetDistance(StopLossPoints);
		var takeDistance = GetDistance(TakeProfitPoints);

		_longStop = entryPrice - stopDistance;
		_longTake = entryPrice + takeDistance;
		_shortStop = null;
		_shortTake = null;

		LogInfo($"Long entry after SAR flip. Close={entryPrice}, SAR={sarValue}, Stop={_longStop}, Take={_longTake}");
	}

	private void TryEnterShort(ICandleMessage candle, decimal sarValue)
	{
		// Prevent duplicate short entries.
		if (Position < 0m)
			return;

		// Cooldown: at least 2 days between trades to avoid over-trading.
		if (_lastTradeTime != default && (candle.OpenTime - _lastTradeTime).TotalHours < 48)
			return;

		var volume = Volume + Math.Abs(Position);
		if (volume <= 0m)
			return;

		SellMarket(volume);
		_lastTradeTime = candle.OpenTime;

		var entryPrice = candle.ClosePrice;
		var stopDistance = GetDistance(StopLossPoints);
		var takeDistance = GetDistance(TakeProfitPoints);

		_shortStop = entryPrice + stopDistance;
		_shortTake = entryPrice - takeDistance;
		_longStop = null;
		_longTake = null;

		LogInfo($"Short entry after SAR flip. Close={entryPrice}, SAR={sarValue}, Stop={_shortStop}, Take={_shortTake}");
	}

	private void CheckProtectiveLevels(ICandleMessage candle)
	{
		var position = Position;

		if (position > 0m)
		{
			if (_longStop is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Math.Abs(position));
				LogInfo($"Long stop-loss triggered at {stop}.");
				ResetLongTargets();
			}
			else if (_longTake is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Math.Abs(position));
				LogInfo($"Long take-profit triggered at {take}.");
				ResetLongTargets();
			}
		}
		else if (position < 0m)
		{
			if (_shortStop is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(Math.Abs(position));
				LogInfo($"Short stop-loss triggered at {stop}.");
				ResetShortTargets();
			}
			else if (_shortTake is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(Math.Abs(position));
				LogInfo($"Short take-profit triggered at {take}.");
				ResetShortTargets();
			}
		}
	}

	private void ResetLongTargets()
	{
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortTargets()
	{
		_shortStop = null;
		_shortTake = null;
	}

	private decimal GetDistance(int basePoints)
	{
		var multiplier = UseStopMultiplier ? 10 : 1;
		return basePoints * multiplier * _priceStep;
	}

	private decimal GetPriceStep()
	{
		// Use security price step when available, otherwise fall back to a minimal tick.
		var step = Security?.PriceStep ?? 0.0001m;
		return step > 0m ? step : 0.0001m;
	}
}