GitHub で見る

EMA クロス 2 戦略

概要

この戦略は、MQL リポジトリの MetaTrader 4 Expert Advisor "EMA_CROSS_2" の StockSharp 移植です。オリジナルの EA は 2 つの指数移動平均 (EMA) を監視し、平均が注文を交換するたびに成行注文を発注します。このコンバージョンでは、スクリプトの逆張りの性質が維持されます。つまり、ロング EMA がショート EMA を上回ったときに買い、ショート EMA がロング EMA を上回ったときに売ります。その一方で、ロジックは高レベルの StockSharp 戦略インフラストラクチャにラップされています。

この戦略は、構成可能なローソク足のデータ型によって提供される完成したローソク足で動作します。同じバー内でトリガーが繰り返されることを避けるために、シグナルはローソク足付近で評価されます。リスク管理は、ブローカー ポイント (価格ステップ) で表されるテイクプロフィット、ストップロス、トレーリング ストップの距離を使用して、MetaTrader の動作を模倣します。

取引ロジック

  1. 指標の計算
    • 完成したすべてのローソク足の短期EMAと長期EMAを計算します。
    • 最初の評価を無視した元の first_time フラグと一致する最初のインジケーターの更新をスキップします。
    • その後、ロングとショートの EMA 間の相対的な順序が反転したときに、方向の変化を検出します。
  2. 信号解釈
    • ロング EMA がショート EMA の上に移動すると、元の EA が買い取引を開始しました。 StockSharp ポートは、古典的なクロスオーバー システムとは逆の動作をするにもかかわらず、この逆張りルールを維持します。
    • ショート EMA がロング EMA を上回って終了すると、戦略は売り取引を開始します。
    • 新しいポジションは、現在オープンしているエクスポージャーがない場合にのみ許可され、OrdersTotal() < 1 条件が再現されます。
  3. 注文の実行
    • 取引は、設定可能な固定量の成行注文として送信されます。
    • エントリー時に、ストラテジーはパラメーターを通じて提供されるピップ距離を使用してストップロスとテイクプロフィットの価格を記録します。
  4. リスク管理
    • 終了したキャンドルごとに、戦略は価格行動が保存されたストップロスまたはテイクプロフィットレベルに触れたかどうかをチェックします。いずれかのレベルを突破すると、成行注文でポジション全体がクローズされます。
    • 価格がトレーリングディスタンスを超えて有利に動くと、トレーリングストップ(ブローカーポイントでも定義されます)が適用されます。ロングポジションの場合、保護ストップは上方に移動します。ショートポジションの場合、価格が下落します。
    • 位置が平坦になると、保存されている保護レベルはクリアされます。

パラメーター

名前 説明 デフォルト
CandleType インジケーターの計算とシグナル検出に使用されるキャンドル シリーズ。 15分の時間枠
OrderVolume ロット/契約における各成行注文のボリューム。 2
TakeProfitPoints ブローカーポイント(価格ステップ)で表される利食いレベルまでの距離。 0 の値はテイクプロフィットを無効にします。 20
StopLossPoints ブローカーポイントで表されるストップロスレベルまでの距離。 0 の値はストップロスを無効にします。 30
TrailingStopPoints オープンポジションをトレーリングするときに使用される距離。 0 はトレーリングストップを無効にします。 50
ShortEmaPeriod 高速 EMA の長さ。 5
LongEmaPeriod 低速の EMA の長さ。 60

実装メモ

  • この戦略は、推奨される高レベルの API パターンに従って、SubscribeCandles().Bind(shortEma, longEma, ProcessCandle) を使用してローソク足データを EMA インジケーターに接続します。
  • インジケーター値は、バインディング コールバックですぐに使用できる 10 進数として受信されるため、手動でバッファーにインデックスを作成する必要はありません。
  • 保護距離は、商品 PriceStep を乗算することで、MetaTrader ポイントから StockSharp の価格に変換されます。商品が分数ピップ価格設定 (小数点以下 3 または 5) を使用する場合、ヘルパーはそれに応じてピップ サイズを計算します。
  • StockSharp は、MetaTrader と同じ OrderModify ワークフローを公開していないため、ストップロス、テイクプロフィット、およびトレーリング動作は市場出口とともに内部的に実装されます。 4. 結果として得られる取引管理は、元のロジックを反映しています。レベルはすべてのローソク足でチェックされ、突破されるとすぐに出口が発生します。
  • 最初のクロスオーバー評価は、MQL スクリプトで早すぎる取引を防止する first_time の保護機能を再現するために意図的に無視されます。

