GitHub で見る

Cloudzs Trade 2 戦略

概要

Cloudzs Trade 2 戦略は、MetaTrader 4 エキスパート アドバイザー cloudzs_trade_2 の StockSharp 移植です。オリジナルのロボットは、確率的オシレーター反転とダブルフラクタル確認フィルターを組み合わせ、積極的なトレーリングロジックを使用してオープンポジションを保護します。この C# バージョンは、シグナル フローと取引管理ルールを再作成し、パラメーターを StrategyParam オブジェクトとして公開するので、StockSharp UI から最適化または調整できます。

この戦略は、単一のローソク足シリーズ (構成可能な時間枠) を監視し、2 つの独立した条件を評価します。

  1. Stochastic 反転 – %D ラインが極端なゾーン (売りの場合は >= 80、買いの場合 <= 20) を離れるときにトリガーされ、%D が前のローソク足の %K ラインを横切ったことが確認され、元の MQL ロジックと厳密に一致します。
  2. ダブル フラクタル確認 – 同じタイプの 2 つの連続したフラクタル シグナル (売りの場合は上部の 2 つのフラクタル、買いの場合は下部の 2 つのフラクタル) が表示されるまで待機します。

いずれかの条件によって買いまたは売りリクエストが生成された場合、ストラテジーはその方向にエントリーします (アクティブな取引がなく、前回のエグジットが別の日に行われた場合)。すでに取引を行っている場合、CloseOnOpposite が有効であれば、同じ条件を使用して早期終了できます。

パラメーター

名前 説明 デフォルト
LotSplitter 現在の口座価値から取引量を概算するために使用される係数。 0.1
MaxVolume 計算されたボリュームの上限 (0 はキャップを無効にします)。 0
TakeProfitOffset 絶対価格単位での固定利食い距離。 0
TrailingStopOffset 価格単位でのトレーリングストップ距離。 0.01
StopLossOffset 価格単位での固定ストップロス距離。 0.05
MinProfitOffset ProfitPointsOffset に達した後、有利な推移の後に保持する最小利益。 0
ProfitPointsOffset MinProfitOffset が施行される前に必須の有利な動き。 0
%K Period / %D Period / Slowing Stochastic オシレーター構成。 8 / 8 / 4
Method 元の MT4 確率的メソッド識別子 (情報提供、StockSharp は単一の実装を公開しているため使用されません)。 3
PriceMode 元の MT4 価格モード識別子 (情報提供のみ)。 1
UseStochasticCondition 確率ベースの信号生成を有効にします。 true
UseFractalCondition フラクタルベースの信号生成を有効にします。 true
CloseOnOpposite 反対のシグナルが現れたら、アクティブなポジションを閉じます。 true
CandleType 計算に使用される時間枠/データ型。 15-minute time frame

取引シグナル

ロングエントリー

  • %D ラインは 20 以下で、%K を下回っています (MT4 の以前のローソク足の比較と一致します)。
  • または 2 つの連続した下位フラクタルが検出されます。
  • オープンポジションがなく、最後の決済は別の暦日に行われました。

ショートエントリー

  • %D ラインは 80 以上で、%K を超えています。
  • または 2 つの連続した上部フラクタルが表示されます。
  • オープンポジションがなく、最後の決済は別の暦日に行われました。

終了ルール

  • ハードストップロスまたはテイクプロフィットレベルに達しました(構成されている場合)。
  • トレーリングストップは取引に有利に動き、価格は更新されたストップレベルに達します。
  • ポジションが ProfitPointsOffset の有利な動きを経験した後、MinProfitOffset への反発で取引が終了します。
  • オプションの早期反転: CloseOnOpposite が true で反対のシグナルが発火した場合、取引は終了します。

リスク管理

  • ストップロスとテイクプロフィットの距離は、MT4 コードからの生のピップオフセットを模倣します (ここでは価格差として解釈されます)。
  • トレーリングストップは終値を使用して更新され、収益性の高い方向にのみ移動します。
  • LotSplitter パラメータは、元の取引量の式に従おうとし、取引サイズを口座値によってスケーリングし、それを MaxVolume で制限します。

