GitHub で見る

Currencyprofits 高値-安値チャネル戦略

概要

この戦略は、MetaTraderのエキスパートアドバイザー Currencyprofits_01.1 のStockSharpポートです。高速/低速の移動平均トレンドフィルターと最近のチャネル極値のブレイクアウトを組み合わせています。高速移動平均が低速移動平均を上回っている場合、戦略は強気環境を予想し、前のチャネルウィンドウの最安値に価格が再テストするのを待ちます。ショートトレードは、高速平均が低速平均を下回り、価格がチャネルの最高値を再テストするときに取られます。

実装はローソク足データを提供するあらゆる銘柄で機能します。すべての計算はバックテストとライブ取引の両方で安定性を確保するために、クローズしたローソク足で実行されます。

トレードロジック

  1. 設定されたローソク足タイプを購読し、前の ChannelLength ローソク足(デフォルト:6バー)に基づいて2つの移動平均とDonchianスタイルのチャネルを計算します。
  2. 1バーシフトを使用するオリジナルのMQLロジックを模倣するために、インジケーターからの前のローソク足値を保存します。
  3. ロングエントリー: 前の高速MAが前の低速MAより大きく、現在のローソク足の安値が前のチャネルの安値に触れるまたは突破する場合。
  4. ショートエントリー: 前の高速MAが前の低速MAより小さく、現在のローソク足の高値が前のチャネルの高値に触れるまたは突破する場合。
  5. エグジットルール:
    • 次のローソク足が保存されたチャネルの高値を超えて終値を付けた場合、または保護ストップが到達した場合にロングポジションをクローズします。
    • 次のローソク足が保存されたチャネルの安値を下回って終値を付けた場合、またはストップロスが発動した場合にショートポジションをクローズします。
  6. 一度に一つのポジションのみがアクティブです;トレードが開いている間、戦略は新しいシグナルを無視します。

ポジションサイジング

  • RiskPercent はトレードごとにリスクを取れるポートフォリオ価値の割合を定義します(デフォルト 0.14、すなわち14%)。
  • ストップロス距離は StopLossPoints に証券の PriceStep を掛けて導出されます(メタデータが利用できない場合はポイントを使用)。
  • 1契約あたりの現金リスクは取引所ステップ値(StepPrice)で推定されます。証券がこの情報を公開していない場合、生の価格距離が使用されます。
  • 最終的な注文数量は銘柄の取引制約(VolumeStepMinVolumeMaxVolume)に合わせて調整されます。リスクベースのサイジングが計算できない場合、戦略の基本 Volume が使用されます。

パラメーター

  • FastLength – トレンドを検出するために使用する高速移動平均の長さ(デフォルト32)。
  • FastMaType – 高速移動平均のタイプ(Simple、Exponential、Smoothed、Weighted)。
  • SlowLength – 低速移動平均の長さ(デフォルト86)。
  • SlowMaType – 低速移動平均のタイプ。
  • PriceSource – 両方の移動平均に適用するローソク足価格(デフォルト:終値)。
  • ChannelLength – 高値/安値チャネルを形成する前のローソク足の数(デフォルト6)。
  • StopLossPoints – 価格に変換される前の銘柄ポイントで表したストップ距離(デフォルト170)。
  • RiskPercent – トレードごとにリスクを取る資本の割合(デフォルト0.14 → 14%)。
  • CandleType – すべての計算に使用するローソク足の時間軸(デフォルト1時間、希望するチャート期間に合わせて変更可能)。

使用上の注意

  • 正確なポジションサイジングのために Security.PriceStepSecurity.StepPrice、および出来高メタデータが入力されていることを確認します。
  • リスクベースのサイジングが無効の場合(例:RiskPercent = 0)、戦略の Volume に合理的なフォールバック値を設定します。
  • ロジックはクローズしたローソク足で取引します;ライブ実行はシグナルを確認するバークローズ時に発生します。
  • ストップロスは内部で管理されます;ソースのエキスパートアドバイザーを反映して、別のテイクプロフィットはありません。

ソース

