GitHub で見る

修正された平均チャネル戦略

概要

修正された平均チャネル戦略は、MetaTrader エキスパート アドバイザー e-CA-5 の C# ポートです。システムは、設定可能なシグマ オフセットによって価格が修正移動平均を横切るときにローソク足がクローズおよびポジションをオープンするたびに、「修正平均」(CA) インジケーターを再構築します。変換された実装は、StockSharp のハイレベル ローソク足 API に依存し、成行注文を使用し、保護的出口 (ストップロス、テイクプロフィット、トレーリングストップ) を内部的に管理して、元の Expert Advisor の動作を反映します。

平均インジケーターを修正

CA フィルターは、移動平均とボラティリティ フィードバックを組み合わせます。 MQL バージョンは、移動平均の長さ、平均化方法、および適用価格の 3 つの入力を公開します。 StockSharp ポート:

  1. 移動平均のタイプは、MaTypeOption パラメータ (SMA、EMA、SMMA、LWMA) と MaPeriod の長さによって選択されます。
  2. 同じ期間の StandardDeviation インジケーターは、現在のボラティリティを測定します。
  3. 完成したローソクごとに、修正値が繰り返し計算されます。
    • M_t を最新の足の MA 値、CA_{t-1} を前の足の修正値とします。
    • v1 = StdDev_t^2v2 = (CA_{t-1} - M_t)^2 を計算します。
    • v2 <= 0 または v2 < v1 の場合は、補正係数 k = 0 を維持します。それ以外の場合は、k = 1 - v1 / v2 を設定します。
    • CA_t = CA_{t-1} + k * (M_t - CA_{t-1})を更新します。
    • 一番最初の修正値はデフォルトで移動平均自体になります。

このフィードバック ループにより、閑静な期間中の MA が抑制され、価格が現在のボラティリティ推定値を超えて乖離した場合に迅速な調整が可能になります。

取引ロジック

  1. この戦略は、構成されたローソク足タイプ (CandleType) をサブスクライブし、移動平均と標準偏差の両方が完全に形成されるまで待機します。
  2. Once a candle finishes, the algorithm calculates the new corrected value and compares the previous candle close against the previous corrected level.
  3. 2 つのシグマ オフセット、SigmaBuyPointsSigmaSellPoints は、商品の PriceStep を使用して価格距離に変換されます。
  4. エントリールールは、前のローソク足の終値と新たに計算された修正レベルを使用します。
    • 前回の終値が修正平均に買いシグマを加えた値を下回っており、現在の終値がその上限を上回って終了した場合は、買いとなります。
    • 前回の終値が修正平均から売りシグマを引いた値を上回り、現在の終値がその下限を下回って終了した場合は、売り
  5. ネットポジションは 1 つだけ許可されます。新しい取引は、エクスポージャーが存在しない場合にのみ送信されます。

StockSharp バージョンは完成したローソク足で動作するため、ブレイクアウトの確認はティックごとではなくバーごとに 1 回発生し、バックテストやローソク足データによるライブ オートメーションに適した決定的な動作を提供します。

リスク管理

この移植では、元の Expert Advisor の 3 つの保護メカニズムがすべて再現されます。

  • 固定ストップロス: StopLossPoints に価格ステップを乗じた値が、エントリー価格と保護ストップの間の距離を定義します。トリガーされたストップは、成行注文でポジション全体をクローズします。
  • 固定テイクプロフィット: TakeProfitPoints は利益目標距離に変換されます。価格がローソク足のレベルに達すると、ポジションは成行注文でクローズされます。
  • トレーリング ストップ: TrailingPoints がゼロより大きい場合、この戦略は含み益を追跡し、価格が少なくともその距離だけ進んだ時点で、最新の終値の後ろにトレーリング レベルを保存します。トレーリングストップは前方にのみ移動し、新しいトレーリングレベルが受け入れられる前の最小限の改善を表す TrailingStepPoints を尊重します。後続レベルは、商品のティック サイズに合わせて Security.ShrinkPrice で四捨五入されます。