注意事項と制限事項

  • StockSharp StochasticOscillator は単一のスムージング実装を公開します。したがって、Method パラメータと PriceMode パラメータは参照用に保持されますが、インジケーターの動作は変更されません。
  • 元の MT4 スクリプトはティックごとに動作しました。このポートは、StockSharp のベスト プラクティスに合わせて、完成したローソク足のシグナルを評価します。
  • 出来高の計算は利用可能なポートフォリオの値に依存します。アカウント情報が存在しない場合は、LotSplitter 値に戻ります。

使用法

  1. 戦略を StockSharp プロジェクトに追加し、取引する商品を選択します。
  2. ローソク足の時間枠を構成し、必要に応じて確率的/フラクタル設定を調整します。
  3. 金融商品のティックサイズに一致する現実的なストップロス/テイクプロフィットオフセットを提供します。
  4. Designer、Runner、または API 経由で戦略を開始し、信号情報のログ メッセージを監視します。
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>
/// Port of the "cloudzs trade 2" MetaTrader 4 expert advisor.
/// Combines stochastic reversals with fractal confirmations and mirrors the original trailing logic.
/// </summary>
public class CloudzsTrade2Strategy : Strategy
{
	private readonly StrategyParam<decimal> _lotSplitter;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _takeProfitOffset;
	private readonly StrategyParam<decimal> _trailingStopOffset;
	private readonly StrategyParam<decimal> _stopLossOffset;
	private readonly StrategyParam<decimal> _minProfitOffset;
	private readonly StrategyParam<decimal> _profitPointsOffset;
	private readonly StrategyParam<int> _kPeriod;
	private readonly StrategyParam<int> _dPeriod;
	private readonly StrategyParam<int> _slowingPeriod;
	private readonly StrategyParam<int> _method;
	private readonly StrategyParam<int> _priceMode;
	private readonly StrategyParam<bool> _useStochasticCondition;
	private readonly StrategyParam<bool> _useFractalCondition;
	private readonly StrategyParam<bool> _closeOnOpposite;
	private readonly StrategyParam<DataType> _candleType;

	private StochasticOscillator _stochastic;

	private decimal _previousK;
	private decimal _previousD;
	private decimal _lastK;
	private decimal _lastD;
	private bool _hasPrevious;
	private bool _hasLast;

	private decimal _high1;
	private decimal _high2;
	private decimal _high3;
	private decimal _high4;
	private decimal _high5;
	private decimal _low1;
	private decimal _low2;
	private decimal _low3;
	private decimal _low4;
	private decimal _low5;
	private FractalTypes? _latestFractal;
	private FractalTypes? _previousFractal;
	private int _fractalSeedCount;

	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private decimal _entryPrice;
	private decimal _maxFavorableMove;
	private DateTime? _lastExitDate;

	private enum FractalTypes
	{
		Up,
		Down
	}

	/// <summary>
	/// Lot coefficient used to estimate order size from account value.
	/// </summary>
	public decimal LotSplitter
	{
		get => _lotSplitter.Value;
		set => _lotSplitter.Value = value;
	}

	/// <summary>
	/// Maximum allowed order size.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

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

	/// <summary>
	/// Trailing stop distance expressed in price units.
	/// </summary>
	public decimal TrailingStopOffset
	{
		get => _trailingStopOffset.Value;
		set => _trailingStopOffset.Value = value;
	}

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

	/// <summary>
	/// Minimum profit required to keep the position open after reaching the <see cref="ProfitPointsOffset"/> threshold.
	/// </summary>
	public decimal MinProfitOffset
	{
		get => _minProfitOffset.Value;
		set => _minProfitOffset.Value = value;
	}

	/// <summary>
	/// Profit cushion that must be reached before <see cref="MinProfitOffset"/> becomes active.
	/// </summary>
	public decimal ProfitPointsOffset
	{
		get => _profitPointsOffset.Value;
		set => _profitPointsOffset.Value = value;
	}

	/// <summary>
	/// Lookback period for the stochastic oscillator.
	/// </summary>
	public int KPeriod
	{
		get => _kPeriod.Value;
		set => _kPeriod.Value = value;
	}