MetaTrader バージョンとの違い

  • 資金管理: 元の EA は常に Lots パラメータを交換しました。この変換により、OrderVolume を通じて同じコンセプトが公開され、それが戦略 Volume プロパティに割り当てられるため、デザイナーやオプティマイザーはそれを再利用できます。
  • 注文の配置: MetaTrader は、OrderSend 内で直接ストップロスとテイクプロフィットを適用しました。 StockSharp では、これらのレベルは戦略によって追跡され、違反された場合は成行注文で閉じられます。
  • トレーリング ストップの精度: EA は、ティック データ (Bid/Ask) を使用して移動されたストップです。このポートは、ローソク足のクローズ時にトレーリング ロジックを更新します。これは、このサンプル プロジェクト内で利用可能な最も細かい粒度です。距離とアクティブ化ルールは同じままです。
  • エラー処理とログ記録が簡素化されました。 StockSharp ログは、標準戦略ログを通じて詳細情報を提供します。

使用のヒント

  • CandleType を元の EA のバックテスト中に使用された時間枠に合わせて、同等のインジケーターの動作を維持します。
  • 小数ピップで引用されたシンボルを取引する場合、設定されたポイント距離が希望のピップ数を反映していることを確認してください (たとえば、EURUSD では 10 ポイントが 1 ピップに相当します)。
  • OrderVolume を、執行会場で予想される契約サイズに設定します。この戦略では、自動ボリューム スケーリングは実行されません。
  • 各パラメータの組み込みの最適化トグルを使用して、MetaTrader の入力を最適化するのと同じように、EMA の期間とリスク距離の組み合わせを調べます。

ファイル

  • CS/EmaCross2Strategy.cs – 取引ロジックの StockSharp 実装。
  • README.md – 英語のドキュメント (このファイル)。
  • README_zh.md – 中国語翻訳。
  • README_ru.md – ロシア語の翻訳。
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>
/// Counter-trend EMA crossover strategy converted from the MetaTrader 4 expert "EMA_CROSS_2".
/// Buys when the long EMA rises above the short EMA, and sells when the short EMA climbs above the long EMA.
/// Incorporates MetaTrader-style risk management with point-based stop-loss, take-profit, and trailing stop levels.
/// </summary>
public class EmaCross2Strategy : Strategy
{
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<int> _shortEmaPeriod;
	private readonly StrategyParam<int> _longEmaPeriod;

	private ExponentialMovingAverage _shortEma;
	private ExponentialMovingAverage _longEma;

	private bool _skipFirstSignal = true;
	private int _lastDirection;
	private decimal? _stopLossPrice;
	private decimal? _takeProfitPrice;
	private decimal _pointSize;
	private decimal _entryPrice;

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

	/// <summary>
	/// Order volume applied to new market orders.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set => _orderVolume.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in broker points.
	/// </summary>
	public decimal TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in broker points.
	/// </summary>
	public decimal StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Trailing stop distance expressed in broker points.
	/// </summary>
	public decimal TrailingStopPoints
	{
		get => _trailingStopPoints.Value;
		set => _trailingStopPoints.Value = value;
	}

	/// <summary>
	/// Period of the short EMA.
	/// </summary>
	public int ShortEmaPeriod
	{
		get => _shortEmaPeriod.Value;
		set => _shortEmaPeriod.Value = value;
	}

	/// <summary>
	/// Period of the long EMA.
	/// </summary>
	public int LongEmaPeriod
	{
		get => _longEmaPeriod.Value;
		set => _longEmaPeriod.Value = value;
	}

