GitHub で見る

5/8 MAクロス戦略

概要

5/8 MAクロス戦略は、MetaTraderのエキスパートアドバイザー「5_8 MACross」のStockSharpへのポートです。終値で計算された高速指数移動平均(EMA)と始値で計算された低速EMAを比較します。システムは2つの移動平均のクロスオーバーで動作し、標準的な時間ベースのローソク足を提供する任意のシンボルと時間軸に適用できます。

インジケーター

  • 高速EMA – 設定可能な長さ(デフォルト5)、ローソク足の終値から計算。
  • 低速EMA – 設定可能な長さ(デフォルト8)、ローソク足の始値から計算。

トレードロジック

  1. 戦略は部分的なデータを避けるために完了したローソク足のみを処理します。
  2. 高速EMAが前のローソク足で低速EMAより下または等しく、現在のローソク足でその上にクロスした場合にロングエントリーが生成されます。
  3. 高速EMAが前のローソク足で低速EMAより上または等しく、現在のローソク足でその下にクロスした場合にショートエントリーが生成されます。
  4. シグナルが現れると、戦略はエクスポージャーを反転します:オープンポジションを閉じ、新しい方向でVolumeコントラクトとなるようにサイズを調整した成行注文を送信します。

リスク管理

  • テイクプロフィット – 価格ポイントで表されるオプションの目標。ポイントサイズはインストゥルメントの価格ステップから導出され、3桁と5桁の気配値ではMetaTraderのpip処理をエミュレートするために値が自動的に10倍されます。
  • ストップロス – エントリー価格から価格ポイントで表されるオプションの保護ストップ。
  • トレーリングストップ – 価格ポイントのオプションの距離。ポジションが開かれた後、戦略は最高値(ロング用)または最安値(ショート用)を追跡し、利益方向にのみストップを移動します。初期ストップロスが指定されていない場合でも、トレーリングストップはエントリー直後に保護を開始します。
  • テイクプロフィットまたは(トレーリング)ストップが終値で達成された場合、ポジションは成行で決済されます。

パラメーター

名前 説明 デフォルト値
FastLength 高速EMA(終値ベース)の期間。 5
SlowLength 低速EMA(始値ベース)の期間。 8
TakeProfitPoints 価格ポイントでのテイクプロフィット距離。 40
StopLossPoints 価格ポイントでのストップロス距離(0はストップを無効化)。 0
TrailingStopPoints 価格ポイントでのトレーリングストップ距離(0はトレーリングを無効化)。 0
CandleType 計算に使用するローソク足タイプ/時間軸。 1分の時間軸
Volume 基底クラスStrategyから継承した注文ボリューム。 0.1

MQLバージョンとの違い

  • StockSharpはポジション会計を異なる方法で処理するため、MetaTrader固有のヘッジングチェックとアカウント情報呼び出しは省略されています。
  • シグナルは新しいバーの最初のティックではなく、閉じたローソク足で評価されます。これにより、イベント駆動型環境での安定性が向上します。
  • トレーリングロジックは現在のbid/askティックの代わりにローソク足の高値/安値を使ってストップを進め、履歴処理に対して決定論的な動作を提供します。

使用上の注意

  • 希望のロットサイズに合わせて戦略プロパティでVolumeを設定します。
  • ポートフォリオレベルのリスク管理が必要な場合は、StockSharpの保護モジュールや追加フィルターと組み合わせます。
  • 戦略は指値注文を発注しません。すべてのエントリーと決済は、上記のクロスオーバーとリスクロジックによって生成された成行注文で実行されます。
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>
/// 5/8 exponential moving average crossover strategy converted from MetaTrader.
/// Uses a fast EMA on close prices and a slower EMA on open prices with manual stop, take profit, and trailing logic.
/// </summary>
public class FiveEightMaCrossStrategy : Strategy
{
	private readonly StrategyParam<int> _fastLength;
	private readonly StrategyParam<int> _slowLength;
	private readonly StrategyParam<decimal> _takeProfitPoints;
	private readonly StrategyParam<decimal> _stopLossPoints;
	private readonly StrategyParam<decimal> _trailingStopPoints;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastMa = null!;
	private ExponentialMovingAverage _slowMa = null!;