すべての出口は内部リスク状態をリセットします。次のシグナルが表示されると、ストップ、ターゲット、トレーリングレベルが新しい約定価格から再計算され、元の注文保護を変更する MQL バージョンに近い動作が保証されます。

パラメーター

パラメータ 説明
OrderVolume 市場エントリーに使用される数量。ポジティブである必要があります。
TakeProfitPoints 価格ステップでの利益目標 (0 は利食いを無効にします)。
StopLossPoints 価格ステップのストップロス距離 (0 はストップロスを無効にします)。
TrailingPoints トレーリングストップが有効になるまでに必要な利益距離 (価格ステップ)。
TrailingStepPoints トレーリングストップを再度移動する前に取得する必要がある追加の最小距離。
MaPeriod 移動平均と標準偏差の両方の周期。
MaTypeOption 移動平均タイプ: SMA、EMA、SMMA、または LWMA。
SigmaBuyPoints ロングポジションをオープンする前に、修正平均の上に追加されるシグマオフセット。
SigmaSellPoints ショートポジションをオープンする前に、修正平均を下回るように減算されるシグマオフセット。
CandleType インジケーターの計算とシグナル評価に使用されるキャンドル シリーズ。

すべての数値パラメーターは SetCanOptimize(true) による最適化をサポートしているため、戦略を StockSharp 環境内で直接調整できます。

使用上の注意

  • デフォルトのキャンドル タイプは 1 時間です。元の MetaTrader 戦略を最適化するときに使用した時間枠と一致するように調整します。
  • Security.PriceStep は、すべての「ポイント」入力を実際の価格距離に変換するために使用されます。ステップが構成されていないインストゥルメントは 1 にフォールバックし、インデックスまたは暗号通貨の適切な動作が維持されます。
  • この戦略は完成したキャンドルに対してのみ実行されます。バー内の精度が必要な場合は、タイムフレームを希望の粒度まで下げます。
  • トレーリング ストップは、違反した場合に成行注文とともに実装され、ストップロス価格を変更したオリジナルの EA を模倣します。このアプローチにより、追加の逆指値注文の発行が回避され、リスク管理が戦略自体に含まれます。
  • タスク要件に従って、この変換用の Python バージョンは提供されません。

オリジナルの EA との違い

  • StockSharp のローソク足ベースの API がティックレベルの処理を置き換えます。すべての決定はろうそくが閉じるときに行われます。
  • 注文管理はネット化されています。反対のポジションは同時に保持されず、MetaTrader バージョンの単一注文ロジックと一致します。
  • プロテクティブストップとトレーリングエグジットは、既存の注文チケットを変更するのではなく、成行注文を通じて実行されます。この動作は、他の StockSharp 戦略との実装の一貫性を保ちながら、ネッティング アカウントでも同様です。

これらの適応により、e-CA-5 の取引アイデアが維持され、ロジックが StockSharp のベスト プラクティスとリポジトリ ガイドラインに記載されている高レベルの 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 MetaTrader expert e-CA-5 that trades breakouts around the Corrected Average indicator.
/// The strategy subscribes to candles, rebuilds the indicator and places market orders when price crosses
/// the corrected moving average by the configured sigma offsets.
/// </summary>
public class CorrectedAverageChannelStrategy : Strategy
{
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _trailingPoints;
	private readonly StrategyParam<int> _trailingStepPoints;
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<MaTypes> _maType;
	private readonly StrategyParam<int> _sigmaBuyPoints;
	private readonly StrategyParam<int> _sigmaSellPoints;
	private readonly StrategyParam<DataType> _candleType;

	private DecimalLengthIndicator _ma;
	private StandardDeviation _std;

