GitHub で見る

ErrorEA 戦略

概要

ErrorEA 戦略 は、MetaTrader アドバイザー errorEA.mq4 の StockSharp 移植です。元の専門家は、平均方向性指数の +DI コンポーネントと -DI コンポーネントを比較し、非常に大きな安全ストップロスとタイトなスキャルピングテイクプロフィットを適用しながら、検出されたトレンド方向に成行注文を積み重ね続けました。この C# バージョンは、StockSharp の高レベルの API と同じアイデアを再作成し、明確なパラメータ制御を追加し、リスク モデルを明示的に文書化します。

取引ロジック

  1. 設定されたタイムフレーム (CandleType) をサブスクライブし、受信ローソクを AverageDirectionalIndex インジケーターにフィードします。
  2. ローソク足が完全に閉じて、ADX がそのバーの最終値を生成するまで待ちます。
  3. +DI 行と -DI 行を比較します。
    • +DI > -DI の場合、戦略は市場を強気として扱います。
    • -DI > +DI の場合、市場は弱気とみなされます。
    • 値が等しい場合、新しい信号は生成されません。
  4. 強気シグナルの場合:
    • 既存のショート ネット ポジションをフラット化します (StockSharp はネッティング口座を使用しているため、反対側のヘッジは閉じられています)。
    • ロングスケールイン取引の数がまだ MaxTrades を下回っている場合は、リスク制御ブロックによって返された数量で成行買い注文をもう 1 つ送信します。
  5. 弱気シグナルの場合:
    • 既存のロングポジションをクローズする。
    • ショートトランシェの数が MaxTrades を下回っている場合は、同じポジションサイジングロジックを使用して成行売り注文を 1 つ送信します。
  6. 秘密保持命令は StartProtection によって管理されます:
    • StopLossPoints は価格ステップに変換され、MetaTrader の StopLoss 入力と同様に、幅広い固定ストップとして機能します。
    • EnableTakeProfit が true の場合、TakeProfitPoints は、EA が OrderModify を通じて適用した小規模なスキャルピング ターゲットを複製します。
  7. ポジション カウンタ (_longTrades/_shortTrades) は、ネット ポジションがゼロに戻るか反対側に反転するたびにリセットされ、ストップアウトとリバーサルにわたってスケールイン キャップが適用されるようにします。

リスク管理とサイジング

  • BaseVolume は、MetaTrader からの MiniLots 入力をミラーリングします。これは、あらゆる取引の開始ロットサイズとして機能します。
  • EnableRiskControl が true の場合、ストラテジーは元の PowerRisk 式を再現します: volume = BaseVolume * max(1, PortfolioValue / RiskDivider)。デフォルトの分周器 (10000) は、MQL の実装と一致します。
  • 式が適用されると、結果は MinVolumeMaxVolume、交換制限 (Security.MinVolumeSecurity.MaxVolume)、およびボリューム ステップ (Security.VolumeStep) によってクランプされます。これにより、EA が会場が拒否するようなサイズをリクエストすることがなくなります。
  • 計算されたサイズは、対応する方向が MaxTrades の上限内に留まりながら、すべての新しいスケールイン順序に使用されます。

パラメーター

名前 種類 デフォルト MetaTraderの相手方 説明
AdxPeriod int 14 iADX(..., 14, ...) 平均方向指数の平滑化期間。
CandleType DataType 15分の時間枠 チャートの時間枠 すべての計算に使用されるローソク足シリーズ。
MaxTrades int 9 MaxTrades 方向ごとのスケールイン オーダーの最大数。
EnableRiskControl bool true RiskControl ポートフォリオ値に基づいた動的なロット計算を有効にします。
BaseVolume decimal 0.15 MiniLots リスク乗数を適用する前の基本ロットサイズ。
RiskDivider decimal 10000 暗黙的 (PowerRisk の除数) リスク管理が有効な場合にポートフォリオ値に適用される除算器。
MaxVolume decimal 3 MaxLot 自動計算されたボリュームの上限 (為替四捨五入前)。
MinVolume decimal 0.01 MarketInfo(..., MODE_MINLOT) 最終注文で許可される最小数量。
StopLossPoints int 1000 StopLoss 価格ステップのストップロス距離。停止を無効にするには、0 に設定します。
EnableTakeProfit bool true ScalpeControl タイトなスキャルピングのテイクプロフィットを可能にします。
TakeProfitPoints int 10 ScalpeProfit 価格ステップでの利食い距離。

元のエキスパートアドバイザーとの違い

  • MetaTrader バージョンには、+DI 値を -DI 値で上書きするバグが含まれていました。 StockSharp ポートは、戦略の意図された動作を反映して、正しいコンポーネントを比較します。
  • MetaTrader でヘッジが可能になります。 StockSharp はネッティング環境で動作するため、ポートはシグナル方向に新しい取引を追加する前に反対側のエクスポージャーをクローズします。
  • StockSharp は注文のスリッページを内部で処理し、リスク文字列は純粋に表面的なものであるため、スリッページ検出 (GetSlippage) とコメント出力は削除されました。
  • 注文変更 (OrderModify) は単一の StartProtection 呼び出しに置き換えられ、取引所を意識した丸めでストップロスとテイクプロフィットの両方の距離をカバーします。