	private decimal _prevFast;
	private decimal _prevSlow;
	private bool _isInitialized;

	private decimal _pointValue;
	private decimal? _entryPrice;
	private decimal? _stopPrice;
	private decimal? _takePrice;
	private decimal _trailDistance;
	private decimal _maxPrice;
	private decimal _minPrice;

	/// <summary>
	/// Fast EMA length.
	/// </summary>
	public int FastLength
	{
		get => _fastLength.Value;
		set => _fastLength.Value = value;
	}

	/// <summary>
	/// Slow EMA length.
	/// </summary>
	public int SlowLength
	{
		get => _slowLength.Value;
		set => _slowLength.Value = value;
	}

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

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

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

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

	/// <summary>
	/// Initialize <see cref="FiveEightMaCrossStrategy"/>.
	/// </summary>
	public FiveEightMaCrossStrategy()
	{
		_fastLength = Param(nameof(FastLength), 8)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA Length", "Length of the EMA calculated on closing prices", "Indicators")
			
			.SetOptimize(3, 20, 1);

		_slowLength = Param(nameof(SlowLength), 21)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA Length", "Length of the EMA calculated on opening prices", "Indicators")
			
			.SetOptimize(5, 30, 1);

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 40m)
			.SetDisplay("Take Profit (points)", "Take profit distance expressed in price points", "Risk Management")
			.SetNotNegative()
			
			.SetOptimize(10m, 100m, 10m);

		_stopLossPoints = Param(nameof(StopLossPoints), 0m)
			.SetDisplay("Stop Loss (points)", "Stop loss distance expressed in price points", "Risk Management")
			.SetNotNegative()
			
			.SetOptimize(0m, 100m, 10m);

		_trailingStopPoints = Param(nameof(TrailingStopPoints), 0m)
			.SetDisplay("Trailing Stop (points)", "Trailing stop distance expressed in price points", "Risk Management")
			.SetNotNegative()
			
			.SetOptimize(0m, 100m, 10m);

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

