GitHub で見る

移動平均価格クロス戦略

概要

このパッケージには、MQL/50198 にある MetaTrader 5 のサンプルの 2 つの C# 戦略ポートが含まれています。

  • MovingAveragePriceCrossStrategy – 一度に 1 つのポジションを取引する最小限の移動平均と価格のクロスオーバー システム。
  • MovingAverageMartingaleStrategy – 同じ価格/平均クロスオーバーロジックを維持しながら、損失後にマーチンゲールスタイルのポジションサイジングを適用する拡張バージョン。

どちらの実装も高レベルの StockSharp API に依存し、シグナル評価にローソク足のサブスクリプションを使用し、ストップロスとテイクプロフィットの距離に MetaTrader 互換のパラメーターを公開します。

ファイル

ファイル 説明
CS/MovingAveragePriceCrossStrategy.cs 固定ボリュームと静的保護注文を使用した基準価格/MA クロスオーバー。
CS/MovingAverageMartingaleStrategy.cs Martingale バリアントは、取引に負けた後にボリュームと保護距離を調整します。

取引ロジック

移動平均価格クロス戦略

  1. 構成された時間枠のローソク足をサブスクライブし、単純移動平均 (SMA) を計算します。
  2. MT5 エキスパートの動作を模倣するために、完成したローソク足でのみシグナルを評価します。
  3. 最後の 2 つの完了したローソク足を使用して、SMA とローソク足終値の間のクロスオーバーを検出します。
    • **移動平均がローソク足の終値を上回った場合(価格が平均を下回った場合)、売ります
    • **移動平均がローソク足の終値を下回った場合(価格が平均を上回った場合)、**購入します。
  4. 現在オープンなポジションがない場合は、シグナルごとに 1 つの成行注文を発注します。
  5. MetaTrader ポイント距離を絶対価格オフセットに変換して、StartProtection 経由で自動保護を適用します。

移動平均マーチンゲール戦略

  1. 同じローソク足サブスクリプションと SMA シグナル生成を基本戦略と共有します。
  2. 各クローズポジション後に実現した損益を追跡し、最後の取引結果を保存します。
  3. 新しいクロスオーバーシグナルが表示され、オープンなポジションがない場合:
    • 最後の取引が損失が出ていた場合、次の取引量に VolumeMultiplier を乗算し (上限は MaxVolume)、ストップロスとテイクプロフィットの距離を TargetMultiplier だけ拡大します。
    • 最後の取引が利益を上げた場合、取引量と保護距離を初期値にリセットします。
  4. 成行注文を送信する直前に、動的に調整されたオフセットを使用して StartProtection を適用します。
  5. 元のエキスパートアドバイザーのロジックに一致して、一度に 1 つのポジションのみの取引を継続します。

リスク管理

  • プロテクティブ レベルは MetaTrader ポイントで表され、検出されたピップ サイズ (3/5 10 進数の FX シンボルに合わせて調整された PriceStep) を使用して絶対価格オフセットに自動的に変換されます。
  • マーチンゲール戦略では、ストップロスとテイクプロフィットの乗数を制限して距離の暴走を防ぎます。
  • ポジションボリュームは、無効な注文を避けるために、商品の VolumeStepMinVolume、およびオプションの MaxVolume に合わせて調整されます。

パラメーター

共有入力

パラメータ 戦略 デフォルト 説明
CandleType 両方 1 minute 信号計算に使用されるキャンドルのデータ型。
MaPeriod 両方 50 単純移動平均の長さ。

移動平均価格クロス戦略

パラメータ デフォルト 説明
OrderVolume 1 注文量は商品ステップに合わせて調整されます。
TakeProfitPoints 150 MetaTrader ポイント単位のテイクプロフィット距離 (0 無効)。
StopLossPoints 150 MetaTrader ポイント単位のストップロス距離 (0 無効)。

移動平均マーチンゲール戦略

パラメータ デフォルト 説明
StartingVolume 1 収益性の高い取引後にベースボリュームが回復しました。
MaxVolume 5 マルチプライヤを適用した後の最大ボリューム。
TakeProfitPoints 100 最初のテイクプロフィット距離(MetaTrader ポイント)。
StopLossPoints 300 最初のストップロス距離 (MetaTrader ポイント)。
VolumeMultiplier 2 損失後の次の注文量に適用される係数。
TargetMultiplier 2 損失後のストップロスとテイクプロフィットの距離に適用される係数。