使い方のヒント

  • 組み込みの音量調整が正しく機能できるように、セキュリティに適切な PriceStepVolumeStepMinVolume、および MaxVolume メタデータがあることを確認してください。
  • BaseVolumeMinVolume、および MaxVolume を取引する商品に合わせます。また、コンストラクターは、調整された基本ボリュームを Strategy.Volume に割り当てます。これにより、UI での手動アクションが自動注文と一致します。
  • +DI/-DI 信号のノイズが多すぎる場合は、タイムフレームまたは ADX 周期を増やします。スケールイン ロジックは、安定した傾向のときに最高のパフォーマンスを発揮します。
  • 小さな利益をスキャルピングするのではなく、ストップロスでポジションを終了させたい場合は、EnableTakeProfit を無効にします。
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;

using StockSharp.Algo;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Port of the "errorEA" MetaTrader strategy that compares +DI and -DI lines of ADX.
/// </summary>
public class ErrorEaStrategy : Strategy
{
	private readonly StrategyParam<int> _adxPeriod;
	private readonly StrategyParam<int> _maxTrades;
	private readonly StrategyParam<bool> _enableRiskControl;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _minVolume;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _riskDivider;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<bool> _enableTakeProfit;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<DataType> _candleType;

	private AverageDirectionalIndex _adx;
	private int _longTrades;
	private int _shortTrades;

	/// <summary>
	/// ADX averaging period.
	/// </summary>
	public int AdxPeriod
	{
		get => _adxPeriod.Value;
		set => _adxPeriod.Value = value;
	}

	/// <summary>
	/// Maximum number of scale-in entries per direction.
	/// </summary>
	public int MaxTrades
	{
		get => _maxTrades.Value;
		set => _maxTrades.Value = value;
	}

	/// <summary>
	/// Enables dynamic position sizing based on the portfolio value.
	/// </summary>
	public bool EnableRiskControl
	{
		get => _enableRiskControl.Value;
		set => _enableRiskControl.Value = value;
	}

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

	/// <summary>
	/// Minimum order volume that should be used.
	/// </summary>
	public decimal MinVolume
	{
		get => _minVolume.Value;
		set => _minVolume.Value = value;
	}

	/// <summary>
	/// Base volume multiplier that matches the MiniLots parameter from MQL.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Divider applied to the portfolio value when risk control is enabled.
	/// </summary>
	public decimal RiskDivider
	{
		get => _riskDivider.Value;
		set => _riskDivider.Value = value;
	}

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

	/// <summary>
	/// Enables the scalping take-profit mode from the original EA.
	/// </summary>
	public bool EnableTakeProfit
	{
		get => _enableTakeProfit.Value;
		set => _enableTakeProfit.Value = value;
	}

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

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

	/// <summary>
	/// Initializes a new instance of the <see cref="ErrorEaStrategy"/> class.
	/// </summary>
	public ErrorEaStrategy()
	{
		_adxPeriod = Param(nameof(AdxPeriod), 14)
			.SetRange(5, 50)
			.SetDisplay("ADX Period", "Smoothing period for the Average Directional Index", "Indicators")
			;

		_maxTrades = Param(nameof(MaxTrades), 9)
			.SetRange(1, 15)
			.SetDisplay("Max Trades", "Maximum number of simultaneous entries per direction", "Risk")
			;

		_enableRiskControl = Param(nameof(EnableRiskControl), true)
			.SetDisplay("Enable Risk Control", "Adjust volume by portfolio value similar to the MQL version", "Risk");

		_maxVolume = Param(nameof(MaxVolume), 3m)
			.SetNotNegative()
			.SetDisplay("Max Volume", "Upper limit for market orders", "Risk");

		_minVolume = Param(nameof(MinVolume), 0.01m)
			.SetNotNegative()
			.SetDisplay("Min Volume", "Lower limit for market orders", "Risk");

		_baseVolume = Param(nameof(BaseVolume), 0.15m)
			.SetNotNegative()
			.SetDisplay("Base Volume", "Base lot used before applying risk control", "Risk")
			;

		_riskDivider = Param(nameof(RiskDivider), 10000m)
			.SetNotNegative()
			.SetDisplay("Risk Divider", "Portfolio divider used to scale volume when risk control is enabled", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 1000)
			.SetNotNegative()
			.SetDisplay("Stop Loss Points", "Stop distance converted to price steps", "Protection")
			;

		_enableTakeProfit = Param(nameof(EnableTakeProfit), true)
			.SetDisplay("Enable Take Profit", "Activate the small scalping take profit from the EA", "Protection");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 10)
			.SetNotNegative()
			.SetDisplay("Take Profit Points", "Take-profit distance converted to price steps", "Protection")
			;