	private decimal _priceStep;
	private decimal _sigmaBuyOffset;
	private decimal _sigmaSellOffset;
	private decimal _stopLossDistance;
	private decimal _takeProfitDistance;
	private decimal _trailingDistance;
	private decimal _trailingStepDistance;

	private decimal? _previousCorrected;
	private decimal? _previousClose;

	private decimal? _entryPrice;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal? _longTrailingStop;
	private decimal? _shortTrailingStop;
	private decimal _previousPosition;
	private decimal? _lastTradePrice;
	private Sides? _lastTradeSide;

	/// <summary>
	/// Order size used for market entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

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

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

	/// <summary>
	/// Trailing stop trigger expressed in price steps.
	/// </summary>
	public int TrailingPoints
	{
		get => _trailingPoints.Value;
		set => _trailingPoints.Value = value;
	}

	/// <summary>
	/// Minimum increment required to advance the trailing stop in price steps.
	/// </summary>
	public int TrailingStepPoints
	{
		get => _trailingStepPoints.Value;
		set => _trailingStepPoints.Value = value;
	}

	/// <summary>
	/// Moving average period used by the Corrected Average filter.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Moving average type replicated from the MetaTrader input.
	/// </summary>
	public MaTypes MaTypesOption
	{
		get => _maType.Value;
		set => _maType.Value = value;
	}

	/// <summary>
	/// Buy-side sigma expressed in price steps.
	/// </summary>
	public int SigmaBuyPoints
	{
		get => _sigmaBuyPoints.Value;
		set => _sigmaBuyPoints.Value = value;
	}

	/// <summary>
	/// Sell-side sigma expressed in price steps.
	/// </summary>
	public int SigmaSellPoints
	{
		get => _sigmaSellPoints.Value;
		set => _sigmaSellPoints.Value = value;
	}

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

	/// <summary>
	/// Initializes a new instance of the <see cref="CorrectedAverageChannelStrategy"/> class.
	/// </summary>
	public CorrectedAverageChannelStrategy()
	{
		_orderVolume = Param(nameof(OrderVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Order Volume", "Market order size used for entries", "Trading")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 60)
			.SetNotNegative()
			.SetDisplay("Take Profit (points)", "Distance from entry to the profit target in price steps", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 40)
			.SetNotNegative()
			.SetDisplay("Stop Loss (points)", "Distance from entry to the protective stop in price steps", "Risk")
			;

		_trailingPoints = Param(nameof(TrailingPoints), 0)
			.SetNotNegative()
			.SetDisplay("Trailing Trigger (points)", "Profit distance required before the trailing stop activates", "Risk")
			;

		_trailingStepPoints = Param(nameof(TrailingStepPoints), 0)
			.SetNotNegative()
			.SetDisplay("Trailing Step (points)", "Minimum advance in price steps before the trailing stop moves", "Risk")
			;

		_maPeriod = Param(nameof(MaPeriod), 35)
			.SetRange(2, 500)
			.SetDisplay("MA Period", "Period of the moving average and standard deviation", "Indicator")
			;

		_maType = Param(nameof(MaTypesOption), MaTypes.Sma)
			.SetDisplay("MA Type", "Moving average type used inside the Corrected Average", "Indicator");

		_sigmaBuyPoints = Param(nameof(SigmaBuyPoints), 5)
			.SetNotNegative()
			.SetDisplay("Sigma BUY (points)", "Offset added above the corrected average before buying", "Signal")
			;

		_sigmaSellPoints = Param(nameof(SigmaSellPoints), 5)
			.SetNotNegative()
			.SetDisplay("Sigma SELL (points)", "Offset subtracted from the corrected average before selling", "Signal")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for calculations", "Data");
	}

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

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

		_ma = null;
		_std = null;
		_priceStep = 0m;
		_sigmaBuyOffset = 0m;
		_sigmaSellOffset = 0m;
		_stopLossDistance = 0m;
		_takeProfitDistance = 0m;
		_trailingDistance = 0m;
		_trailingStepDistance = 0m;
		_previousCorrected = null;
		_previousClose = null;
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
		_previousPosition = 0m;
		_lastTradePrice = null;
		_lastTradeSide = null;
	}

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

		_ma = CreateMa(MaTypesOption, MaPeriod);
		_std = new StandardDeviation
		{
			Length = MaPeriod
		};

		_priceStep = Security?.PriceStep ?? 0m;
		if (_priceStep <= 0m)
		{
			_priceStep = 1m;
		}

		_sigmaBuyOffset = GetPriceOffset(SigmaBuyPoints);
		_sigmaSellOffset = GetPriceOffset(SigmaSellPoints);
		_stopLossDistance = GetPriceOffset(StopLossPoints);
		_takeProfitDistance = GetPriceOffset(TakeProfitPoints);
		_trailingDistance = GetPriceOffset(TrailingPoints);
		_trailingStepDistance = GetPriceOffset(TrailingStepPoints);

		Volume = OrderVolume;

		var subscription = SubscribeCandles(CandleType);
		subscription.Bind(_ma, _std, ProcessCandle).Start();
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		base.OnOwnTradeReceived(trade);

		if (trade.Trade != null)
		{
			_lastTradePrice = trade.Trade.Price;
		}

		_lastTradeSide = trade.Order.Side;
	}