使用上の注意

  • MetaTrader 個の「ポイント」は、ほとんどの商品の 1 つの PriceStep に対応します。戦略は、MT5 の動作に一致するように、10 進数 3 または 5 の FX シンボルに対して自動的に 10 を掛けます。
  • どちらの戦略も必要な証券は 1 つだけで、ポジションがオープンしている間はシグナルを無視し、元のエキスパートの PositionsTotal() ガードを再現します。
  • StockSharp デザイナー内の公開パラメータの最適化を有効にして、MT5 入力調整を複製します。
namespace StockSharp.Samples.Strategies;

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;

/// <summary>
/// Moving average crossover strategy with martingale money management converted from the MT5 "MovingAverageMartinGale" expert advisor.
/// Scales trade volume and protective distances after losses while resetting to the base configuration after profitable trades.
/// </summary>
public class MovingAverageMartingaleStrategy : Strategy
{
	private readonly StrategyParam<int> _maPeriod;
	private readonly StrategyParam<decimal> _startingVolume;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<int> _takeProfitPoints;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<decimal> _volumeMultiplier;
	private readonly StrategyParam<decimal> _targetMultiplier;
	private readonly StrategyParam<DataType> _candleType;

	private SMA _sma;
	private decimal? _previousClose;
	private decimal? _previousMa;
	private decimal? _currentClose;
	private decimal? _currentMa;
	private decimal _pipSize;

	private decimal _currentVolume;
	private decimal _currentTakeProfitPoints;
	private decimal _currentStopLossPoints;
	private decimal _lastRealizedPnL;
	private decimal _previousPosition;
	private decimal _lastTradeResult;

	/// <summary>
	/// Initializes a new instance of the <see cref="MovingAverageMartingaleStrategy"/> class.
	/// </summary>
	public MovingAverageMartingaleStrategy()
	{
		_maPeriod = Param(nameof(MaPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("MA period", "Length of the simple moving average used for entries.", "Indicator")
			;

		_startingVolume = Param(nameof(StartingVolume), 1m)
			.SetGreaterThanZero()
			.SetDisplay("Starting volume", "Base order volume used after profitable trades.", "Money management")
			;

		_maxVolume = Param(nameof(MaxVolume), 5m)
			.SetGreaterThanZero()
			.SetDisplay("Maximum volume", "Upper limit for martingale scaling.", "Money management")
			;

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 100)
			.SetNotNegative()
			.SetDisplay("Take profit (points)", "Initial profit target distance expressed in MetaTrader points.", "Risk")
			;

		_stopLossPoints = Param(nameof(StopLossPoints), 300)
			.SetNotNegative()
			.SetDisplay("Stop loss (points)", "Initial stop-loss distance expressed in MetaTrader points.", "Risk")
			;

		_volumeMultiplier = Param(nameof(VolumeMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Volume multiplier", "Factor applied to the next trade volume after a loss.", "Money management")
			;

		_targetMultiplier = Param(nameof(TargetMultiplier), 2m)
			.SetGreaterThanZero()
			.SetDisplay("Target multiplier", "Factor applied to stop-loss and take-profit distances after a loss.", "Money management")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle type", "Primary timeframe processed by the strategy.", "General");
	}

	/// <summary>
	/// Moving average period used for generating signals.
	/// </summary>
	public int MaPeriod
	{
		get => _maPeriod.Value;
		set => _maPeriod.Value = value;
	}

	/// <summary>
	/// Base position volume restored after profitable trades.
	/// </summary>
	public decimal StartingVolume
	{
		get => _startingVolume.Value;
		set => _startingVolume.Value = value;
	}

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

	/// <summary>
	/// Initial take-profit distance expressed in MetaTrader points.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Initial stop-loss distance expressed in MetaTrader points.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the trade volume after a losing trade.
	/// </summary>
	public decimal VolumeMultiplier
	{
		get => _volumeMultiplier.Value;
		set => _volumeMultiplier.Value = value;
	}

	/// <summary>
	/// Multiplier applied to stop-loss and take-profit distances after a losing trade.
	/// </summary>
	public decimal TargetMultiplier
	{
		get => _targetMultiplier.Value;
		set => _targetMultiplier.Value = value;
	}

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

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

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

		_sma = null;
		_previousClose = null;
		_previousMa = null;
		_currentClose = null;
		_currentMa = null;
		_pipSize = 0m;

		_currentVolume = 0m;
		_currentTakeProfitPoints = 0m;
		_currentStopLossPoints = 0m;
		_lastRealizedPnL = 0m;
		_previousPosition = 0m;
		_lastTradeResult = 0m;
	}

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

		_pipSize = CalculatePipSize();

		_currentVolume = NormalizeVolume(StartingVolume);
		_currentTakeProfitPoints = TakeProfitPoints;
		_currentStopLossPoints = StopLossPoints;
		_lastRealizedPnL = PnL;
		_previousPosition = Position;
		_lastTradeResult = 0m;

		_sma = new SMA { Length = MaPeriod };

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

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

		if (_previousPosition != 0m && Position == 0m)
		{
			var tradePnL = PnL - _lastRealizedPnL;
			_lastRealizedPnL = PnL;
			_lastTradeResult = tradePnL;
		}

		_previousPosition = Position;
	}

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

		if (!IsFormedAndOnlineAndAllowTrading())
			return;

		if (_currentClose is null)
		{
			_currentClose = candle.ClosePrice;
			_currentMa = maValue;
			return;
		}

		if (_previousClose is null)
		{
			_previousClose = _currentClose;
			_previousMa = _currentMa;
			_currentClose = candle.ClosePrice;
			_currentMa = maValue;
			return;
		}

		if (_sma?.IsFormed != true)
		{
			_previousClose = _currentClose;
			_previousMa = _currentMa;
			_currentClose = candle.ClosePrice;
			_currentMa = maValue;
			return;
		}

		var previousClose = _previousClose.Value;
		var previousMa = _previousMa!.Value;
		var currentClose = _currentClose.Value;
		var currentMa = _currentMa!.Value;

		var crossedBelowPrice = previousMa < previousClose && currentMa > currentClose;
		var crossedAbovePrice = previousMa > previousClose && currentMa < currentClose;

		if (Position == 0m && (crossedBelowPrice || crossedAbovePrice))
		{
			ApplyMartingaleAdjustments();
		}

		if (Position == 0m)
		{
			var volume = NormalizeVolume(_currentVolume);

			if (volume <= 0m)
			{
				ShiftBuffers(candle, maValue);
				return;
			}

			if (crossedBelowPrice)
			{
				ApplyProtection();
				SellMarket(volume);
			}
			else if (crossedAbovePrice)
			{
				ApplyProtection();
				BuyMarket(volume);
			}
		}

		ShiftBuffers(candle, maValue);
	}

