GitHub で見る

Executor AO 戦略

概要

Executor AOは、もともと「Executor AO」MetaTraderエキスパートアドバイザーとして配布されていた、ソーサースタイルのAwesome Oscillator戦略です。 StockSharpポートはインジケーターベースの反転検出を維持しながら、資金管理を固定注文サイズに簡略化しています。 この戦略は設定されたタイムフレームの完成したローソク足を監視し、Awesome Oscillatorの傾き変化を評価し、 ゼロラインの下または上で強気または弱気の条件が発生したときに単一のネットポジションを開きます。 オプションの保護ストップ、テイクプロフィット、トレーリングロジックは元のEAのリスク管理動作を再現します。

取引ロジック

  1. CandleTypeで定義されたローソク足シリーズにサブスクライブし、設定された AoShortPeriodAoLongPeriod パラメーターで 完成したすべてのローソク足をAwesome Oscillatorに入力する。
  2. 元のエキスパートが使用したMetaTraderのバッファアクセスパターンを再現するために、最後の3つの完成したAwesome Oscillator値を保存する。
  3. ポジションが開いていない場合:
    • 強気セットアップ: 最新のAO値が前の値より大きく、前の値が2本前の値より小さく(谷)、最新の値が -MinimumAoIndent を下回っている。 その場合、TradeVolume ロットで市場買い注文を送信する。
    • 弱気セットアップ: 最新のAO値が前の値より小さく、前の値が2本前の値より大きく(ピーク)、最新の値が MinimumAoIndent を上回っている。 その場合、固定ボリュームで市場売り注文を送信する。
  4. ポジションが存在する場合、戦略はEAの出口をエミュレートする:
    • StopLossPipsTakeProfitPips を調整されたピップサイズで乗算してエントリーからストップロスとテイクプロフィット価格を計算する (MetaTraderの3/5桁処理が複製される)。
    • 価格がポジションに有利に TrailingStopPips + TrailingStepPips ピップ以上動いたときにトレーリングストップルールを適用する。 新しいレベルが前のレベルを超えている場合にのみストップを進める(EAのトレーリングステップ要件に対応)。
    • テイクプロフィットまたはストップロスに価格が触れたとき、または前のバーのAwesome Oscillator値がプラスに転じたときにロングポジションをクローズする。 価格が目標に達したとき、または前のAO値がゼロを下回ったときにショートポジションをクローズする。
  5. すべての注文は成行注文; StockSharpのネットポジションモデルにより、一度に1つの方向のみアクティブになることが保証される。

パラメーター

名前 デフォルト 説明
CandleType DataType 5分ローソク足 戦略の計算と取引に使用される主要な時間軸。
TradeVolume decimal 1 各エントリーに使用される固定注文サイズ。
AoShortPeriod int 5 Awesome Oscillatorの短いSMAの速い期間。
AoLongPeriod int 34 Awesome Oscillatorの長いSMAの遅い期間。
MinimumAoIndent decimal 0.001 新しいシグナルに必要なゼロからの最小絶対距離。AOがゼロ付近を推移しているときの取引を防ぐ。
StopLossPips decimal 50 MetaTraderスタイルのピップで表現された保護ストップロス距離。ストップを無効にするには 0 に設定する。
TakeProfitPips decimal 50 ピップで表現されたテイクプロフィット距離。ターゲットを無効にするには 0 に設定する。
TrailingStopPips decimal 5 トレーリングストップ活性化距離。ゼロより大きい場合のみ使用される。
TrailingStepPips decimal 5 トレーリングストップが更新される前に必要な最小価格改善。トレーリングが有効な場合は正の値を維持する必要がある。

MetaTrader EAとの違い

  • MetaTraderバージョンはリスクベースのポジションサイジングを許可していた。StockSharpポートは固定ロットオプション(TradeVolume)を実装し、 明確さのためにパーセントリスク管理を省いている。
  • 注文管理は戦略内でシミュレートされる: ストップロスまたはテイクプロフィットの閾値が完成したローソク足で達成されると、戦略はポジションを フラットにするために成行注文を送信する。これは別の子注文を作成せずにEAの動作を反映する。
  • トレーリング調整はすべてのティックではなくローソク足のクローズイベントで発生する。これにより、同じ閾値ロジックに従いながら 高レベルAPIとの実装が一貫する。
  • すべてのコードパスは、インジケーターバッファを手動でコピーする代わりに、StockSharpの高レベル SubscribeCandles + Bind パターンに依存する。

使用のヒント

  • 戦略を開始する前に TradeVolume を銘柄のロットステップに合わせる。コンストラクターは同じ値を Strategy.Volume にも割り当てるため、 ヘルパーメソッドは自動的に選択されたサイズを使用する。
  • MinimumAoIndent はノイズの多い市場でゼロ付近での頻繁な変動を避けるために増加させることができる。0 に設定するとEAの最も積極的な 動作が再現される。
  • トレーリングストップを有効にするとき、TrailingStepPips をゼロ以上に保つ; そうしないとコンストラクターが例外をスローし、元のEAの パラメーター検証を再現する。
  • 変換後の谷/ピーク検出を検証するために、ローソク足とAwesome Oscillatorオーバーレイの両方を視覚化するために戦略をチャートに添付する。

