GitHub で見る

ChandelExit 再エントリー戦略

この戦略は、MetaTraderのエキスパート "Exp_ChandelExitSign_ReOpen" をStockSharpの高レベルAPIに移植したものです。Chandelier Exitバンドを使用してブレイクアウトを取引し、トレンドが継続する場合にポジションを自動的に再開します。システムは設定可能な上位時間軸で計算されたインジケーターシグナルに反応しながら、ATRベースのストップとオプションのテイクプロフィットレベルでリスクを管理します。

核心的なアイデアは、Chandelier Exitをトレンドフィルターと動的なトレーリングバリアの両方として扱うことです。下バンドが上バンドを上抜けすると強気のインパルスが検出され、逆の場合は弱気のインパルスが現れます。戦略はロングとショートの両側で対称的に機能することができ、各シグナルはパラメーターを通じて個別に有効化または無効化できます。ポジションに入った後、追加注文が許可される前に価格が一定数の価格ステップ(PriceStepPoints)進む必要があります。追加は元のエキスパートアドバイザーの動作を模倣し、ポジションサイズの暴走を防ぐために MaxAdditions で制限されています。

トレードロジック

  • シグナル計算
    • RangePeriod バー(Shift でオフセット)は、Chandelier Exitバンドが使用する最高の高値と最低の安値を定義します。
    • AtrPeriodAtrMultiplier は、エグジットバンドを価格から遠ざけるボラティリティバッファを生成します。
    • SignalBar(デフォルト1)は実行を遅延させるため、戦略が前回の完成したローソク足で行動し、MT5実装を再現します。
  • エントリー
    • ロング: 下バンドが上バンドを上抜けすると(IsUpSignal)トリガーされます。EnableBuyEntries = true が必要です。ショートポジションが存在する場合、EnableSellExits = true のときまず平滑化を試みます。
    • ショート: バンドが逆方向にクロスする(IsDownSignal)と EnableSellEntries = true でトリガーされます。既存のロングは EnableBuyExits = true の場合のみクローズされます。
  • エグジット
    • ロングポジションは EnableBuyExits = true のときの弱気シグナルで、または保護的なストップ/ターゲットに到達したときにクローズします。
    • ショートポジションは EnableSellExits = true のときの強気シグナルで、または保護レベルを通してクローズします。
    • エントリーとエグジットの両方のトグルが有効になっている場合、戦略は最新のローソク足がエントリーのみを生成した場合でもクローズシグナルが利用できるように古いインジケーター値もスキャンします。
  • 再エントリー / スケールイン
    • 各エントリー後、最後の約定価格が保存されます。価格が少なくとも PriceStepPoints * PriceStep だけ有利な方向に動くと、Volume サイズの追加注文が MaxAdditions 回まで送信されます。
    • 各追加は、保護が最新の露出に近く保たれるよう、最新の約定にストップ/テイク計算をリセットします。
  • リスク管理
    • StopLossPointsTakeProfitPoints は最後の約定からの価格ステップで距離を表します。ストップとターゲットはオプションで、無効にするにはゼロに設定します。
    • すべての保護チェックは完成したローソク足ごとに実行されます。価格がイントラバーでストップまたはターゲットに違反した場合、ポジションは成行でクローズされます。

デフォルトパラメーター

パラメーター デフォルト 説明
CandleType TimeSpan.FromHours(4).TimeFrame() インジケーター計算に使用する時間軸。
RangePeriod 15 最高値/最低値の観察ウィンドウ。
Shift 1 範囲を計算する前にスキップする最近のバー数。
AtrPeriod 14 ボラティリティバッファのATR長。
AtrMultiplier 4 バッファに適用するATR乗数。
SignalBar 1 シグナルを読み取る完了バーの数(最新から)。
PriceStepPoints 300 トレードに追加する前の価格ステップでの最小有利な動き。
MaxAdditions 10 最初のエントリー後の追加注文の最大数。
StopLossPoints 1000 価格ステップでのストップロス距離。
TakeProfitPoints 2000 価格ステップでのテイクプロフィット距離。
EnableBuyEntries / EnableSellEntries true シグナルでロング/ショートトレードを開くことを許可。
EnableBuyExits / EnableSellExits true 反対シグナルでロング/ショートトレードをクローズすることを許可。

