在 GitHub 上查看

Clouds Trade 2 策略

本策略为 Vladimir Karputov 的 "cloud's trade 2" EA 的 C# 版本。它结合了两次最近的比尔·威廉姆斯分形信号以及在超买/超卖区域的随机指标交叉来捕捉突破。风控部分复刻原始输入,提供固定止损止盈、带步长的移动止损以及按金额和点数锁定利润的功能。

交易逻辑

  • 数据源:单一时间框蜡烛线(默认 15 分钟)。
  • 指标
    • 随机指标,使用可配置的 %K 回溯长度、平滑长度和 %D 平滑。
    • 五根蜡烛的滑动高低价窗口,用于重建上下分形。
  • 开仓条件
    • 做多:最新的两个有效分形均为下分形,或 %D 下降到 20 以下并向下穿越 %K。当前不得持有仓位,并且“每日一次”过滤器允许开仓。
    • 做空:最新两个分形均为上分形,或 %D 升至 80 以上并向上穿越 %K。
  • 平仓与风控
    • 以入场价为基准的固定止损与止盈距离。
    • 可选的移动止损,仅在浮盈超过“移动距离 + 步长”后上调(或下调)止损。
    • 当未实现收益达到设定的金额或点数时立即平仓。
    • 为贴近 MQL 版本,止损/止盈通过检查蜡烛最高价与最低价进行模拟。

参数说明

  • Order Volume:每次开仓的基础手数。
  • Stop/Take Offsets:止损与止盈的绝对价格距离;若要还原原始 EA 的“点”设置,需要根据交易品种的最小价格变动来调整。
  • Trailing Stop & Step:移动止损的距离与最小步长。
  • Min Profit (Currency / Points):按金额或价格差触发的提前平仓阈值。
  • Use Fractals / Use Stochastic:分别启用分形或随机指标信号。
  • One Trade Per Day:限制每天只开一次新仓。
  • Stochastic Settings:%K、Slowing 与 %D 的长度。
  • Candle Type:用于计算信号的蜡烛时间框架。