インジケーター

  • Awesome Oscillator: 中央値の速い単純移動平均と遅い単純移動平均の差。デフォルトの5/34設定はMetaTraderインジケーターと一致する。
using System;

using StockSharp.Algo;
using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Awesome Oscillator swing strategy converted from the "Executor AO" MetaTrader expert.
/// Implements the saucer-based entry logic with optional stop, take-profit, and trailing exit management.
/// </summary>
public class ExecutorAoStrategy : Strategy
{
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _aoShortPeriod;
	private readonly StrategyParam<int> _aoLongPeriod;
	private readonly StrategyParam<decimal> _minimumAoIndent;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<decimal> _trailingStopPips;
	private readonly StrategyParam<decimal> _trailingStepPips;
	private readonly StrategyParam<DataType> _candleType;
	private static readonly object _sync = new();

	private AwesomeOscillator _ao = null!;

	private decimal? _currentAo;
	private decimal? _previousAo;
	private decimal? _previousAo2;

	private decimal _pipSize;

	private decimal? _longEntryPrice;
	private decimal? _longStop;
	private decimal? _longTake;

	private decimal? _shortEntryPrice;
	private decimal? _shortStop;
	private decimal? _shortTake;

	/// <summary>
	/// Initializes a new instance of the <see cref="ExecutorAoStrategy"/> class.
	/// </summary>
	public ExecutorAoStrategy()
	{
		_tradeVolume = Param(nameof(TradeVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Trade Volume", "Fixed order size", "Risk")
			;

		_aoShortPeriod = Param(nameof(AoShortPeriod), 5)
			.SetDisplay("AO Short Period", "Fast period for Awesome Oscillator", "Indicators")
			;

		_aoLongPeriod = Param(nameof(AoLongPeriod), 34)
			.SetDisplay("AO Long Period", "Slow period for Awesome Oscillator", "Indicators")
			;

		_minimumAoIndent = Param(nameof(MinimumAoIndent), 0.001m)
			.SetNotNegative()
			.SetDisplay("Minimum AO Indent", "Minimum distance from zero before signals are valid", "Logic")
			;

		_stopLossPips = Param(nameof(StopLossPips), 50m)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Protective stop distance expressed in pips", "Risk")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 50m)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Target distance expressed in pips", "Risk")
			;

		_trailingStopPips = Param(nameof(TrailingStopPips), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Stop (pips)", "Trailing distance in pips", "Risk")
			;

		_trailingStepPips = Param(nameof(TrailingStepPips), 5m)
			.SetNotNegative()
			.SetDisplay("Trailing Step (pips)", "Minimum move before trailing adjusts", "Risk")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe used for analysis", "General");
	}

	/// <summary>
	/// Fixed order volume used for market entries.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set
		{
			_tradeVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Fast period for the Awesome Oscillator calculation.
	/// </summary>
	public int AoShortPeriod
	{
		get => _aoShortPeriod.Value;
		set => _aoShortPeriod.Value = value;
	}

	/// <summary>
	/// Slow period for the Awesome Oscillator calculation.
	/// </summary>
	public int AoLongPeriod
	{
		get => _aoLongPeriod.Value;
		set => _aoLongPeriod.Value = value;
	}

	/// <summary>
	/// Minimum absolute AO value required before trades are allowed.
	/// </summary>
	public decimal MinimumAoIndent
	{
		get => _minimumAoIndent.Value;
		set => _minimumAoIndent.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in pips.
	/// </summary>
	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance in pips.
	/// </summary>
	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Trailing stop distance in pips.
	/// </summary>
	public decimal TrailingStopPips
	{
		get => _trailingStopPips.Value;
		set => _trailingStopPips.Value = value;
	}

	/// <summary>
	/// Minimum step required before the trailing stop moves.
	/// </summary>
	public decimal TrailingStepPips
	{
		get => _trailingStepPips.Value;
		set => _trailingStepPips.Value = value;
	}

	/// <summary>
	/// Candle series used to generate signals.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

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

		_ao = null!;
		_currentAo = null;
		_previousAo = null;
		_previousAo2 = null;
		_pipSize = 0m;
		ResetLongState();
		ResetShortState();
	}

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

		Volume = TradeVolume;
		_pipSize = CalculatePipSize();

		if (TrailingStopPips > 0m && TrailingStepPips <= 0m)
			throw new InvalidOperationException("Trailing step must be positive when trailing stop is enabled.");

		_ao = new AwesomeOscillator
		{
			ShortMa = { Length = AoShortPeriod },
			LongMa = { Length = AoLongPeriod }
		};

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

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

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

		lock (_sync)
		{
			var aoValue = _ao.Process(new CandleIndicatorValue(_ao, candle) { IsFinal = true });
			if (!aoValue.IsFinal || _ao == null || !_ao.IsFormed)
				return;

			var previousAo = _currentAo;
			var previousAo2 = _previousAo;

			var positionClosed = HandleActivePositions(candle, previousAo);

			StoreAoValue(aoValue.ToDecimal());

			if (positionClosed || !previousAo.HasValue || !previousAo2.HasValue || !_currentAo.HasValue)
				return;

			if (Position != 0m)
				return;

			var current = _currentAo.Value;
			var prev = previousAo.Value;
			var prev2 = previousAo2.Value;
			var indent = MinimumAoIndent;

			if (current > prev && prev < prev2 && current <= -indent)
			{
				OpenLong(candle.ClosePrice);
				return;
			}

			if (current < prev && prev > prev2 && current >= indent)
				OpenShort(candle.ClosePrice);
		}
	}

	private bool HandleActivePositions(ICandleMessage candle, decimal? previousAo)
	{
		if (Position > 0m)
		{
			_longEntryPrice ??= candle.ClosePrice;
			UpdateTrailingForLong(candle);

			if (_longTake.HasValue && candle.HighPrice >= _longTake.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetLongState();
				return true;
			}

			if (_longStop.HasValue && candle.LowPrice <= _longStop.Value)
			{
				SellMarket(Math.Abs(Position));
				ResetLongState();
				return true;
			}

			if (previousAo.HasValue && previousAo.Value > 0m)
			{
				SellMarket(Math.Abs(Position));
				ResetLongState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			_shortEntryPrice ??= candle.ClosePrice;
			UpdateTrailingForShort(candle);

			if (_shortTake.HasValue && candle.LowPrice <= _shortTake.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetShortState();
				return true;
			}

			if (_shortStop.HasValue && candle.HighPrice >= _shortStop.Value)
			{
				BuyMarket(Math.Abs(Position));
				ResetShortState();
				return true;
			}

			if (previousAo.HasValue && previousAo.Value < 0m)
			{
				BuyMarket(Math.Abs(Position));
				ResetShortState();
				return true;
			}
		}
		else
		{
			ResetLongState();
			ResetShortState();
		}

		return false;
	}

	private void OpenLong(decimal price)
	{
		var volume = GetTradeVolume();
		if (volume <= 0m)
			return;

		BuyMarket(volume);

		_longEntryPrice = price;
		_longStop = StopLossPips > 0m ? price - StopLossPips * _pipSize : null;
		_longTake = TakeProfitPips > 0m ? price + TakeProfitPips * _pipSize : null;
		ResetShortState();
	}

	private void OpenShort(decimal price)
	{
		var volume = GetTradeVolume();
		if (volume <= 0m)
			return;

		SellMarket(volume);

		_shortEntryPrice = price;
		_shortStop = StopLossPips > 0m ? price + StopLossPips * _pipSize : null;
		_shortTake = TakeProfitPips > 0m ? price - TakeProfitPips * _pipSize : null;
		ResetLongState();
	}

	private void UpdateTrailingForLong(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || !_longEntryPrice.HasValue)
			return;

		var trailingDistance = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;
		var price = candle.ClosePrice;
		var entry = _longEntryPrice.Value;

		if (price - entry > trailingDistance + trailingStep)
		{
			var minimalAllowed = price - (trailingDistance + trailingStep);
			if (!_longStop.HasValue || _longStop.Value < minimalAllowed)
				_longStop = price - trailingDistance;
		}
	}

	private void UpdateTrailingForShort(ICandleMessage candle)
	{
		if (TrailingStopPips <= 0m || TrailingStepPips <= 0m || !_shortEntryPrice.HasValue)
			return;

		var trailingDistance = TrailingStopPips * _pipSize;
		var trailingStep = TrailingStepPips * _pipSize;
		var price = candle.ClosePrice;
		var entry = _shortEntryPrice.Value;

		if (entry - price > trailingDistance + trailingStep)
		{
			var maximalAllowed = price + (trailingDistance + trailingStep);
			if (!_shortStop.HasValue || _shortStop.Value > maximalAllowed)
				_shortStop = price + trailingDistance;
		}
	}

	private decimal GetTradeVolume()
	{
		var volume = Volume;
		if (volume <= 0m)
			volume = TradeVolume;
		return volume;
	}

	private void StoreAoValue(decimal value)
	{
		_previousAo2 = _previousAo;
		_previousAo = _currentAo;
		_currentAo = value;
	}

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

		var decimals = GetDecimalPlaces(priceStep);
		var factor = decimals == 3 || decimals == 5 ? 10m : 1m;
		return priceStep * factor;
	}

	private static int GetDecimalPlaces(decimal value)
	{
		value = Math.Abs(value);
		if (value == 0m)
			return 0;

		var bits = decimal.GetBits(value);
		return (bits[3] >> 16) & 0xFF;
	}

	private void ResetLongState()
	{
		_longEntryPrice = null;
		_longStop = null;
		_longTake = null;
	}

	private void ResetShortState()
	{
		_shortEntryPrice = null;
		_shortStop = null;
		_shortTake = null;
	}
}