実践的なメモ

  • 戦略は基本注文サイズを定義するために Volume に依存します。追加トレードは同じサイズを再利用します。リスク制限に合わせて Volume または MaxAdditions を調整します。
  • 再エントリーは価格ステップで表現された動きを必要とするため、証券メタデータ(PriceStep)が正しく設定されていることを確認します。大きなポイント値を持つ銘柄は異なるデフォルトが必要な場合があります。
  • SignalBar をゼロに設定して最新の完成したローソク足で行動できますが、元のエキスパートはシグナルを生成したローソク足で行動することを避けるために1バー遅延を使用していました。
  • 両方のロングとショートトレードをサポートするシンボル/ポートフォリオの組み合わせで戦略を開始します。必要に応じて一方向に制限するために組み込みパラメータートグルを使用します。
  • チャートヘルパー(DrawCandlesDrawIndicatorDrawOwnTrades)はチャートエリアが利用可能なときに自動的に起動し、バンドと約定の視覚化を容易にします。

ワークフロー例

  1. 強気のクロスオーバーを待ちます:上位時間軸のローソク足で下バンドが上バンドを上抜けします。
  2. ポジションが存在せず、ロングエントリーが有効化されている場合、Volume サイズの成行買い注文を出します。ストップとターゲットは約定価格を基準に設定されます。
  3. 価格が少なくとも PriceStepPoints * PriceStep 上昇したら、追加の買い注文を送信します(MaxAdditions を守りながら)。
  4. 弱気シグナルが現れたとき、ストップロスに達したとき、またはテイクプロフィットに達したときにロング全体をクローズします。プロセスはショートトレードのために対称的です。

