GitHub で見る

FxNode セーフトンネル戦略

概要

この戦略は、MetaTrader 4のエキスパートアドバイザー FxNode - Safe Tunnel をStockSharpに移植したものです。システムはZigZagベースのトレンドチャネルを使用します:最新のスウィング高値を結んでレジスタンスラインを形成し、スウィング安値はサポートラインを作成します。市場価格が設定可能な許容範囲内でチャネルの境界のいずれかに触れ、すべての安全チェックが通過した場合にポジションが開かれます。

変換は元のワークフローに従いながら、StockSharpの高レベルAPIに適応させています:

  • ローソク足のサブスクリプションがロジックを駆動します。完全に形成されたローソク足のみが処理されます。
  • Highest/Lowestのペアがトンネルのトレンドラインを描くために使用されるZigZagディテクターをエミュレートします。
  • AverageTrueRangeインジケーターが、MQLバージョンがATRCheck() * 10で生成していたボラティリティベースのストップアンカーを提供します。
  • Level1クォートが監視され、新しいトレードを許可する前に戦略が最大スプレッドを適用できます。

エントリー条件

  1. 設定可能なZigZag深度、偏差(pips単位)、バックステップでスウィング高値と安値を検出します。最新の2つの高値と2つの安値がトレンドラインを定義します。
  2. 現在のローソク足の終値時点での各トレンドラインの価格を計算し、最新のスウィング高値と安値の間の垂直距離を測定します。
  3. ロングセットアップ:最良のask価格は下方トレンドラインより上に留まる必要がありますが、TouchDistanceBuyPipsバッファを超えてはいけません。ショートは上方トレンドラインと最良のbidの周辺で条件を反映します。
  4. オプションのセッションフィルター(デフォルトは深夜~06:00)がトレードを許可する必要があります。戦略はまた、元のAllowToOrder()の制限を模倣して、金曜日、土曜日、日曜日の新規注文をブロックします。
  5. クォートが利用可能な場合、現在のスプレッド(ask – bid)がMaxSpreadPipsを超えてはなりません。
  6. MaxOpenPositionsは最大ネットエクスポージャーを制御します。StockSharpはネッティングを使用するため、この値は個別のチケットではなく総ポジションボリュームの上限として機能します。

エグジット条件

  • 初期ストップロス:元のEAはそれをATR * 10に設定しました。ポートはMaxStopLossPipsの上限を尊重しながら同じ乗数を維持します。
  • 初期テイクプロフィット:デフォルトは最新のスウィング高値と安値の間の距離ですが、設定された場合はTakeProfitPipsによって制限されます。
  • 固定利益目標:FixedTakeProfitPipsがゼロより大きい場合、価格がエントリーからその数のpipsを獲得すると、ポジションが決済されます。
  • トレーリングストップ:ローソク足の終値がトレードに有利な方向にTrailingStopPipsを超えて動くと、ストップロスが利益をロックするように締め付けられます。
  • 週末エグジット:CloseBeforeWeekendが有効な場合、金曜日の23:50以降にオープンポジションが決済されます。

すべてのエグジットは、元の動作と一致するよう成行注文で実行されます。

リスクとサイジング

ロットサイズは3段階で計算されます:

  1. 銘柄の価格ステップと金銭的なステップ価値が既知であると仮定して、ポートフォリオ価値のRiskPercentageをリスクにさらすことを試みます。
  2. リスクサイジングが計算できない場合、StaticVolumeにフォールバックします。
  3. 最終ボリュームをMinVolumeMaxVolumeの間にクランプします。

StockSharpは銘柄ごとに単一のネットポジションを報告するため、元のMaxOpenPositionの制限は独立したチケット数ではなく、最大総エクスポージャーとして解釈されます。

パラメーター

名前 デフォルト 説明
CandleType 30分ローソク足 分析とトレードの主要な時間軸。
TrendPreference 両方 ロングのみ、ショートのみ、または対称的なトレードを選択します。
TakeProfitPips 800 pipsでの最大テイクプロフィット距離(0は制限を無効にします)。
MaxStopLossPips 200 pipsでの最大ストップロス距離(0は制限を無効にします)。
FixedTakeProfitPips 0 pipsで表した早期エグジット距離。
TouchDistanceBuyPips 20 ロングエントリーでは、ask価格がこのバッファ内で下方トレンドラインより上に留まる必要があります。
TouchDistanceSellPips 20 ショートエントリーは上方トレンドライン近くでバッファ要件を反映します。
TrailingStopPips 50 トレードが利益になった後に適用されるトレーリング距離。
StaticVolume 1 リスクベースのサイジングが不可能な場合のフォールバック注文ボリューム。
MinVolume / MaxVolume 0.02 / 10 最終注文ボリュームの境界。
MaxSpreadPips 15 新規エントリーに対してpipsで許可される最大スプレッド。
RiskPercentage 30 トレードごとにリスクにさらされるポートフォリオのパーセンテージ。常にStaticVolumeを使用するには0に設定します。
MaxOpenPositions 1 最大ネットエクスポージャー(現在の注文ボリュームの倍数)。
UseTimeFilter true トレードウィンドウを有効にします。
SessionStart / SessionEnd 00:00 / 06:00 トレードウィンドウ。開始が終了より遅い場合、ウィンドウは深夜をまたぎます。
CloseBeforeWeekend true 金曜日の23:50以降にオープンポジションを決済します。
AtrPeriod 14 ストップ計算に使用するATRのルックバック。
ZigZagDepth 5 ZigZagルックバック深度。
ZigZagDeviationPips 3 連続するピボット間の最小距離(pips単位)。
ZigZagBackstep 1 有効なピボット間のバー数。
ZigZagHistory 10 トレンドライン投影のために保存されるピボットの数。