备注

  • 盈亏判断采用“价格变动 × 仓位”近似原策略中的佣金和掉期调整。
  • 移动止损的触发条件与 MQL 脚本一致,需要超过设定距离再加步长才会移动。
  • 在外汇品种中,可将止损/止盈距离设为所需点数乘以 Point(例如五位报价的 50 点 ≈ 0.005)。
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 "cloud's trade 2" MQL5 strategy that combines stochastic reversals with fractal confirmations.
/// </summary>
public class CloudsTrade2Strategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _stopLossOffset;
	private readonly StrategyParam<decimal> _takeProfitOffset;
	private readonly StrategyParam<decimal> _trailingStopOffset;
	private readonly StrategyParam<decimal> _trailingStepOffset;
	private readonly StrategyParam<decimal> _minProfitCurrency;
	private readonly StrategyParam<decimal> _minProfitPoints;
	private readonly StrategyParam<bool> _useFractals;
	private readonly StrategyParam<bool> _useStochastic;
	private readonly StrategyParam<bool> _oneTradePerDay;
	private readonly StrategyParam<int> _kPeriod;
	private readonly StrategyParam<int> _dPeriod;
	private readonly StrategyParam<int> _slowingPeriod;
	private readonly StrategyParam<DataType> _candleType;

	private StochasticOscillator _stochastic;

	private decimal _priorK;
	private decimal _priorD;
	private decimal _lastK;
	private decimal _lastD;
	private bool _hasPriorStoch;
	private bool _hasLastStoch;

	private decimal _h1;
	private decimal _h2;
	private decimal _h3;
	private decimal _h4;
	private decimal _h5;
	private decimal _l1;
	private decimal _l2;
	private decimal _l3;
	private decimal _l4;
	private decimal _l5;
	private FractalTypes? _latestFractal;
	private FractalTypes? _previousFractal;
	private int _fractalBufferCount;

	private decimal? _stopPrice;
	private decimal? _takeProfitPrice;
	private decimal _entryPrice;
	private DateTime? _lastEntryDate;

	private enum FractalTypes
	{
		Up,
		Down
	}

	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	public decimal StopLossOffset
	{
		get => _stopLossOffset.Value;
		set => _stopLossOffset.Value = value;
	}

	public decimal TakeProfitOffset
	{
		get => _takeProfitOffset.Value;
		set => _takeProfitOffset.Value = value;
	}

	public decimal TrailingStopOffset
	{
		get => _trailingStopOffset.Value;
		set => _trailingStopOffset.Value = value;
	}

	public decimal TrailingStepOffset
	{
		get => _trailingStepOffset.Value;
		set => _trailingStepOffset.Value = value;
	}

	public decimal MinProfitCurrency
	{
		get => _minProfitCurrency.Value;
		set => _minProfitCurrency.Value = value;
	}

	public decimal MinProfitPoints
	{
		get => _minProfitPoints.Value;
		set => _minProfitPoints.Value = value;
	}

	public bool UseFractals
	{
		get => _useFractals.Value;
		set => _useFractals.Value = value;
	}

	public bool UseStochastic
	{
		get => _useStochastic.Value;
		set => _useStochastic.Value = value;
	}

	public bool OneTradePerDay
	{
		get => _oneTradePerDay.Value;
		set => _oneTradePerDay.Value = value;
	}

	public int KPeriod
	{
		get => _kPeriod.Value;
		set => _kPeriod.Value = value;
	}

	public int DPeriod
	{
		get => _dPeriod.Value;
		set => _dPeriod.Value = value;
	}

	public int SlowingPeriod
	{
		get => _slowingPeriod.Value;
		set => _slowingPeriod.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public CloudsTrade2Strategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Default order volume", "General");

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

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

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

		_trailingStepOffset = Param(nameof(TrailingStepOffset), 0.0005m)
		.SetDisplay("Trailing Step", "Minimum price improvement for trailing", "Risk");

		_minProfitCurrency = Param(nameof(MinProfitCurrency), 10m)
		.SetDisplay("Min Profit (Currency)", "Close position when unrealized profit reaches this amount", "Exit");

		_minProfitPoints = Param(nameof(MinProfitPoints), 0.001m)
		.SetDisplay("Min Profit (Points)", "Close position after this favorable price move", "Exit");

		_useFractals = Param(nameof(UseFractals), true)
		.SetDisplay("Use Fractals", "Enable fractal based signals", "Signals");

		_useStochastic = Param(nameof(UseStochastic), false)
		.SetDisplay("Use Stochastic", "Enable stochastic based signals", "Signals");

		_oneTradePerDay = Param(nameof(OneTradePerDay), true)
		.SetDisplay("One Trade Per Day", "Allow only one entry per trading day", "Risk");

		_kPeriod = Param(nameof(KPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("%K Period", "Lookback for stochastic calculation", "Stochastic");

		_dPeriod = Param(nameof(DPeriod), 3)
		.SetGreaterThanZero()
		.SetDisplay("%D Period", "Smoothing length for %D line", "Stochastic");

		_slowingPeriod = Param(nameof(SlowingPeriod), 3)
		.SetGreaterThanZero()
		.SetDisplay("Slowing", "Smoothing length for %K line", "Stochastic");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Timeframe for signal evaluation", "General");
	}

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

	protected override void OnReseted()
	{
		base.OnReseted();

		_priorK = 0m;
		_priorD = 0m;
		_lastK = 0m;
		_lastD = 0m;
		_hasPriorStoch = false;
		_hasLastStoch = false;

		_h1 = _h2 = _h3 = _h4 = _h5 = 0m;
		_l1 = _l2 = _l3 = _l4 = _l5 = 0m;
		_latestFractal = null;
		_previousFractal = null;
		_fractalBufferCount = 0;

		_stopPrice = null;
		_takeProfitPrice = null;
		_entryPrice = 0m;
		_lastEntryDate = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		Volume = OrderVolume;

		var subscription = SubscribeCandles(CandleType);
		if (UseStochastic)
		{
			_stochastic = new StochasticOscillator
			{
				K = { Length = KPeriod },
				D = { Length = DPeriod }
			};

			subscription
				.BindEx(_stochastic, ProcessCandle)
				.Start();
		}
		else
		{
			subscription
				.Bind(ProcessCandleWithoutStochastic)
				.Start();
		}

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

	private void ProcessCandleWithoutStochastic(ICandleMessage candle)
		=> ProcessCandle(candle, null);

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

		Volume = OrderVolume;

		// Evaluate indicator signals for the finished candle
		var stochSignal = EvaluateStochasticSignal(stochValue);
		UpdateFractals(candle);
		var fractalSignal = GetFractalSignal();

		HandleOpenPosition(candle);

		var signal = 0;
		if (stochSignal == 2 || fractalSignal == 2)
		signal = 2;
		else if (stochSignal == 1 || fractalSignal == 1)
		signal = 1;

		if (signal == 0)
		return;

		if (Position != 0)
		return;

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

		if (signal == 1)
		{
			BuyMarket(OrderVolume);
			InitializeTargets(candle.ClosePrice, true);
			_lastEntryDate = candle.OpenTime.Date;
		}
		else if (signal == 2)
		{
			SellMarket(OrderVolume);
			InitializeTargets(candle.ClosePrice, false);
			_lastEntryDate = candle.OpenTime.Date;
		}
	}

	private int EvaluateStochasticSignal(IIndicatorValue stochValue)
	{
		if (!UseStochastic || stochValue is not StochasticOscillatorValue typed)
		return 0;

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

		// Seed the buffers with the first finalized stochastic values
		if (!_hasLastStoch)
		{
			_lastK = currentK;
			_lastD = currentD;
			_hasLastStoch = true;
			return 0;
		}

		if (!_hasPriorStoch)
		{
			_priorK = _lastK;
			_priorD = _lastD;
			_lastK = currentK;
			_lastD = currentD;
			_hasPriorStoch = true;
			return 0;
		}

		var sellSignal = _lastD >= 80m && _priorD <= _priorK && _lastD >= _lastK;
		var buySignal = _lastD <= 20m && _priorD >= _priorK && _lastD <= _lastK;

		_priorK = _lastK;
		_priorD = _lastD;
		_lastK = currentK;
		_lastD = currentD;

		if (sellSignal)
		return 2;

		if (buySignal)
		return 1;

		return 0;
	}

	private void UpdateFractals(ICandleMessage candle)
	{
		// Shift the rolling window so that index 3 represents the potential fractal point
		_h1 = _h2;
		_h2 = _h3;
		_h3 = _h4;
		_h4 = _h5;
		_h5 = candle.HighPrice;

		_l1 = _l2;
		_l2 = _l3;
		_l3 = _l4;
		_l4 = _l5;
		_l5 = candle.LowPrice;

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

		var upFractal = _h3 > _h1 && _h3 > _h2 && _h3 > _h4 && _h3 > _h5;
		var downFractal = _l3 < _l1 && _l3 < _l2 && _l3 < _l4 && _l3 < _l5;

		if (upFractal)
		RegisterFractal(FractalTypes.Up);

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

	private int GetFractalSignal()
	{
		if (!UseFractals)
		return 0;

		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 RegisterFractal(FractalTypes type)
	{
		_previousFractal = _latestFractal;
		_latestFractal = type;
	}

	private void HandleOpenPosition(ICandleMessage candle)
	{
		if (Position == 0)
		return;

		if (Position > 0)
		{
			// Manage protective logic for long positions
			UpdateTrailing(candle, true);

			if (_stopPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice is decimal take && candle.HighPrice >= take)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			var profit = (candle.ClosePrice - _entryPrice) * Position;
			var priceGain = candle.ClosePrice - _entryPrice;

			if (MinProfitCurrency > 0m && profit >= MinProfitCurrency)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (MinProfitPoints > 0m && priceGain >= MinProfitPoints)
			{
				SellMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
		else if (Position < 0)
		{
			// Manage protective logic for short positions
			UpdateTrailing(candle, false);

			if (_stopPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (_takeProfitPrice is decimal take && candle.LowPrice <= take)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			var profit = (_entryPrice - candle.ClosePrice) * Math.Abs(Position);
			var priceGain = _entryPrice - candle.ClosePrice;

			if (MinProfitCurrency > 0m && profit >= MinProfitCurrency)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
				return;
			}

			if (MinProfitPoints > 0m && priceGain >= MinProfitPoints)
			{
				BuyMarket(Math.Abs(Position));
				ResetTradeState();
			}
		}
	}

	private void UpdateTrailing(ICandleMessage candle, bool isLong)
	{
		// Follow the original trailing stop rules using configurable offsets
		if (TrailingStopOffset <= 0m)
		return;

		if (isLong)
		{
			var profitDistance = candle.ClosePrice - _entryPrice;
			if (profitDistance > TrailingStopOffset + TrailingStepOffset)
			{
				var newStop = candle.ClosePrice - TrailingStopOffset;
				if (_stopPrice is not decimal currentStop || newStop > currentStop + TrailingStepOffset)
				_stopPrice = newStop;
			}
		}
		else
		{
			var profitDistance = _entryPrice - candle.ClosePrice;
			if (profitDistance > TrailingStopOffset + TrailingStepOffset)
			{
				var newStop = candle.ClosePrice + TrailingStopOffset;
				if (_stopPrice is not decimal currentStop || newStop < currentStop - TrailingStepOffset)
				_stopPrice = newStop;
			}
		}
	}

	private void InitializeTargets(decimal entryPrice, bool isLong)
	{
		// Store the latest entry price and prepare static protective levels
		_entryPrice = entryPrice;

		if (isLong)
		{
			_stopPrice = StopLossOffset > 0m ? entryPrice - StopLossOffset : null;
			_takeProfitPrice = TakeProfitOffset > 0m ? entryPrice + TakeProfitOffset : null;
		}
		else
		{
			_stopPrice = StopLossOffset > 0m ? entryPrice + StopLossOffset : null;
			_takeProfitPrice = TakeProfitOffset > 0m ? entryPrice - TakeProfitOffset : null;
		}
	}

	private void ResetTradeState()
	{
		_stopPrice = null;
		_takeProfitPrice = null;
		_entryPrice = 0m;
	}
}