このドキュメントは、戦略パラメーター、高レベルのローソク足サブスクリプション、明示的なポジション管理などのStockSharp規約を採用しながら、元のMT5戦略を反映しています。

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>
/// Strategy converted from the ChandelExitSign expert advisor with re-entry logic.
/// </summary>
public class ChandelExitReopenStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _rangePeriod;
	private readonly StrategyParam<int> _shift;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _atrMultiplier;
	private readonly StrategyParam<int> _signalBar;
	private readonly StrategyParam<decimal> _priceStepPoints;
	private readonly StrategyParam<int> _maxAdditions;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<bool> _enableBuyEntries;
	private readonly StrategyParam<bool> _enableSellEntries;
	private readonly StrategyParam<bool> _enableBuyExits;
	private readonly StrategyParam<bool> _enableSellExits;

	private readonly List<CandleInfo> _history = new();
	private readonly List<SignalInfo> _signals = new();

	private decimal? _previousUp;
	private decimal? _previousDown;
	private int _direction;

	private int _longAdditions;
	private int _shortAdditions;
	private decimal? _longEntryPrice;
	private decimal? _shortEntryPrice;
	private decimal? _longStopPrice;
	private decimal? _shortStopPrice;
	private decimal? _longTakePrice;
	private decimal? _shortTakePrice;
	private DateTimeOffset? _lastLongAdditionTime;
	private DateTimeOffset? _lastShortAdditionTime;

	/// <summary>
	/// Initializes a new instance of <see cref="ChandelExitReopenStrategy"/>.
	/// </summary>
	public ChandelExitReopenStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Type of candles used for signals", "General");

		_rangePeriod = Param(nameof(RangePeriod), 15)
			.SetDisplay("Range Period", "Lookback for highest high and lowest low", "Indicator")
			.SetGreaterThanZero()
			;

		_shift = Param(nameof(Shift), 1)
			.SetDisplay("Shift", "Bars to skip from the most recent data", "Indicator")
			.SetNotNegative()
			;

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetDisplay("ATR Period", "ATR length for volatility filter", "Indicator")
			.SetGreaterThanZero()
			;

		_atrMultiplier = Param(nameof(AtrMultiplier), 4m)
			.SetDisplay("ATR Multiplier", "Multiplier applied to ATR", "Indicator")
			.SetGreaterThanZero()
			;

		_signalBar = Param(nameof(SignalBar), 1)
			.SetDisplay("Signal Bar", "How many bars back to read signals", "Trading")
			.SetNotNegative();

		_priceStepPoints = Param(nameof(PriceStepPoints), 1000m)
			.SetDisplay("Re-entry Distance", "Minimum favorable move in price steps before adding", "Position Management")
			.SetNotNegative()
			;

		_maxAdditions = Param(nameof(MaxAdditions), 1)
			.SetDisplay("Max Additions", "Maximum number of re-entries after the initial position", "Position Management")
			.SetNotNegative();

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
			.SetDisplay("Stop Loss Points", "Stop-loss distance in price steps", "Risk Management")
			.SetNotNegative();

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 2000)
			.SetDisplay("Take Profit Points", "Take-profit distance in price steps", "Risk Management")
			.SetNotNegative();

		_enableBuyEntries = Param(nameof(EnableBuyEntries), true)
			.SetDisplay("Enable Long Entries", "Allow opening long positions on up signals", "Trading");

		_enableSellEntries = Param(nameof(EnableSellEntries), true)
			.SetDisplay("Enable Short Entries", "Allow opening short positions on down signals", "Trading");

		_enableBuyExits = Param(nameof(EnableBuyExits), true)
			.SetDisplay("Enable Long Exits", "Allow closing long positions on down signals", "Trading");

		_enableSellExits = Param(nameof(EnableSellExits), true)
			.SetDisplay("Enable Short Exits", "Allow closing short positions on up signals", "Trading");
	}

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

	/// <summary>
	/// Range length for the Chandelier exit bands.
	/// </summary>
	public int RangePeriod
	{
		get => _rangePeriod.Value;
		set => _rangePeriod.Value = value;
	}

	/// <summary>
	/// Number of the most recent bars skipped before measuring the range.
	/// </summary>
	public int Shift
	{
		get => _shift.Value;
		set => _shift.Value = value;
	}

	/// <summary>
	/// ATR length used in the signal calculation.
	/// </summary>
	public int AtrPeriod
	{
		get => _atrPeriod.Value;
		set => _atrPeriod.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the ATR value.
	/// </summary>
	public decimal AtrMultiplier
	{
		get => _atrMultiplier.Value;
		set => _atrMultiplier.Value = value;
	}

	/// <summary>
	/// Offset of the signal bar relative to the latest finished candle.
	/// </summary>
	public int SignalBar
	{
		get => _signalBar.Value;
		set => _signalBar.Value = value;
	}

	/// <summary>
	/// Required move in price steps before another position add is allowed.
	/// </summary>
	public decimal PriceStepPoints
	{
		get => _priceStepPoints.Value;
		set => _priceStepPoints.Value = value;
	}

	/// <summary>
	/// Maximum number of additional entries after the first fill.
	/// </summary>
	public int MaxAdditions
	{
		get => _maxAdditions.Value;
		set => _maxAdditions.Value = value;
	}

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

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

	/// <summary>
	/// Enables long entries generated by the up buffer.
	/// </summary>
	public bool EnableBuyEntries
	{
		get => _enableBuyEntries.Value;
		set => _enableBuyEntries.Value = value;
	}

	/// <summary>
	/// Enables short entries generated by the down buffer.
	/// </summary>
	public bool EnableSellEntries
	{
		get => _enableSellEntries.Value;
		set => _enableSellEntries.Value = value;
	}

	/// <summary>
	/// Enables long exits on down signals.
	/// </summary>
	public bool EnableBuyExits
	{
		get => _enableBuyExits.Value;
		set => _enableBuyExits.Value = value;
	}

	/// <summary>
	/// Enables short exits on up signals.
	/// </summary>
	public bool EnableSellExits
	{
		get => _enableSellExits.Value;
		set => _enableSellExits.Value = value;
	}

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

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

		_history.Clear();
		_signals.Clear();

		_previousUp = null;
		_previousDown = null;
		_direction = 0;

		ResetLongState();
		ResetShortState();
	}

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

		var atr = new AverageTrueRange { Length = AtrPeriod };
		var subscription = SubscribeCandles(CandleType);

		subscription
			.BindEx(atr, ProcessCandle)
			.Start();

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

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

		var atr = atrValue.IsFinal ? atrValue.ToDecimal() : 0m;
		var info = new CandleInfo(candle.OpenTime, candle.HighPrice, candle.LowPrice, candle.ClosePrice, atr);

		_history.Add(info);

		SignalInfo signal;
		if (atrValue.IsFinal)
		{
			signal = CalculateSignal(info);
		}
		else
		{
			signal = SignalInfo.Empty(info.Time);
		}

		_signals.Add(signal);
		TrimCache();

		if (!atrValue.IsFinal)
			return;

		if (_signals.Count <= SignalBar)
			return;

		var signals = _signals.ToArray();
		var targetIndex = signals.Length - 1 - SignalBar;
		if (targetIndex < 0)
			return;

		var targetSignal = signals[targetIndex];
		if (targetSignal is null)
			return;

		var buyOpen = targetSignal.IsUpSignal && EnableBuyEntries;
		var sellOpen = targetSignal.IsDownSignal && EnableSellEntries;
		var buyClose = targetSignal.IsDownSignal && EnableBuyExits;
		var sellClose = targetSignal.IsUpSignal && EnableSellExits;

		if (((EnableBuyEntries && EnableBuyExits) || (EnableSellEntries && EnableSellExits)) && !buyClose && !sellClose)
		{
			for (var idx = targetIndex - 1; idx >= 0; idx--)
			{
				var previousSignal = signals[idx];
				if (previousSignal is null)
					continue;

				if (!sellClose && EnableSellExits && previousSignal.IsUpSignal)
				{
					sellClose = true;
					break;
				}

				if (!buyClose && EnableBuyExits && previousSignal.IsDownSignal)
				{
					buyClose = true;
					break;
				}
			}
		}

		var step = Security.PriceStep ?? 1m;
		var priceStep = PriceStepPoints * step;

		var longClosed = false;
		var shortClosed = false;

		if (Position > 0m)
		{
			if (_longStopPrice is decimal sl && candle.LowPrice <= sl)
			{
				SellMarket();
				ResetLongState();
				longClosed = true;
				this.LogInfo($"Long stop triggered at {sl:0.########}");
			}
			else if (_longTakePrice is decimal tp && candle.HighPrice >= tp)
			{
				SellMarket();
				ResetLongState();
				longClosed = true;
				this.LogInfo($"Long take profit triggered at {tp:0.########}");
			}
		}

		if (Position < 0m)
		{
			if (_shortStopPrice is decimal sl && candle.HighPrice >= sl)
			{
				BuyMarket();
				ResetShortState();
				shortClosed = true;
				this.LogInfo($"Short stop triggered at {sl:0.########}");
			}
			else if (_shortTakePrice is decimal tp && candle.LowPrice <= tp)
			{
				BuyMarket();
				ResetShortState();
				shortClosed = true;
				this.LogInfo($"Short take profit triggered at {tp:0.########}");
			}
		}

		if (!longClosed && buyClose && Position > 0m)
		{
			SellMarket();
			ResetLongState();
			longClosed = true;
			this.LogInfo($"Long exit on down signal at {candle.ClosePrice:0.########}");
		}

		if (!shortClosed && sellClose && Position < 0m)
		{
			BuyMarket();
			ResetShortState();
			shortClosed = true;
			this.LogInfo($"Short exit on up signal at {candle.ClosePrice:0.########}");
		}

		if (!longClosed && Position > 0m && MaxAdditions > 0 && _longEntryPrice is decimal lastLongPrice && priceStep > 0m && _longAdditions < MaxAdditions)
		{
			if (candle.ClosePrice - lastLongPrice >= priceStep && _lastLongAdditionTime != candle.OpenTime)
			{
				if (Volume > 0m)
				{
					BuyMarket();
					_longAdditions++;
					_longEntryPrice = candle.ClosePrice;
					_lastLongAdditionTime = candle.OpenTime;
					UpdateLongProtection(candle.ClosePrice, step);
					this.LogInfo($"Added to long position at {candle.ClosePrice:0.########} (add #{_longAdditions})");
				}
			}
		}

		if (!shortClosed && Position < 0m && MaxAdditions > 0 && _shortEntryPrice is decimal lastShortPrice && priceStep > 0m && _shortAdditions < MaxAdditions)
		{
			if (lastShortPrice - candle.ClosePrice >= priceStep && _lastShortAdditionTime != candle.OpenTime)
			{
				if (Volume > 0m)
				{
					SellMarket();
					_shortAdditions++;
					_shortEntryPrice = candle.ClosePrice;
					_lastShortAdditionTime = candle.OpenTime;
					UpdateShortProtection(candle.ClosePrice, step);
					this.LogInfo($"Added to short position at {candle.ClosePrice:0.########} (add #{_shortAdditions})");
				}
			}
		}

		if (buyOpen && Position < 0m && !EnableSellExits)
		buyOpen = false;

		if (sellOpen && Position > 0m && !EnableBuyExits)
		sellOpen = false;

		if (buyOpen && Volume > 0m)
		{
			BuyMarket();
			ResetShortState();
			_longAdditions = 0;
			_longEntryPrice = candle.ClosePrice;
			_lastLongAdditionTime = candle.OpenTime;
			UpdateLongProtection(candle.ClosePrice, step);
			this.LogInfo($"Opened long position at {candle.ClosePrice:0.########}");
		}

		if (sellOpen && Volume > 0m)
		{
			SellMarket();
			ResetLongState();
			_shortAdditions = 0;
			_shortEntryPrice = candle.ClosePrice;
			_lastShortAdditionTime = candle.OpenTime;
			UpdateShortProtection(candle.ClosePrice, step);
			this.LogInfo($"Opened short position at {candle.ClosePrice:0.########}");
		}
	}

	private void TrimCache()
	{
		var maxItems = Math.Max(RangePeriod + Shift + 5, SignalBar + 5) + 50;
		if (_history.Count <= maxItems)
			return;

		var removeCount = _history.Count - maxItems;
		_history.RemoveRange(0, removeCount);
		_signals.RemoveRange(0, removeCount);
	}

	private SignalInfo CalculateSignal(CandleInfo current)
	{
		var history = _history.ToArray();
		var currentIndex = history.Length - 1;
		var range = RangePeriod;
		var shift = Shift;

		if (range <= 0 || currentIndex - shift < 0)
		return SignalInfo.Empty(current.Time);

		var windowEnd = currentIndex - shift;
		var windowStart = windowEnd - (range - 1);

		if (windowStart < 0 || windowEnd >= history.Length)
		return SignalInfo.Empty(current.Time);

		var highestHigh = decimal.MinValue;
		var lowestLow = decimal.MaxValue;

		for (var i = windowStart; i <= windowEnd; i++)
		{
			var item = history[i];
			if (item is null)
				continue;

			if (item.High > highestHigh)
			highestHigh = item.High;
			if (item.Low < lowestLow)
			lowestLow = item.Low;
		}

		if (highestHigh == decimal.MinValue || lowestLow == decimal.MaxValue)
			return SignalInfo.Empty(current.Time);

		var atr = current.Atr * AtrMultiplier;
		var upperBand = highestHigh - atr;
		var lowerBand = lowestLow + atr;

		decimal up;
		decimal down;

		if (_direction >= 0)
		{
			if (current.Close < upperBand)
			{
				_direction = -1;
				up = lowerBand;
				down = upperBand;
			}
			else
			{
				up = upperBand;
				down = lowerBand;
			}
		}
		else
		{
			if (current.Close > lowerBand)
			{
				_direction = 1;
				down = lowerBand;
				up = upperBand;
			}
			else
			{
				up = lowerBand;
				down = upperBand;
			}
		}

		var isUpSignal = false;
		var isDownSignal = false;

		if (_previousDown is decimal prevDn && _previousUp is decimal prevUp)
		{
			if (prevDn <= prevUp && down > up)
			isUpSignal = true;

			if (prevDn >= prevUp && down < up)
			isDownSignal = true;
		}

		_previousUp = up;
		_previousDown = down;

		return new SignalInfo(current.Time, isUpSignal, isDownSignal, up, down);
	}

	private void UpdateLongProtection(decimal entryPrice, decimal step)
	{
		_longStopPrice = StopLossPoints > 0 ? entryPrice - StopLossPoints * step : null;
		_longTakePrice = TakeProfitPoints > 0 ? entryPrice + TakeProfitPoints * step : null;
	}

	private void UpdateShortProtection(decimal entryPrice, decimal step)
	{
		_shortStopPrice = StopLossPoints > 0 ? entryPrice + StopLossPoints * step : null;
		_shortTakePrice = TakeProfitPoints > 0 ? entryPrice - TakeProfitPoints * step : null;
	}

	private void ResetLongState()
	{
		_longAdditions = 0;
		_longEntryPrice = null;
		_longStopPrice = null;
		_longTakePrice = null;
		_lastLongAdditionTime = null;
	}

	private void ResetShortState()
	{
		_shortAdditions = 0;
		_shortEntryPrice = null;
		_shortStopPrice = null;
		_shortTakePrice = null;
		_lastShortAdditionTime = null;
	}

	private sealed record CandleInfo(DateTimeOffset Time, decimal High, decimal Low, decimal Close, decimal Atr);

	private sealed record SignalInfo(DateTimeOffset Time, bool IsUpSignal, bool IsDownSignal, decimal Up, decimal Down)
	{
		public static SignalInfo Empty(DateTimeOffset time) => new(time, false, false, 0m, 0m);
	}
}