	/// <summary>
	/// Initialize strategy parameters.
	/// </summary>
	public EmaCross2Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(15).TimeFrame())
		.SetDisplay("Candle Type", "Time frame used for EMA calculations", "General");

		_orderVolume = Param(nameof(OrderVolume), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Volume of each market order", "Trading")
		
		.SetOptimize(0.1m, 5m, 0.1m);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Take Profit (points)", "Distance from entry to take-profit in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_stopLossPoints = Param(nameof(StopLossPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Stop Loss (points)", "Distance from entry to stop-loss in broker points", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 500m)
		.SetNotNegative()
		.SetDisplay("Trailing Stop (points)", "Trailing distance maintained after entry", "Risk")
		
		.SetOptimize(0m, 200m, 5m);

		_shortEmaPeriod = Param(nameof(ShortEmaPeriod), 5)
		.SetGreaterThanZero()
		.SetDisplay("Short EMA", "Length of the fast EMA", "Indicators")
		
		.SetOptimize(2, 40, 1);

		_longEmaPeriod = Param(nameof(LongEmaPeriod), 60)
		.SetGreaterThanZero()
		.SetDisplay("Long EMA", "Length of the slow EMA", "Indicators")
		
		.SetOptimize(10, 200, 5);
	}

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

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

		Volume = OrderVolume;
		_shortEma = null;
		_longEma = null;
		_skipFirstSignal = true;
		_lastDirection = 0;
		_stopLossPrice = null;
		_takeProfitPrice = null;
		_pointSize = 0m;
		_entryPrice = 0m;
	}

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

		Volume = OrderVolume;
		_pointSize = CalculatePointSize();

		_shortEma = new EMA { Length = ShortEmaPeriod };
		_longEma = new EMA { Length = LongEmaPeriod };

		var subscription = SubscribeCandles(CandleType);
		subscription
		.Bind(_shortEma, _longEma, ProcessCandle)
		.Start();

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

	private void ProcessCandle(ICandleMessage candle, decimal shortEmaValue, decimal longEmaValue)
	{
		// Work only with finished candles to avoid repeated signals inside the same bar.
		if (candle.State != CandleStates.Finished)
		return;

		if (_pointSize <= 0m)
		_pointSize = CalculatePointSize();

		if (CheckRisk(candle))
		return;

		if (Position != 0)
		UpdateTrailingStop(candle);
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		ResetRiskLevels();

		var signal = EvaluateCross(longEmaValue, shortEmaValue);

		if (signal == 0)
		return;

		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		if (Position != 0)
		return;

		var volume = OrderVolume;
		if (volume <= 0m)
		volume = 1m;

		if (signal == 1)
		{
			BuyMarket(volume);
			SetRiskLevels(candle.ClosePrice, true);
		}
		else if (signal == 2)
		{
			SellMarket(volume);
			SetRiskLevels(candle.ClosePrice, false);
		}
	}

	private int EvaluateCross(decimal longValue, decimal shortValue)
	{
		var currentDirection = 0;

		if (longValue > shortValue)
		currentDirection = 1;
		else if (longValue < shortValue)
		currentDirection = 2;

		if (_skipFirstSignal)
		{
			_skipFirstSignal = false;
			return 0;
		}

		if (currentDirection != 0 && currentDirection != _lastDirection)
		{
			_lastDirection = currentDirection;
			return _lastDirection;
		}

		return 0;
	}

	private bool CheckRisk(ICandleMessage candle)
	{
		if (Position > 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.LowPrice <= _stopLossPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.HighPrice >= _takeProfitPrice.Value)
			{
				SellMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (Position < 0)
		{
			var size = Math.Abs(Position);

			if (_stopLossPrice.HasValue && candle.HighPrice >= _stopLossPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}

			if (_takeProfitPrice.HasValue && candle.LowPrice <= _takeProfitPrice.Value)
			{
				BuyMarket(size);
				ResetRiskLevels();
				return true;
			}
		}
		else if (_stopLossPrice.HasValue || _takeProfitPrice.HasValue)
		{
			ResetRiskLevels();
		}

		return false;
	}

	private void UpdateTrailingStop(ICandleMessage candle)
	{
		if (TrailingStopPoints <= 0m || _pointSize <= 0m)
		return;

		var distance = TrailingStopPoints * _pointSize;
		if (distance <= 0m)
		return;

		var entryPrice = _entryPrice > 0 ? _entryPrice : candle.ClosePrice;

		if (Position > 0)
		{
			var profit = candle.ClosePrice - entryPrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice - distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value < candidate)
				_stopLossPrice = candidate;
			}
		}
		else if (Position < 0)
		{
			var profit = entryPrice - candle.ClosePrice;
			if (profit > distance)
			{
				var candidate = candle.ClosePrice + distance;
				if (!_stopLossPrice.HasValue || _stopLossPrice.Value > candidate)
				_stopLossPrice = candidate;
			}
		}
	}

	private void SetRiskLevels(decimal executionPrice, bool isLong)
	{
		if (_pointSize <= 0m)
		{
			ResetRiskLevels();
			return;
		}

		_stopLossPrice = StopLossPoints > 0m
		? executionPrice + (isLong ? -1m : 1m) * StopLossPoints * _pointSize
		: null;

		_takeProfitPrice = TakeProfitPoints > 0m
		? executionPrice + (isLong ? 1m : -1m) * TakeProfitPoints * _pointSize
		: null;
	}

	private void ResetRiskLevels()
	{
		_stopLossPrice = null;
		_takeProfitPrice = null;
	}

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

		if (Position != 0 && _entryPrice == 0m)
			_entryPrice = trade.Trade.Price;

		if (Position == 0m)
			_entryPrice = 0m;
	}

	private decimal CalculatePointSize()
	{
		var step = Security?.PriceStep ?? 0m;
		return step > 0m ? step : 1m;
	}
}