	/// <inheritdoc />
	protected override void OnPositionReceived(Position position)
	{
		base.OnPositionReceived(position);

		if (_previousPosition == 0m && Position != 0m)
		{
			var entryPrice = _lastTradePrice ?? _previousClose;
			if (entryPrice is decimal price)
			{
				if (Position > 0m && _lastTradeSide == Sides.Buy)
				{
					InitializeRiskState(price, true);
				}
				else if (Position < 0m && _lastTradeSide == Sides.Sell)
				{
					InitializeRiskState(price, false);
				}
			}
		}
		else if (Position == 0m && _previousPosition != 0m)
		{
			ResetRiskState();
		}

		_previousPosition = Position;
	}

	private void ProcessCandle(ICandleMessage candle, decimal maValue, decimal stdValue)
	{
		if (candle.State != CandleStates.Finished)
			return;

		if (_ma is null || _std is null)
			return;

		if (!_ma.IsFormed || !_std.IsFormed)
		{
			_previousCorrected = maValue;
			_previousClose = candle.ClosePrice;
			return;
		}

		var previousCorrected = _previousCorrected;
		var previousClose = _previousClose;

		decimal corrected;

		if (previousCorrected is not decimal prevCorrected)
		{
			corrected = maValue;
		}
		else
		{
			var diff = prevCorrected - maValue;
			var v2 = diff * diff;
			var v1 = stdValue * stdValue;
			var k = (v2 <= 0m || v2 < v1) ? 0m : 1m - (v1 / v2);
			corrected = prevCorrected + k * (maValue - prevCorrected);
		}

		if (HandleTrailing(candle))
		{
			_previousCorrected = corrected;
			_previousClose = candle.ClosePrice;
			return;
		}

		if (HandleRiskExit(candle))
		{
			_previousCorrected = corrected;
			_previousClose = candle.ClosePrice;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_previousCorrected = corrected;
			_previousClose = candle.ClosePrice;
			return;
		}

		if (Position == 0m && previousCorrected is decimal prevCorr && previousClose is decimal prevCls)
		{
			var buyThreshold = corrected + _sigmaBuyOffset;
			var sellThreshold = corrected - _sigmaSellOffset;

			var buySignal = prevCls < prevCorr + _sigmaBuyOffset && candle.ClosePrice >= buyThreshold;
			var sellSignal = prevCls > prevCorr - _sigmaSellOffset && candle.ClosePrice <= sellThreshold;

			if (buySignal)
			{
				BuyMarket();
			}
			else if (sellSignal)
			{
				SellMarket();
			}
		}

		_previousCorrected = corrected;
		_previousClose = candle.ClosePrice;
	}