		Volume = 0.1m;
	}

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

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

	_fastMa = null!;
	_slowMa = null!;
	_prevFast = 0m;
	_prevSlow = 0m;
	_isInitialized = false;
	_pointValue = 0m;
	ResetPositionState();
	}

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

	_fastMa = new EMA { Length = FastLength };
	_slowMa = new EMA { Length = SlowLength };

	_pointValue = CalculatePointValue();

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

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

	private decimal CalculatePointValue()
	{
	var step = Security?.PriceStep;

	if (step == null || step.Value <= 0m)
	return 1m;

	var stepValue = step.Value;
	var stepDouble = (double)stepValue;

	if (stepDouble <= 0d)
	return stepValue;

	var digitsDouble = Math.Log10(1d / stepDouble);
	var digits = (int)Math.Round(digitsDouble, MidpointRounding.AwayFromZero);
	var multiplier = (digits == 3 || digits == 5) ? 10m : 1m;

	return stepValue * multiplier;
	}

	private void ProcessCandle(ICandleMessage candle)
	{
	// Process only finished candles to avoid repainting.
	if (candle.State != CandleStates.Finished)
	return;

	// Feed indicators with the corresponding price source.
	var fastValue = _fastMa.Process(new DecimalIndicatorValue(_fastMa, candle.ClosePrice, candle.OpenTime) { IsFinal = true }).ToDecimal();
	var slowValue = _slowMa.Process(new DecimalIndicatorValue(_slowMa, candle.OpenPrice, candle.OpenTime) { IsFinal = true }).ToDecimal();

	if (!_fastMa.IsFormed || !_slowMa.IsFormed)
	{
	_prevFast = fastValue;
	_prevSlow = slowValue;
	return;
	}

	HandleRiskManagement(candle);

	// indicators are processed manually

	if (!_isInitialized)
	{
	_prevFast = fastValue;
	_prevSlow = slowValue;
	_isInitialized = true;
	return;
	}

	var crossUp = _prevFast <= _prevSlow && fastValue > slowValue;
	var crossDown = _prevFast >= _prevSlow && fastValue < slowValue;

	if (crossUp && Position <= 0)
	{
	EnterLong(candle);
	}
	else if (crossDown && Position >= 0)
	{
	EnterShort(candle);
	}

	_prevFast = fastValue;
	_prevSlow = slowValue;
	}

	private void EnterLong(ICandleMessage candle)
	{
	var positionVolume = Position < 0 ? Math.Abs(Position) : 0m;
	var volume = Volume + positionVolume;

	if (volume <= 0m)
	return;

	ResetPositionState();

	// Enter long position with volume including any short covering.
	BuyMarket(volume);

	_entryPrice = candle.ClosePrice;
	_takePrice = TakeProfitPoints > 0m ? _entryPrice + TakeProfitPoints * _pointValue : null;
	_stopPrice = StopLossPoints > 0m ? _entryPrice - StopLossPoints * _pointValue : null;
	_trailDistance = TrailingStopPoints > 0m ? TrailingStopPoints * _pointValue : 0m;
	_maxPrice = candle.HighPrice;
	_minPrice = candle.LowPrice;

	if (_trailDistance > 0m)
	{
	var trailStart = _entryPrice.Value - _trailDistance;
	if (_stopPrice == null || trailStart > _stopPrice.Value)
	_stopPrice = trailStart;
	}
	}

	private void EnterShort(ICandleMessage candle)
	{
	var positionVolume = Position > 0 ? Position : 0m;
	var volume = Volume + positionVolume;

	if (volume <= 0m)
	return;

	ResetPositionState();

	// Enter short position with volume including any long exit.
	SellMarket(volume);

	_entryPrice = candle.ClosePrice;
	_takePrice = TakeProfitPoints > 0m ? _entryPrice - TakeProfitPoints * _pointValue : null;
	_stopPrice = StopLossPoints > 0m ? _entryPrice + StopLossPoints * _pointValue : null;
	_trailDistance = TrailingStopPoints > 0m ? TrailingStopPoints * _pointValue : 0m;
	_maxPrice = candle.HighPrice;
	_minPrice = candle.LowPrice;

	if (_trailDistance > 0m)
	{
	var trailStart = _entryPrice.Value + _trailDistance;
	if (_stopPrice == null || trailStart < _stopPrice.Value)
	_stopPrice = trailStart;
	}
	}

	private void HandleRiskManagement(ICandleMessage candle)
	{
	if (Position > 0 && _entryPrice.HasValue)
	{
	// Update trailing stop using the highest price reached since entry.
	_maxPrice = Math.Max(_maxPrice, candle.HighPrice);

	if (_trailDistance > 0m)
	{
	var trailCandidate = _maxPrice - _trailDistance;
	if (_stopPrice == null || trailCandidate > _stopPrice.Value)
	_stopPrice = trailCandidate;
	}

	if (_takePrice.HasValue && candle.ClosePrice >= _takePrice.Value)
	{
	SellMarket(Math.Abs(Position));
	ResetPositionState();
	return;
	}

	if (_stopPrice.HasValue && candle.ClosePrice <= _stopPrice.Value)
	{
	SellMarket(Math.Abs(Position));
	ResetPositionState();
	return;
	}
	}
	else if (Position < 0 && _entryPrice.HasValue)
	{
	// Update trailing stop using the lowest price reached since entry.
	_minPrice = Math.Min(_minPrice, candle.LowPrice);

	if (_trailDistance > 0m)
	{
	var trailCandidate = _minPrice + _trailDistance;
	if (_stopPrice == null || trailCandidate < _stopPrice.Value)
	_stopPrice = trailCandidate;
	}

	if (_takePrice.HasValue && candle.ClosePrice <= _takePrice.Value)
	{
	BuyMarket(Math.Abs(Position));
	ResetPositionState();
	return;
	}

	if (_stopPrice.HasValue && candle.ClosePrice >= _stopPrice.Value)
	{
	BuyMarket(Math.Abs(Position));
	ResetPositionState();
	}
	}
	}

	private void ResetPositionState()
	{
	_entryPrice = null;
	_stopPrice = null;
	_takePrice = null;
	_trailDistance = 0m;
	_maxPrice = 0m;
	_minPrice = 0m;
	}
}