	/// <summary>
	/// Smoothing period for the %D line.
	/// </summary>
	public int DPeriod
	{
		get => _dPeriod.Value;
		set => _dPeriod.Value = value;
	}

	/// <summary>
	/// Smoothing period for the %K line.
	/// </summary>
	public int SlowingPeriod
	{
		get => _slowingPeriod.Value;
		set => _slowingPeriod.Value = value;
	}

	/// <summary>
	/// Original stochastic method identifier (kept for reference).
	/// </summary>
	public int Method
	{
		get => _method.Value;
		set => _method.Value = value;
	}

	/// <summary>
	/// Original price mode identifier (kept for reference).
	/// </summary>
	public int PriceMode
	{
		get => _priceMode.Value;
		set => _priceMode.Value = value;
	}

	/// <summary>
	/// Enable stochastic based entry logic.
	/// </summary>
	public bool UseStochasticCondition
	{
		get => _useStochasticCondition.Value;
		set => _useStochasticCondition.Value = value;
	}

	/// <summary>
	/// Enable fractal based entry logic.
	/// </summary>
	public bool UseFractalCondition
	{
		get => _useFractalCondition.Value;
		set => _useFractalCondition.Value = value;
	}

	/// <summary>
	/// Close the active position when the opposite signal appears.
	/// </summary>
	public bool CloseOnOpposite
	{
		get => _closeOnOpposite.Value;
		set => _closeOnOpposite.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="CloudzsTrade2Strategy"/> class.
	/// </summary>
	public CloudzsTrade2Strategy()
	{
		_lotSplitter = Param(nameof(LotSplitter), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Lot Splitter", "Coefficient used to derive order size", "Trading");

		_maxVolume = Param(nameof(MaxVolume), 0m)
			.SetDisplay("Max Volume", "Maximum volume limit (0 disables the cap)", "Trading");

		_takeProfitOffset = Param(nameof(TakeProfitOffset), 0m)
			.SetDisplay("Take Profit", "Take profit distance in price units", "Risk");

		_trailingStopOffset = Param(nameof(TrailingStopOffset), 0.01m)
			.SetDisplay("Trailing Stop", "Trailing stop distance in price units", "Risk");

		_stopLossOffset = Param(nameof(StopLossOffset), 0.05m)
			.SetDisplay("Stop Loss", "Stop loss distance in price units", "Risk");

		_minProfitOffset = Param(nameof(MinProfitOffset), 0m)
			.SetDisplay("Min Profit", "Minimum profit to keep after pullback", "Risk");

		_profitPointsOffset = Param(nameof(ProfitPointsOffset), 0m)
			.SetDisplay("Profit Points", "Favorable move required before min profit rule", "Risk");

		_kPeriod = Param(nameof(KPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("%K Period", "Base length of the stochastic oscillator", "Indicators");

		_dPeriod = Param(nameof(DPeriod), 8)
			.SetGreaterThanZero()
			.SetDisplay("%D Period", "Smoothing length for the stochastic signal", "Indicators");

		_slowingPeriod = Param(nameof(SlowingPeriod), 4)
			.SetGreaterThanZero()
			.SetDisplay("Slowing", "Additional smoothing length for %K", "Indicators");

		_method = Param(nameof(Method), 3)
			.SetDisplay("Method", "Original MQL MA method identifier", "Indicators");

		_priceMode = Param(nameof(PriceMode), 1)
			.SetDisplay("Price Mode", "Original MQL price mode identifier", "Indicators");

		_useStochasticCondition = Param(nameof(UseStochasticCondition), true)
			.SetDisplay("Use Stochastic", "Enable stochastic reversal filter", "Signals");

		_useFractalCondition = Param(nameof(UseFractalCondition), true)
			.SetDisplay("Use Fractals", "Enable double fractal confirmation", "Signals");

		_closeOnOpposite = Param(nameof(CloseOnOpposite), true)
			.SetDisplay("Close On Opposite", "Exit when the opposite signal fires", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for the strategy", "General");
	}

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

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

		_stochastic = null;
		_previousK = 0m;
		_previousD = 0m;
		_lastK = 0m;
		_lastD = 0m;
		_hasPrevious = false;
		_hasLast = false;

		_high1 = _high2 = _high3 = _high4 = _high5 = 0m;
		_low1 = _low2 = _low3 = _low4 = _low5 = 0m;
		_latestFractal = null;
		_previousFractal = null;
		_fractalSeedCount = 0;

		_stopPrice = null;
		_takeProfitPrice = null;
		_entryPrice = 0m;
		_maxFavorableMove = 0m;
		_lastExitDate = null;
	}

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

		_stochastic = new StochasticOscillator
		{
			K = { Length = KPeriod },
			D = { Length = DPeriod }
		};

		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_stochastic, ProcessCandle)
			.Start();

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

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

		UpdateFractals(candle);

		var stochasticSignal = UseStochasticCondition ? EvaluateStochasticSignal(stochasticValue) : 0;
		var fractalSignal = UseFractalCondition ? EvaluateFractalSignal() : 0;

		var combinedSignal = 0;
		if (stochasticSignal == 2 || fractalSignal == 2)
			combinedSignal = 2;
		else if (stochasticSignal == 1 || fractalSignal == 1)
			combinedSignal = 1;

		ManageOpenPosition(candle, combinedSignal);

		if (Position != 0)
			return;

		if (_lastExitDate.HasValue && _lastExitDate.Value == candle.OpenTime.Date)
			return;

		if (combinedSignal == 0)
			return;

		var volume = CalculateOrderVolume(candle.ClosePrice);
		if (volume <= 0m)
			return;

		Volume = volume;

		if (combinedSignal == 1)
		{
			BuyMarket();
			InitializeTargets(candle.ClosePrice, true);
		}
		else if (combinedSignal == 2)
		{
			SellMarket();
			InitializeTargets(candle.ClosePrice, false);
		}
	}

	private decimal CalculateOrderVolume(decimal price)
	{
		if (price <= 0m)
			return LotSplitter;

		var accountValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
		if (accountValue <= 0m)
			accountValue = LotSplitter;

		var estimated = LotSplitter * accountValue / price;
		var normalized = Math.Floor(estimated * 10m) / 10m;

		if (normalized <= 0m)
			normalized = LotSplitter;

		if (MaxVolume > 0m && normalized > MaxVolume)
			normalized = MaxVolume;

		return normalized;
	}

	private int EvaluateStochasticSignal(IIndicatorValue stochasticValue)
	{
		if (_stochastic is null || stochasticValue is not StochasticOscillatorValue typed)
			return 0;

		if (typed.K is not decimal currentK || typed.D is not decimal currentD)
			return 0;

		if (!_hasLast)
		{
			_lastK = currentK;
			_lastD = currentD;
			_hasLast = true;
			return 0;
		}

		if (!_hasPrevious)
		{
			_previousK = _lastK;
			_previousD = _lastD;
			_lastK = currentK;
			_lastD = currentD;
			_hasPrevious = true;
			return 0;
		}

		var sellSignal = _lastD >= 80m && _previousD <= _previousK && _lastD >= _lastK;
		var buySignal = _lastD <= 20m && _previousD >= _previousK && _lastD <= _lastK;

		_previousK = _lastK;
		_previousD = _lastD;
		_lastK = currentK;
		_lastD = currentD;

		if (sellSignal)
			return 2;

		if (buySignal)
			return 1;

		return 0;
	}

	private void UpdateFractals(ICandleMessage candle)
	{
		_high1 = _high2;
		_high2 = _high3;
		_high3 = _high4;
		_high4 = _high5;
		_high5 = candle.HighPrice;

		_low1 = _low2;
		_low2 = _low3;
		_low3 = _low4;
		_low4 = _low5;
		_low5 = candle.LowPrice;

		if (_fractalSeedCount < 5)
		{
			_fractalSeedCount++;
			return;
		}

		var upFractal = _high3 > _high1 && _high3 > _high2 && _high3 > _high4 && _high3 > _high5;
		var downFractal = _low3 < _low1 && _low3 < _low2 && _low3 < _low4 && _low3 < _low5;

		if (upFractal)
			RegisterFractal(FractalTypes.Up);

		if (downFractal)
			RegisterFractal(FractalTypes.Down);
	}

	private void RegisterFractal(FractalTypes type)
	{
		_previousFractal = _latestFractal;
		_latestFractal = type;
	}

	private int EvaluateFractalSignal()
	{
		if (_latestFractal is null || _previousFractal is null)
			return 0;

		if (_latestFractal == FractalTypes.Up && _previousFractal == FractalTypes.Up)
			return 2;

		if (_latestFractal == FractalTypes.Down && _previousFractal == FractalTypes.Down)
			return 1;

		return 0;
	}

	private void ManageOpenPosition(ICandleMessage candle, int combinedSignal)
	{
		if (Position == 0)
			return;

		if (Position > 0)
			ManageLongPosition(candle, combinedSignal);
		else
			ManageShortPosition(candle, combinedSignal);
	}

	private void ManageLongPosition(ICandleMessage candle, int combinedSignal)
	{
		UpdateTrailingStop(candle, true);

		if (_stopPrice is decimal stop && candle.LowPrice <= stop)
		{
			SellMarket();
			FinalizeExit(candle);
			return;
		}

		if (_takeProfitPrice is decimal take && candle.HighPrice >= take)
		{
			SellMarket();
			FinalizeExit(candle);
			return;
		}

		var currentGain = candle.ClosePrice - _entryPrice;
		var favorable = candle.HighPrice - _entryPrice;
		if (favorable > _maxFavorableMove)
			_maxFavorableMove = favorable;

		if (ProfitPointsOffset > 0m && _maxFavorableMove >= ProfitPointsOffset && currentGain <= MinProfitOffset)
		{
			SellMarket();
			FinalizeExit(candle);
			return;
		}

		if (CloseOnOpposite && combinedSignal == 2)
		{
			SellMarket();
			FinalizeExit(candle);
		}
	}

	private void ManageShortPosition(ICandleMessage candle, int combinedSignal)
	{
		UpdateTrailingStop(candle, false);

		if (_stopPrice is decimal stop && candle.HighPrice >= stop)
		{
			BuyMarket();
			FinalizeExit(candle);
			return;
		}

		if (_takeProfitPrice is decimal take && candle.LowPrice <= take)
		{
			BuyMarket();
			FinalizeExit(candle);
			return;
		}

		var currentGain = _entryPrice - candle.ClosePrice;
		var favorable = _entryPrice - candle.LowPrice;
		if (favorable > _maxFavorableMove)
			_maxFavorableMove = favorable;

		if (ProfitPointsOffset > 0m && _maxFavorableMove >= ProfitPointsOffset && currentGain <= MinProfitOffset)
		{
			BuyMarket();
			FinalizeExit(candle);
			return;
		}

		if (CloseOnOpposite && combinedSignal == 1)
		{
			BuyMarket();
			FinalizeExit(candle);
		}
	}

	private void UpdateTrailingStop(ICandleMessage candle, bool isLong)
	{
		if (TrailingStopOffset <= 0m)
			return;

		if (isLong)
		{
			var potentialStop = candle.ClosePrice - TrailingStopOffset;
			if (_stopPrice is null || potentialStop > _stopPrice)
			{
				if (potentialStop > _entryPrice)
					_stopPrice = potentialStop;
			}
		}
		else
		{
			var potentialStop = candle.ClosePrice + TrailingStopOffset;
			if (_stopPrice is null || potentialStop < _stopPrice)
			{
				if (potentialStop < _entryPrice)
					_stopPrice = potentialStop;
			}
		}
	}

	private void InitializeTargets(decimal entryPrice, bool isLong)
	{
		_entryPrice = entryPrice;
		_maxFavorableMove = 0m;

		_stopPrice = StopLossOffset > 0m
			? isLong ? entryPrice - StopLossOffset : entryPrice + StopLossOffset
			: null;

		_takeProfitPrice = TakeProfitOffset > 0m
			? isLong ? entryPrice + TakeProfitOffset : entryPrice - TakeProfitOffset
			: null;
	}

	private void FinalizeExit(ICandleMessage candle)
	{
		_stopPrice = null;
		_takeProfitPrice = null;
		_entryPrice = 0m;
		_maxFavorableMove = 0m;
		_lastExitDate = candle.OpenTime.Date;
	}
}