	private void ApplyMartingaleAdjustments()
	{
		if (_lastTradeResult < 0m)
		{
			var nextVolume = Math.Min(_currentVolume * VolumeMultiplier, MaxVolume);
			_currentVolume = NormalizeVolume(nextVolume);

			_currentTakeProfitPoints = Math.Min(_currentTakeProfitPoints * TargetMultiplier, 100000m);
			_currentStopLossPoints = Math.Min(_currentStopLossPoints * TargetMultiplier, 100000m);
		}
		else if (_lastTradeResult > 0m)
		{
			_currentVolume = NormalizeVolume(StartingVolume);
			_currentTakeProfitPoints = TakeProfitPoints;
			_currentStopLossPoints = StopLossPoints;
		}

		_lastTradeResult = 0m;
	}

	private void ApplyProtection()
	{
		var stopDistance = _currentStopLossPoints > 0m ? _currentStopLossPoints * _pipSize : 0m;
		var takeDistance = _currentTakeProfitPoints > 0m ? _currentTakeProfitPoints * _pipSize : 0m;

		StartProtection(
			stopLoss: stopDistance > 0m ? new Unit(stopDistance, UnitTypes.Absolute) : null,
			takeProfit: takeDistance > 0m ? new Unit(takeDistance, UnitTypes.Absolute) : null);

		Volume = NormalizeVolume(_currentVolume);
	}

	private void ShiftBuffers(ICandleMessage candle, decimal maValue)
	{
		_previousClose = _currentClose;
		_previousMa = _currentMa;
		_currentClose = candle.ClosePrice;
		_currentMa = maValue;
	}

	private decimal NormalizeVolume(decimal volume)
	{
		var step = Security?.VolumeStep ?? 1m;
		if (step <= 0m)
			step = 1m;

		var minVolume = Security?.MinVolume ?? step;
		if (volume < minVolume)
			volume = minVolume;

		var multiplier = volume / step;
		var rounded = Math.Round(multiplier, MidpointRounding.AwayFromZero) * step;

		if (rounded < minVolume)
			rounded = minVolume;

		var maxVolume = Security?.MaxVolume;
		if (maxVolume is decimal max && rounded > max)
			rounded = max;

		rounded = Math.Min(rounded, MaxVolume);

		return Math.Max(rounded, step);
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0m;
		if (step <= 0m)
			step = 1m;

		var decimals = Security?.Decimals ?? 0;
		if (decimals == 3 || decimals == 5)
			step *= 10m;

		return step;
	}
}