	private bool HandleTrailing(ICandleMessage candle)
	{
		if (_trailingDistance <= 0m || _entryPrice is null)
			return false;

		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return false;

		if (Position > 0m)
		{
			var moved = candle.ClosePrice - _entryPrice.Value;
			if (moved > _trailingDistance)
			{
				var candidate = candle.ClosePrice - _trailingDistance;
				if (_longTrailingStop is null || candidate - _longTrailingStop.Value >= _trailingStepDistance)
				{
					_longTrailingStop = Security?.ShrinkPrice(candidate) ?? candidate;
				}
			}

			if (_longTrailingStop is decimal trailing && candle.LowPrice <= trailing)
			{
				SellMarket(volume);
				ResetRiskState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			var moved = _entryPrice.Value - candle.ClosePrice;
			if (moved > _trailingDistance)
			{
				var candidate = candle.ClosePrice + _trailingDistance;
				if (_shortTrailingStop is null || _shortTrailingStop.Value - candidate >= _trailingStepDistance)
				{
					_shortTrailingStop = Security?.ShrinkPrice(candidate) ?? candidate;
				}
			}

			if (_shortTrailingStop is decimal trailing && candle.HighPrice >= trailing)
			{
				BuyMarket(volume);
				ResetRiskState();
				return true;
			}
		}

		return false;
	}

	private bool HandleRiskExit(ICandleMessage candle)
	{
		var volume = Math.Abs(Position);
		if (volume <= 0m)
			return false;

		if (Position > 0m)
		{
			if (_stopLossPrice is decimal stop && candle.LowPrice <= stop)
			{
				SellMarket(volume);
				ResetRiskState();
				return true;
			}

			if (_takeProfitPrice is decimal target && candle.HighPrice >= target)
			{
				SellMarket(volume);
				ResetRiskState();
				return true;
			}
		}
		else if (Position < 0m)
		{
			if (_stopLossPrice is decimal stop && candle.HighPrice >= stop)
			{
				BuyMarket(volume);
				ResetRiskState();
				return true;
			}

			if (_takeProfitPrice is decimal target && candle.LowPrice <= target)
			{
				BuyMarket(volume);
				ResetRiskState();
				return true;
			}
		}

		return false;
	}

	private void InitializeRiskState(decimal entryPrice, bool isLong)
	{
		_entryPrice = entryPrice;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;

		if (_stopLossDistance > 0m)
		{
			var rawPrice = isLong ? entryPrice - _stopLossDistance : entryPrice + _stopLossDistance;
			_stopLossPrice = Security?.ShrinkPrice(rawPrice) ?? rawPrice;
		}

		if (_takeProfitDistance > 0m)
		{
			var rawPrice = isLong ? entryPrice + _takeProfitDistance : entryPrice - _takeProfitDistance;
			_takeProfitPrice = Security?.ShrinkPrice(rawPrice) ?? rawPrice;
		}
	}

	private void ResetRiskState()
	{
		_entryPrice = null;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_longTrailingStop = null;
		_shortTrailingStop = null;
	}

	private decimal GetPriceOffset(int points)
	{
		if (points <= 0 || _priceStep <= 0m)
			return 0m;

		return points * _priceStep;
	}

	private static DecimalLengthIndicator CreateMa(MaTypes type, int length)
	{
		return type switch
		{
			MaTypes.Sma => new SMA { Length = length },
			MaTypes.Ema => new EMA { Length = length },
			MaTypes.Smma => new SmoothedMovingAverage { Length = length },
			MaTypes.Lwma => new WeightedMovingAverage { Length = length },
			_ => throw new ArgumentOutOfRangeException(nameof(type))
		};
	}

	/// <summary>
	/// Supported moving average types.
	/// </summary>
	public enum MaTypes
	{
		Sma,
		Ema,
		Smma,
		Lwma
	}
}