GitHub で見る

Gazonkos 押し目戦略

概要

Gazonkos 押し目戦略は、MetaTrader 5の元のエキスパートアドバイザー gazonkos の変換版です。このアプローチはEUR/USDの時間足チャートを取引し、2つの過去の終値の間の強いモメンタムを探します。そのモメンタムを検出した後、事前定義されたサイズの押し目を待ってから、最初の動きの方向にエントリーします。StockSharpの実装は、ローソク足サブスクリプションと保護注文を備えた高レベルAPIを使用しながら、ソースコードと同じステージ状態マシンを維持します。

取引ロジック

  1. 適格性チェック – 1時間に1つのポジションのみ許可されます。同じ時間内に別のトレードが開かれた場合、または設定された同時トレード数がすでに実行中の場合、戦略は待機します。
  2. モメンタム検出 – 2つの過去のローソク足(SecondShift マイナス FirstShift)の終値を比較します。差が Delta を超えると、戦略は意図した方向を記録します(新しい終値が高い場合はロング、そうでない場合はショート)。
  3. 押し目追跡 – モメンタムが現れた瞬間から、コードはその時間内に達した最高値(ロングセットアップ)または最安値(ショートセットアップ)を監視します。価格が少なくとも Rollback 分押し目になると、セットアップは実行可能になります。押し目が起きる前に時間が変わった場合、シグナルは破棄されます。
  4. 注文実行 – 押し目条件が満たされると、戦略は固定のテイクプロフィットとストップロス距離で成行注文を出します。ポジションサイジングは TradeVolume パラメーターで制御され、組み込みの StartProtection ヘルパーが保護注文を管理します。

このシーケンスは、ワークフローを調整するために STATE 変数と Trade 変数を使用したMT5バージョンを密接に反映しています。

リスク管理

  • StartProtection は絶対価格単位での固定テイクプロフィットとストップロス距離を設定します。これはエキスパートが各注文にTP/SLを付加していた方法と同様です。
  • ActiveTrades は、絶対ポジション値を設定ボリュームと許可されたトレード数の積と比較することで、最大総エクスポージャーを制限します。
  • 時間ゲーティングと押し目確認の組み合わせにより、横ばい状態での過剰取引を削減します。

パラメーター

名前 デフォルト値 説明
TakeProfit 0.0016 テイクプロフィットの絶対距離(価格単位)。5桁EUR/USD相場で16ポイントに相当します。
Rollback 0.0016 モメンタムシグナル後に達した極値からの必要な押し目。
StopLoss 0.0040 保護ストップロスの絶対距離。EUR/USDで40ポイントに相当します。
Delta 0.0040 強い動きを定義する2つの過去の終値の最小差。
TradeVolume 0.1 BuyMarket()SellMarket() に渡されるデフォルト注文ボリューム。
FirstShift 3 終値比較に使用するより古いバーインデックス(ローソク足の遡り数)。
SecondShift 2 終値比較に使用するより新しいバーインデックス。
ActiveTrades 1 同時トレードの最大数。制限を無効にするにはゼロに設定してください。
CandleType 1時間 時間軸 分析に使用するローソク足シリーズ。ソースEAと同様にデフォルトは時間足。

備考

  • 戦略は合理的なティックサイズを持つ任意の銘柄で機能します。銘柄のポイント値に合わせて DeltaRollbackTakeProfitStopLoss を調整してください。
  • すべてのインラインコメントはプロジェクトガイドラインの要求に従って英語で書かれています。
  • この戦略の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>
/// Momentum breakout with rollback confirmation inspired by the gazonkos MT5 expert.
/// The strategy waits for a spread between two historical closes, then joins the trend after a pullback.
/// </summary>
public class GazonkosStrategy : Strategy
{
	private readonly StrategyParam<decimal> _takeProfit;
	private readonly StrategyParam<decimal> _rollback;
	private readonly StrategyParam<decimal> _stopLoss;
	private readonly StrategyParam<decimal> _delta;
	private readonly StrategyParam<decimal> _tradeVolume;
	private readonly StrategyParam<int> _firstShift;
	private readonly StrategyParam<int> _secondShift;
	private readonly StrategyParam<int> _activeTrades;
	private readonly StrategyParam<DataType> _candleType;

	private readonly List<decimal> _closeHistory = new();

	private int _state;
	private int _tradeDirection;
	private decimal _maxPrice;
	private decimal _minPrice;
	private bool _canTrade;
	private int _lastTradeHour;
	private int _lastSignalHour;
	private int _maxHistory;

	/// <summary>
	/// Take profit distance expressed in absolute price units.
	/// </summary>
	public decimal TakeProfit
	{
		get => _takeProfit.Value;
		set => _takeProfit.Value = value;
	}

	/// <summary>
	/// Rollback distance that confirms the entry.
	/// </summary>
	public decimal Rollback
	{
		get => _rollback.Value;
		set => _rollback.Value = value;
	}

	/// <summary>
	/// Stop loss distance expressed in absolute price units.
	/// </summary>
	public decimal StopLoss
	{
		get => _stopLoss.Value;
		set => _stopLoss.Value = value;
	}

	/// <summary>
	/// Minimum difference between historical closes to detect momentum.
	/// </summary>
	public decimal Delta
	{
		get => _delta.Value;
		set => _delta.Value = value;
	}

	/// <summary>
	/// Default volume for market orders.
	/// </summary>
	public decimal TradeVolume
	{
		get => _tradeVolume.Value;
		set => _tradeVolume.Value = value;
	}

	/// <summary>
	/// Older bar shift used in the close difference calculation.
	/// </summary>
	public int FirstShift
	{
		get => _firstShift.Value;
		set => _firstShift.Value = value;
	}