可読性とStockSharpの高レベルAPIとの互換性を重視して MQL/17641/Currencyprofits_01.1.mq5 から変換されました。

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>
/// Currencyprofits strategy that trades trend pullbacks into the recent channel extremes.
/// </summary>
public class CurrencyprofitsHighLowChannelStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<int> _channelLength;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _riskPercent;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<CandlePrices> _priceSource;
	private readonly StrategyParam<MovingAverageTypes> _fastMaType;
	private readonly StrategyParam<MovingAverageTypes> _slowMaType;
	private readonly StrategyParam<int> _signalCooldownBars;

	private decimal? _previousFast;
	private decimal? _previousSlow;
	private decimal? _previousHighest;
	private decimal? _previousLowest;
	private decimal _entryPrice;
	private decimal _stopPrice;
	private int _processedCandles;
	private int _cooldownRemaining;

	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

	public int ChannelLength
	{
		get => _channelLength.Value;
		set => _channelLength.Value = value;
	}

	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	public decimal RiskPercent
	{
		get => _riskPercent.Value;
		set => _riskPercent.Value = value;
	}

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

	public CandlePrices PriceSource
	{
		get => _priceSource.Value;
		set => _priceSource.Value = value;
	}

	public MovingAverageTypes FastMaType
	{
		get => _fastMaType.Value;
		set => _fastMaType.Value = value;
	}

	public MovingAverageTypes SlowMaType
	{
		get => _slowMaType.Value;
		set => _slowMaType.Value = value;
	}

	public int SignalCooldownBars
	{
		get => _signalCooldownBars.Value;
		set => _signalCooldownBars.Value = value;
	}

	private int RequiredBars => Math.Max(Math.Max(FastLength, SlowLength), ChannelLength) + 1;

	public CurrencyprofitsHighLowChannelStrategy()
	{
		_fastLength = Param(nameof(FastLength), 32)
			.SetDisplay("Fast MA Length", "Length of the fast moving average", "Indicators")
			
			.SetOptimize(10, 120, 2);

		_slowLength = Param(nameof(SlowLength), 86)
			.SetDisplay("Slow MA Length", "Length of the slow moving average", "Indicators")
			
			.SetOptimize(20, 200, 2);

		_channelLength = Param(nameof(ChannelLength), 12)
			.SetDisplay("Channel Lookback", "Number of previous candles for high/low channel", "Indicators")
			
			.SetOptimize(3, 20, 1);

		_stopLossPoints = Param(nameof(StopLossPoints), 170m)
			.SetDisplay("Stop Loss (points)", "Distance to stop loss expressed in price steps", "Risk");

		_riskPercent = Param(nameof(RiskPercent), 0.14m)
			.SetDisplay("Risk Fraction", "Fraction of portfolio capital risked per trade", "Risk");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Primary timeframe for calculations", "General");

		_priceSource = Param(nameof(PriceSource), CandlePrices.Close)
			.SetDisplay("MA Price Source", "Price source used by both moving averages", "Indicators");

		_fastMaType = Param(nameof(FastMaType), MovingAverageTypes.Simple)
			.SetDisplay("Fast MA Type", "Moving average algorithm for the fast line", "Indicators");

		_slowMaType = Param(nameof(SlowMaType), MovingAverageTypes.Simple)
			.SetDisplay("Slow MA Type", "Moving average algorithm for the slow line", "Indicators");

		_signalCooldownBars = Param(nameof(SignalCooldownBars), 4)
			.SetNotNegative()
			.SetDisplay("Signal Cooldown Bars", "Closed candles to wait before the next entry", "Risk");
	}

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

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

		_previousFast = null;
		_previousSlow = null;
		_previousHighest = null;
		_previousLowest = null;
		_entryPrice = 0m;
		_stopPrice = 0m;
		_processedCandles = 0;
		_cooldownRemaining = 0;
	}

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

		var fastMa = CreateMovingAverage(FastMaType, FastLength, PriceSource);
		var slowMa = CreateMovingAverage(SlowMaType, SlowLength, PriceSource);
		var highest = new Highest { Length = ChannelLength };
		var lowest = new Lowest { Length = ChannelLength };

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

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

	private void ProcessCandle(ICandleMessage candle, decimal fast, decimal slow, decimal channelHigh, decimal channelLow)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_cooldownRemaining > 0)
			_cooldownRemaining--;

		_processedCandles++;

		if (_processedCandles <= RequiredBars)
		{
			// Collect enough history before taking any decisions.
			_previousFast = fast;
			_previousSlow = slow;
			_previousHighest = channelHigh;
			_previousLowest = channelLow;
			return;
		}

		if (_previousFast is null || _previousSlow is null || _previousHighest is null || _previousLowest is null)
		{
			_previousFast = fast;
			_previousSlow = slow;
			_previousHighest = channelHigh;
			_previousLowest = channelLow;
			return;
		}

		if (Position > 0)
		{
			// Exit long trades when price breaks the opposite channel or the protective stop.
			var exitByChannel = candle.ClosePrice >= _previousHighest.Value;
			var exitByStop = _stopPrice > 0m && candle.LowPrice <= _stopPrice;

			if (exitByChannel || exitByStop)
			{
				SellMarket(Position);
				ResetTradeState();
				_cooldownRemaining = SignalCooldownBars;
			}
		}
		else if (Position < 0)
		{
			// Exit short trades when price hits the lower boundary or the stop.
			var exitByChannel = candle.ClosePrice <= _previousLowest.Value;
			var exitByStop = _stopPrice > 0m && candle.HighPrice >= _stopPrice;

			if (exitByChannel || exitByStop)
			{
				BuyMarket(-Position);
				ResetTradeState();
				_cooldownRemaining = SignalCooldownBars;
			}
		}
		else if (_cooldownRemaining == 0)
		{
			var stopDistance = GetStopDistance();

			if (stopDistance > 0m)
			{
				var bullishTrend = _previousFast.Value > _previousSlow.Value && fast > slow;
				var bearishTrend = _previousFast.Value < _previousSlow.Value && fast < slow;
				var bullishReversal = candle.LowPrice <= _previousLowest.Value && candle.ClosePrice > candle.OpenPrice && candle.ClosePrice > fast;
				var bearishReversal = candle.HighPrice >= _previousHighest.Value && candle.ClosePrice < candle.OpenPrice && candle.ClosePrice < fast;

				// Long entries require a bullish trend and a pullback to the recent low channel.
				if (bullishTrend && bullishReversal)
				{
					var volume = CalculatePositionSize(stopDistance);

					if (volume > 0m)
					{
						BuyMarket(volume);
						_entryPrice = candle.ClosePrice;
						_stopPrice = _entryPrice - stopDistance;
						_cooldownRemaining = SignalCooldownBars;
					}
				}
				// Short entries require a bearish trend and a retest of the recent high channel.
				else if (bearishTrend && bearishReversal)
				{
					var volume = CalculatePositionSize(stopDistance);

					if (volume > 0m)
					{
						SellMarket(volume);
						_entryPrice = candle.ClosePrice;
						_stopPrice = _entryPrice + stopDistance;
						_cooldownRemaining = SignalCooldownBars;
					}
				}
			}
		}

		_previousFast = fast;
		_previousSlow = slow;
		_previousHighest = channelHigh;
		_previousLowest = channelLow;
	}

	private decimal GetStopDistance()
	{
		if (StopLossPoints <= 0m)
			return 0m;

		var priceStep = Security?.PriceStep ?? 0m;

		if (priceStep > 0m)
			return StopLossPoints * priceStep;

		return StopLossPoints;
	}

	private decimal CalculatePositionSize(decimal stopDistance)
	{
		var defaultVolume = AdjustVolume(Volume);

		if (stopDistance <= 0m)
			return defaultVolume;

		var portfolioValue = Portfolio?.CurrentValue;

		if (portfolioValue is null || portfolioValue <= 0m || RiskPercent <= 0m)
			return defaultVolume;

		var riskCapital = portfolioValue.Value * RiskPercent;
		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;

		decimal riskPerContract;

		if (priceStep > 0m && stepPrice > 0m)
		{
			// Convert the stop distance into cash risk per contract using exchange specifications.
			riskPerContract = stopDistance / priceStep * stepPrice;
		}
		else
		{
			// Fallback when the security does not expose step metadata.
			riskPerContract = stopDistance;
		}

		if (riskPerContract <= 0m)
			return defaultVolume;

		var desiredVolume = riskCapital / riskPerContract;
		return AdjustVolume(desiredVolume);
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep ?? 0m;

		if (step > 0m)
		{
			var steps = decimal.Floor(volume / step);
			volume = steps * step;
		}

		var minVolume = Security?.MinVolume ?? 0m;
		if (minVolume > 0m && volume < minVolume)
			volume = minVolume;

		var maxVolume = Security?.MaxVolume ?? 0m;
		if (maxVolume > 0m && volume > maxVolume)
			volume = maxVolume;

		return volume;
	}

	private void ResetTradeState()
	{
		// Clear cached execution details after a position has been closed.
		_entryPrice = 0m;
		_stopPrice = 0m;
	}

	private static DecimalLengthIndicator CreateMovingAverage(MovingAverageTypes type, int length, CandlePrices price)
	{
		return type switch
		{
			MovingAverageTypes.Simple => new SMA { Length = length },
			MovingAverageTypes.Exponential => new EMA { Length = length },
			MovingAverageTypes.Smoothed => new SmoothedMovingAverage { Length = length },
			MovingAverageTypes.Weighted => new WeightedMovingAverage { Length = length },
			_ => new SMA { Length = length },
		};
	}

	public enum MovingAverageTypes
	{
		Simple,
		Exponential,
		Smoothed,
		Weighted,
	}

	public enum CandlePrices
	{
		Open,
		High,
		Low,
		Close,
		Median,
		Typical,
		Weighted
	}
}