		_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();

		_adx = null;
		_longTrades = 0;
		_shortTrades = 0;

		Volume = BaseVolume;
	}

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

		_adx = new AverageDirectionalIndex { Length = AdxPeriod };

		// Subscribe to the configured candle series and calculate ADX on the fly.
		var subscription = SubscribeCandles(CandleType);
		subscription
			.BindEx(_adx, ProcessCandle)
			.Start();

		var takeProfitUnit = EnableTakeProfit && TakeProfitPoints > 0
			? new Unit(TakeProfitPoints, UnitTypes.Absolute)
			: null;
		var stopLossUnit = StopLossPoints > 0
			? new Unit(StopLossPoints, UnitTypes.Absolute)
			: null;

		// Mirror the original stop-loss and scalping take-profit distances.
		StartProtection(
			takeProfit: takeProfitUnit,
			stopLoss: stopLossUnit,
			useMarketOrders: true);

		// Preload the base volume so manual actions in the UI use the same size.
		var adjustedVolume = AdjustVolume(BaseVolume);
		Volume = adjustedVolume > 0m ? adjustedVolume : BaseVolume;

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

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue adxValue)
	{
		// Only evaluate completed candles.
		if (candle.State != CandleStates.Finished)
			return;

		// Wait until ADX indicator is formed.
		if (!_adx.IsFormed)
			return;

		// Ensure ADX produced a final value for this bar.
		if (adxValue is not AverageDirectionalIndexValue adx || !adxValue.IsFinal)
			return;

		var plusDi = adx.Dx.Plus ?? 0m;
		var minusDi = adx.Dx.Minus ?? 0m;

		// Compare +DI and -DI components to determine the signal.
		var direction = CalculateDirection(plusDi, minusDi);

		switch (direction)
		{
			case > 0:
				HandleLongSignal();
				break;
			case < 0:
				HandleShortSignal();
				break;
			default:
				break;
		}
	}

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

		// Reset scaling counters once the net position flips or becomes flat.
		if (Position == 0)
		{
			_longTrades = 0;
			_shortTrades = 0;
		}
		else if (Position > 0)
		{
			_shortTrades = 0;
		}
		else
		{
			_longTrades = 0;
		}
	}

	private int CalculateDirection(decimal plusDi, decimal minusDi)
	{
		if (plusDi > minusDi)
			return 1;

		if (minusDi > plusDi)
			return -1;

		return 0;
	}

	private void HandleLongSignal()
	{
		if (Security is null)
			return;

		// Netting accounts cannot keep opposite positions, so close shorts first.
		if (Position < 0)
		{
			BuyMarket(Math.Abs(Position));
			_shortTrades = 0;
		}

		// Respect the scaling cap inherited from the original EA.
		if (_longTrades >= MaxTrades)
			return;

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

		// Add one more market order using the calculated lot size.
		BuyMarket(volume);
		_longTrades++;
	}

	private void HandleShortSignal()
	{
		if (Security is null)
			return;

		// Flat the long exposure before opening new short trades.
		if (Position > 0)
		{
			SellMarket(Math.Abs(Position));
			_longTrades = 0;
		}

		if (_shortTrades >= MaxTrades)
			return;

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

		SellMarket(volume);
		_shortTrades++;
	}

	private decimal CalculateOrderVolume()
	{
		// Start from the base lot size defined by BaseVolume.
		var volume = BaseVolume;

		if (EnableRiskControl)
		{
			// Reproduce the PowerRisk logic: balance / divider with a floor of 1.
			var portfolioValue = Portfolio?.CurrentValue ?? Portfolio?.BeginValue ?? 0m;
			if (portfolioValue <= 0m)
				portfolioValue = 0m;

			var riskFactor = RiskDivider > 0m ? portfolioValue / RiskDivider : 0m;

			if (riskFactor < 1m)
				riskFactor = 1m;

			volume *= riskFactor;
		}

		// Apply user-defined caps before exchange-specific adjustments.
		if (MaxVolume > 0m && volume > MaxVolume)
			volume = MaxVolume;

		if (MinVolume > 0m && volume < MinVolume)
			volume = MinVolume;

		// Align with exchange volume constraints.
		var adjusted = AdjustVolume(volume);
		if (MaxVolume > 0m && adjusted > MaxVolume)
			adjusted = MaxVolume;

		if (adjusted <= 0m && MinVolume > 0m)
			adjusted = MinVolume;

		return adjusted;
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (Security is null)
			return volume;

		var step = Security.VolumeStep ?? 0m;
		if (step > 0m)
		{
			// Round the value to the nearest allowed volume step.
			var rounded = step * Math.Floor(volume / step);
			volume = rounded > 0m ? rounded : step;
		}

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

		var maxVolume = Security.MaxVolume;
		if (maxVolume != null && maxVolume.Value > 0m && volume > maxVolume.Value)
			volume = maxVolume.Value;

		return volume;
	}
}