	/// <summary>
	/// Recent bar shift used in the close difference calculation.
	/// </summary>
	public int SecondShift
	{
		get => _secondShift.Value;
		set => _secondShift.Value = value;
	}

	/// <summary>
	/// Maximum simultaneous trades counted in volume units.
	/// </summary>
	public int ActiveTrades
	{
		get => _activeTrades.Value;
		set => _activeTrades.Value = value;
	}

	/// <summary>
	/// Candle type used by the strategy.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public GazonkosStrategy()
	{
		_takeProfit = Param(nameof(TakeProfit), 700m)
			.SetDisplay("Take Profit", "Take profit distance in price units", "Risk Management")
			;

		_rollback = Param(nameof(Rollback), 300m)
			.SetDisplay("Rollback", "Required pullback before entering", "Signals")
			;

		_stopLoss = Param(nameof(StopLoss), 1000m)
			.SetDisplay("Stop Loss", "Stop loss distance in price units", "Risk Management")
			;

		_delta = Param(nameof(Delta), 200m)
			.SetDisplay("Delta", "Minimum difference between closes", "Signals")
			;

		_tradeVolume = Param(nameof(TradeVolume), 0.1m)
			.SetDisplay("Trade Volume", "Default volume for market orders", "Orders")
			;

		_firstShift = Param(nameof(FirstShift), 3)
			.SetDisplay("First Shift", "Older close shift for the comparison", "Signals")
			;

		_secondShift = Param(nameof(SecondShift), 2)
			.SetDisplay("Second Shift", "Recent close shift for the comparison", "Signals")
			;

		_activeTrades = Param(nameof(ActiveTrades), 1)
			.SetDisplay("Active Trades", "Maximum simultaneous trades", "Risk Management")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
			.SetDisplay("Candle Type", "Candle series used for signals", "General");
	}

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

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

		_state = 0;
		_tradeDirection = 0;
		_maxPrice = 0m;
		_minPrice = decimal.MaxValue;
		_canTrade = true;
		_lastTradeHour = -1;
		_lastSignalHour = -1;
		_closeHistory.Clear();
		UpdateHistorySize();
	}

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

		Volume = TradeVolume;
		UpdateHistorySize();

		StartProtection(
			takeProfit: new Unit(TakeProfit, UnitTypes.Absolute),
			stopLoss: new Unit(StopLoss, UnitTypes.Absolute),
			isStopTrailing: false,
			useMarketOrders: true);

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

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

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

		UpdateHistorySize();
		AddClose(candle.ClosePrice);

		var hour = candle.CloseTime.Hour;

		if (_state == 0)
		{
			// Evaluate if another trade can be started during the current hour.
			_canTrade = true;

			if (_lastTradeHour == hour)
				_canTrade = false;

			if (ActiveTrades > 0 && Volume > 0 && Math.Abs(Position) >= ActiveTrades * Volume)
				_canTrade = false;

			if (_canTrade)
				_state = 1;
		}

		if (_state == 1)
		{
			// Look for momentum using the difference between historical closes.
			if (!TryGetClose(FirstShift, out var closeFirst) || !TryGetClose(SecondShift, out var closeSecond))
				return;

			if (closeSecond - closeFirst > Delta)
			{
				_tradeDirection = 1;
				_maxPrice = candle.ClosePrice;
				_lastSignalHour = hour;
				_state = 2;
			}
			else if (closeFirst - closeSecond > Delta)
			{
				_tradeDirection = -1;
				_minPrice = candle.ClosePrice;
				_lastSignalHour = hour;
				_state = 2;
			}
		}

		if (_state == 2)
		{
			// Wait for a rollback confirmation during the same hour when the signal appeared.
			if (_lastSignalHour != hour)
			{
				ResetToIdle();
				return;
			}

			if (_tradeDirection == 1)
			{
				if (candle.HighPrice > _maxPrice)
					_maxPrice = candle.HighPrice;

				if (candle.LowPrice < _maxPrice - Rollback)
					_state = 3;
			}
			else if (_tradeDirection == -1)
			{
				if (candle.LowPrice < _minPrice)
					_minPrice = candle.LowPrice;

				if (candle.HighPrice > _minPrice + Rollback)
					_state = 3;
			}
		}

		if (_state == 3)
		{
			// Execute the trade after rollback confirmation.
			if (_tradeDirection == 1 && Position <= 0)
			{
				BuyMarket();
				_lastTradeHour = hour;
				ResetToIdle();
			}
			else if (_tradeDirection == -1 && Position >= 0)
			{
				SellMarket();
				_lastTradeHour = hour;
				ResetToIdle();
			}
		}
	}

	private void UpdateHistorySize()
	{
		var required = Math.Max(Math.Max(FirstShift, SecondShift) + 1, 1);

		if (_maxHistory == required)
			return;

		_maxHistory = required;

		if (_closeHistory.Count > _maxHistory)
			_closeHistory.RemoveRange(_maxHistory, _closeHistory.Count - _maxHistory);
	}

	private void AddClose(decimal close)
	{
		_closeHistory.Insert(0, close);

		if (_closeHistory.Count > _maxHistory)
			_closeHistory.RemoveAt(_closeHistory.Count - 1);
	}

	private bool TryGetClose(int shift, out decimal close)
	{
		close = 0m;

		if (shift < 0)
			return false;

		if (_closeHistory.Count <= shift)
			return false;

		close = _closeHistory[shift];
		return true;
	}

	private void ResetToIdle()
	{
		_state = 0;
		_tradeDirection = 0;
		_maxPrice = 0m;
		_minPrice = decimal.MaxValue;
		_canTrade = true;
		_lastSignalHour = -1;
	}
}