注意事項と制限

  • ZigZagの再構築は、Highest/Lowestインジケーターを偏差とバックステップフィルターと組み合わせることでMQLの動作を反映します。銘柄がカスタムセッションで取引される場合、元のインジケーターと合わせるためにパラメーターを調整することを検討してください。
  • スプレッドフィルタリングにはライブのbid/askクォートが必要です。クォートが不在の場合(例えば、ローソク足のみのデータでのバックテスト中)、スプレッドフィルターはスキップされます。
  • ポートはネットポジションで動作します。独立したチケット管理が必要な環境では、各フィルを個別に追跡するように戦略を拡張する必要があります。
  • MQLバージョンの時間文字列(例:"24:00")はTimeSpanパラメーターに置き換えられます。夜間セッションを再現するには、開始を終了より遅く設定します。例えば23:30から05:30。

使用方法

  1. 戦略を銘柄にアタッチし、ローソク足タイプとパラメーターを設定し、シミュレーションまたはライブモードで実行します。
  2. スプレッドフィルターを正確に適用するために、マーケットデプスまたはLevel1サブスクリプションが有効になっていることを確認します。
  3. 実際の資金で取引する前にリスクコントロールを確認して調整します。
using System;
using System.Collections.Generic;

using Ecng.Common;

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

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Simplified conversion of the FxNode Safe Tunnel EA.
/// Uses Highest/Lowest channel (tunnel) with ATR-based stops.
/// Buys near the lower boundary and sells near the upper boundary.
/// </summary>
public class FxNodeSafeTunnelStrategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _channelPeriod;
	private readonly StrategyParam<int> _atrPeriod;
	private readonly StrategyParam<decimal> _touchPct;

	private decimal _entryPrice;
	private int _cooldown;

	public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
	public int ChannelPeriod { get => _channelPeriod.Value; set => _channelPeriod.Value = value; }
	public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
	public decimal TouchPct { get => _touchPct.Value; set => _touchPct.Value = value; }

	public FxNodeSafeTunnelStrategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe", "General");

		_channelPeriod = Param(nameof(ChannelPeriod), 100)
			.SetGreaterThanZero()
			.SetDisplay("Channel Period", "Lookback for Highest/Lowest channel", "Indicator");

		_atrPeriod = Param(nameof(AtrPeriod), 14)
			.SetGreaterThanZero()
			.SetDisplay("ATR Period", "ATR lookback for stops", "Indicator");

		_touchPct = Param(nameof(TouchPct), 0.02m)
			.SetDisplay("Touch %", "How close price must be to channel boundary (0-1)", "Indicator");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_entryPrice = 0;
		_cooldown = 0;
	}

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

		_entryPrice = 0;

		var highest = new Highest { Length = ChannelPeriod };
		var lowest = new Lowest { Length = ChannelPeriod };
		var atr = new AverageTrueRange { Length = AtrPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(highest, lowest, atr, ProcessCandle)
			.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal high, decimal low, decimal atrVal)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_cooldown > 0)
		{
			_cooldown--;
			return;
		}

		var channelWidth = high - low;
		if (channelWidth <= 0)
			return;

		var touchZone = channelWidth * TouchPct;
		var close = candle.ClosePrice;

		// Check stop/take for active positions
		if (Position > 0)
		{
			// Exit long: price near upper channel or stop loss
			if (close >= high - touchZone || (_entryPrice > 0 && close < _entryPrice - atrVal * 2))
			{
				SellMarket();
				_entryPrice = 0;
				_cooldown = 10;
				return;
			}
		}
		else if (Position < 0)
		{
			// Exit short: price near lower channel or stop loss
			if (close <= low + touchZone || (_entryPrice > 0 && close > _entryPrice + atrVal * 2))
			{
				BuyMarket();
				_entryPrice = 0;
				_cooldown = 10;
				return;
			}
		}

		// Entry signals
		if (Position <= 0 && close <= low + touchZone)
		{
			// Price near lower boundary - buy
			if (Position < 0) BuyMarket(); // close short first
			BuyMarket();
			_entryPrice = close;
			_cooldown = 10;
		}
		else if (Position >= 0 && close >= high - touchZone)
		{
			// Price near upper boundary - sell
			if (Position > 0) SellMarket(); // close long first
			SellMarket();
			_entryPrice = close;
			_cooldown = 10;
		}
	}
}