GitHub で見る

Up3x1動的ポジションサイジング戦略

概要

  • MetaTrader 5エキスパートアドバイザーup3x1.mq5をStockSharp高レベルAPIに変換。
  • 三重指数移動平均(EMA)クロスオーバーによる取引とストップロス、テイクプロフィット、トレーリングストップ管理。
  • バーごとに1つの決定を強制した元のiTickVolume(0) > 1ガードをエミュレートするために完成したローソク足のみを処理します。
  • デフォルトのローソク足シリーズは1時間ですが、時間軸はCandleTypeパラメーターで設定可能です。

取引ロジック

  1. インジケーター
    • 速いEMA(FastPeriod、デフォルト24)。
    • 中間EMA(MediumPeriod、デフォルト60)。
    • 遅いEMA(SlowPeriod、デフォルト120)。
  2. ロングエントリー
    • 前のバー:速いEMAが中間EMAより下で、中間が遅いEMAより下(EMAfast₍t-1₎ < EMAmedium₍t-1₎ < EMAslow₍t-1₎)。
    • 現在のバー:速いEMAが遅いEMAより下のままで中間EMAが速いEMAより下(EMAmedium₍t₎ < EMAfast₍t₎ < EMAslow₍t₎)。
  3. ショートエントリー
    • 前のバー:速いEMAが中間EMAより上で、中間が遅いEMAより上(EMAfast₍t-1₎ > EMAmedium₍t-1₎ > EMAslow₍t-1₎)。
    • 現在のバー:中間EMAが速いEMAを上方クロスし、両方が遅いEMAより上に留まる(EMAmedium₍t₎ > EMAfast₍t₎ > EMAslow₍t₎)。
  4. 両方向のエグジットロジック
    • 価格がエントリーからTakeProfitOffset前進したらテイクプロフィット(ロングはローソク足の高値、ショートは安値を使用)。
    • 価格がエントリーからStopLossOffset戻ったらストップロス(ロングはローソク足の安値、ショートは高値を使用)。
    • トレーリングストップはポジションがTrailingStopOffsetより有利に動いたら起動し、その後ローソク足の極値で評価された固定距離で価格に追従します。
    • 速いEMAが遅いEMAより上に留まりながら中間EMAより下にクロスバックしたらフォールバックエグジット(MQLバージョンのma_one_1 > ma_two_1 > ma_three_1チェックを反映)。

ポジションサイジングとリスク管理

  • RiskFraction(デフォルト0.02)は現在のポートフォリオ価値に乗算して元のFreeMargin * 0.02 / 1000ロットサイジングを近似します。
  • BaseVolume(デフォルト0.1)はポートフォリオデータが利用できないか計算されたサイズが正でない場合のフォールバックとして機能します。
  • 1回以上の損失エグジット後、ボリュームはvolume * losses / 3で削減され、スクリプトの累積lossesカウンターを模倣します(カウンターは元のコードと同様に利益トレードの後にリセットされません)。
  • ボリュームはSecurity.VolumeStepに切り捨てられ、Security.MinVolume / Security.MaxVolumeで制限され、インストゥルメントの最小値が満たせない場合はゼロになります。

パラメーター

パラメーター デフォルト 説明
FastPeriod 24 最も速いEMAの長さ。
MediumPeriod 60 中間EMAの長さ。
SlowPeriod 120 長期トレンドフィルターとして使用される遅いEMAの長さ。
TakeProfitOffset 0.015 テイクプロフィット注文の絶対価格距離(インストゥルメントの見積もりに適応)。
StopLossOffset 0.01 ストップロス注文の絶対価格距離。
TrailingStopOffset 0.004 価格が十分に前進したら利益を確保するトレーリング距離。無効にするには0に設定。
BaseVolume 0.1 動的サイジングが計算できない場合のフォールバック取引サイズ。
RiskFraction 0.02 動的サイジング式に適用されるポートフォリオ価値の割合。
CandleType 1時間時間軸 インジケーター計算と意思決定に使用されるローソク足シリーズ。

変換に関する注意事項

  • トレーリングストップと保護的エグジットは高レベルAPIが完成したローソク足を処理するため、生のティックではなくローソク足の高値/安値を使用します。これによりバックテストとライブ実行全体で動作が決定論的に保たれます。
  • ストップロスとテイクプロフィットは、個別の保護注文を配置するのではなく、評価されたしきい値での成行フラット化コマンドで実行され、高レベル戦略フローとの互換性を確保します。
  • 動的ポジションサイジングはPortfolio.CurrentValueに依存します。利用できない場合、戦略は元のLotCheckの手動Lots入力へのフォールバックと同様にBaseVolumeにフォールバックします。
  • lossesカウンターはMQL実装に従うため意図的に累積(勝ちトレードでリセットされない)です。
  • すべてのコメントはプロジェクトのガイドラインに従い英語です。

使用のヒント

  1. 戦略をインストゥルメントとポートフォリオに付加し、MT5からエミュレートしたいチャート解像度に合わせてCandleTypeを設定します。
  2. インストゥルメントのティックサイズを反映するように価格オフセットを確認してください(例:5桁のForexペアの場合、0.015はソースエキスパートと同様に150ポイントに相当)。
  3. アカウントに対して現実的なポジションサイズを達成するためにRiskFraction / BaseVolumeを調整します。
  4. オプション:TrailingStopOffsetをゼロに設定してトレーリングを無効にします。
  5. MetaTraderのPrint診断を反映する「Enter long」や「Exit short」などのメッセージについてログを監視してください。

リポジトリ構造

API/2512_Up3x1/
├── CS/Up3x1DynamicSizingStrategy.cs      # 変換されたC#戦略
├── 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>
/// EMA triple crossover strategy converted from the MetaTrader 5 "up3x1" expert.
/// Uses three exponential moving averages with optional stop loss, take profit and trailing logic.
/// Position size is reduced after losing trades similar to the original lot optimization routine.
/// </summary>
public class Up3x1DynamicSizingStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _mediumPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<decimal> _takeProfitOffset;
	private readonly StrategyParam<decimal> _stopLossOffset;
	private readonly StrategyParam<decimal> _trailingStopOffset;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _riskFraction;
	private readonly StrategyParam<DataType> _candleType;

	private ExponentialMovingAverage _fastEma;
	private ExponentialMovingAverage _mediumEma;
	private ExponentialMovingAverage _slowEma;

	private bool _hasPrevValues;
	private decimal _prevFast;
	private decimal _prevMedium;
	private decimal _prevSlow;

	private decimal _entryPrice;
	private decimal _highestPrice;
	private decimal _lowestPrice;

	private int _losses;

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Medium EMA period.
	/// </summary>
	public int MediumPeriod
	{
		get => _mediumPeriod.Value;
		set => _mediumPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Absolute price distance for take profit.
	/// </summary>
	public decimal TakeProfitOffset
	{
		get => _takeProfitOffset.Value;
		set => _takeProfitOffset.Value = value;
	}

	/// <summary>
	/// Absolute price distance for stop loss.
	/// </summary>
	public decimal StopLossOffset
	{
		get => _stopLossOffset.Value;
		set => _stopLossOffset.Value = value;
	}

	/// <summary>
	/// Absolute trailing stop distance.
	/// </summary>
	public decimal TrailingStopOffset
	{
		get => _trailingStopOffset.Value;
		set => _trailingStopOffset.Value = value;
	}

	/// <summary>
	/// Base volume used when dynamic sizing cannot be calculated.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Fraction of portfolio value used for dynamic position sizing.
	/// </summary>
	public decimal RiskFraction
	{
		get => _riskFraction.Value;
		set => _riskFraction.Value = value;
	}

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

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public Up3x1DynamicSizingStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 24)
			.SetGreaterThanZero()
			.SetDisplay("Fast EMA", "Period of the fastest EMA", "Indicators");

		_mediumPeriod = Param(nameof(MediumPeriod), 60)
			.SetGreaterThanZero()
			.SetDisplay("Medium EMA", "Period of the middle EMA", "Indicators");

		_slowPeriod = Param(nameof(SlowPeriod), 120)
			.SetGreaterThanZero()
			.SetDisplay("Slow EMA", "Period of the slowest EMA", "Indicators");

		_takeProfitOffset = Param(nameof(TakeProfitOffset), 0.015m)
			.SetDisplay("Take Profit", "Absolute take profit distance in price units", "Risk");

		_stopLossOffset = Param(nameof(StopLossOffset), 0.01m)
			.SetDisplay("Stop Loss", "Absolute stop loss distance in price units", "Risk");

		_trailingStopOffset = Param(nameof(TrailingStopOffset), 0.004m)
			.SetDisplay("Trailing", "Trailing stop distance that follows price", "Risk");

		_baseVolume = Param(nameof(BaseVolume), 0.1m)
			.SetDisplay("Base Volume", "Fallback trade volume if dynamic sizing fails", "Money Management");

		_riskFraction = Param(nameof(RiskFraction), 0.02m)
			.SetDisplay("Risk Fraction", "Fraction of portfolio value used for sizing", "Money Management");

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

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

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

		Volume = BaseVolume;
		ResetState();
		_losses = 0;
		_hasPrevValues = false;
		_prevFast = 0m;
		_prevMedium = 0m;
		_prevSlow = 0m;
	}

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

		Volume = BaseVolume;

		_fastEma = new EMA
		{
			Length = FastPeriod
		};

		_mediumEma = new EMA
		{
			Length = MediumPeriod
		};

		_slowEma = new EMA
		{
			Length = SlowPeriod
		};

		var subscription = SubscribeCandles(CandleType);

		subscription
			.Bind(_fastEma, _mediumEma, _slowEma, ProcessCandle)
			.Start();

		StartProtection(null, null);
	}

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

		if (!_fastEma.IsFormed || !_mediumEma.IsFormed || !_slowEma.IsFormed)
			return;

		if (!_hasPrevValues)
		{
			_prevFast = fastValue;
			_prevMedium = mediumValue;
			_prevSlow = slowValue;
			_hasPrevValues = true;
			return;
		}

		if (!IsFormedAndOnlineAndAllowTrading())
		{
			_prevFast = fastValue;
			_prevMedium = mediumValue;
			_prevSlow = slowValue;
			return;
		}

		if (Position > 0)
		{
			if (TryHandleLongExit(candle, fastValue, mediumValue, slowValue))
			{
				_prevFast = fastValue;
				_prevMedium = mediumValue;
				_prevSlow = slowValue;
				return;
			}
		}
		else if (Position < 0)
		{
			if (TryHandleShortExit(candle, fastValue, mediumValue, slowValue))
			{
				_prevFast = fastValue;
				_prevMedium = mediumValue;
				_prevSlow = slowValue;
				return;
			}
		}
		else
		{
			var bullishSetup = _prevFast < _prevMedium && _prevMedium < _prevSlow && mediumValue < fastValue && fastValue < slowValue;
			var bearishSetup = _prevFast > _prevMedium && _prevMedium > _prevSlow && mediumValue > fastValue && fastValue > slowValue;

			if (bullishSetup)
			{
				TryEnterLong(candle);
			}
			else if (bearishSetup)
			{
				TryEnterShort(candle);
			}
		}

		_prevFast = fastValue;
		_prevMedium = mediumValue;
		_prevSlow = slowValue;
	}

	private void TryEnterLong(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume();

		if (volume <= 0m)
		{
			LogInfo("Skipped long entry because calculated volume is below minimum.");
			return;
		}

		BuyMarket();

		_entryPrice = candle.ClosePrice;
		_highestPrice = candle.HighPrice;
		_lowestPrice = candle.LowPrice;

		LogInfo($"Enter long at {candle.ClosePrice} with volume {volume}. Loss counter: {_losses}.");
	}

	private void TryEnterShort(ICandleMessage candle)
	{
		var volume = CalculateOrderVolume();

		if (volume <= 0m)
		{
			LogInfo("Skipped short entry because calculated volume is below minimum.");
			return;
		}

		SellMarket();

		_entryPrice = candle.ClosePrice;
		_highestPrice = candle.HighPrice;
		_lowestPrice = candle.LowPrice;

		LogInfo($"Enter short at {candle.ClosePrice} with volume {volume}. Loss counter: {_losses}.");
	}

	private bool TryHandleLongExit(ICandleMessage candle, decimal fastValue, decimal mediumValue, decimal slowValue)
	{
		if (_entryPrice <= 0m)
			return false;

		var exitPrice = 0m;
		var reason = string.Empty;

		if (TakeProfitOffset > 0m)
		{
			var target = _entryPrice + TakeProfitOffset;
			if (candle.HighPrice >= target)
			{
				exitPrice = target;
				reason = "Take profit reached";
			}
		}

		if (exitPrice == 0m && StopLossOffset > 0m)
		{
			var stop = _entryPrice - StopLossOffset;
			if (candle.LowPrice <= stop)
			{
				exitPrice = stop;
				reason = "Stop loss triggered";
			}
		}

		_highestPrice = candle.HighPrice > _highestPrice ? candle.HighPrice : _highestPrice;

		if (exitPrice == 0m && TrailingStopOffset > 0m && _highestPrice - _entryPrice > TrailingStopOffset)
		{
			var trail = _highestPrice - TrailingStopOffset;
			if (candle.LowPrice <= trail)
			{
				exitPrice = trail;
				reason = "Trailing stop hit";
			}
		}

		if (exitPrice == 0m)
		{
			var reversal = _prevFast > _prevMedium && _prevMedium > _prevSlow && slowValue < fastValue && fastValue < mediumValue;
			if (reversal)
			{
				exitPrice = candle.ClosePrice;
				reason = "EMA reversal";
			}
		}

		if (exitPrice == 0m)
			return false;

		ExitPosition(exitPrice, reason);
		return true;
	}

	private bool TryHandleShortExit(ICandleMessage candle, decimal fastValue, decimal mediumValue, decimal slowValue)
	{
		if (_entryPrice <= 0m)
			return false;

		var exitPrice = 0m;
		var reason = string.Empty;

		if (TakeProfitOffset > 0m)
		{
			var target = _entryPrice - TakeProfitOffset;
			if (candle.LowPrice <= target)
			{
				exitPrice = target;
				reason = "Take profit reached";
			}
		}

		if (exitPrice == 0m && StopLossOffset > 0m)
		{
			var stop = _entryPrice + StopLossOffset;
			if (candle.HighPrice >= stop)
			{
				exitPrice = stop;
				reason = "Stop loss triggered";
			}
		}

		_lowestPrice = _lowestPrice == 0m || candle.LowPrice < _lowestPrice ? candle.LowPrice : _lowestPrice;

		if (exitPrice == 0m && TrailingStopOffset > 0m && _entryPrice - _lowestPrice > TrailingStopOffset)
		{
			var trail = _lowestPrice + TrailingStopOffset;
			if (candle.HighPrice >= trail)
			{
				exitPrice = trail;
				reason = "Trailing stop hit";
			}
		}

		if (exitPrice == 0m)
		{
			var reversal = _prevFast > _prevMedium && _prevMedium > _prevSlow && slowValue < fastValue && fastValue < mediumValue;
			if (reversal)
			{
				exitPrice = candle.ClosePrice;
				reason = "EMA reversal";
			}
		}

		if (exitPrice == 0m)
			return false;

		ExitPosition(exitPrice, reason);
		return true;
	}

	private void ExitPosition(decimal exitPrice, string reason)
	{
		var isLong = Position > 0;
		var volume = Math.Abs(Position);

		if (volume <= 0m)
			return;

		var pnl = isLong
			? (exitPrice - _entryPrice) * volume
			: (_entryPrice - exitPrice) * volume;

		if (isLong)
		{
			SellMarket();
		}
		else
		{
			BuyMarket();
		}

		LogInfo($"Exit {(isLong ? "long" : "short")} at {exitPrice} because {reason}. Approx PnL: {pnl}.");

		if (pnl < 0m)
			_losses++;

		ResetState();
	}

	private decimal CalculateOrderVolume()
	{
		var volume = 0m;

		var portfolioValue = Portfolio?.CurrentValue ?? 0m;

		if (portfolioValue > 0m && RiskFraction > 0m)
			volume = portfolioValue * RiskFraction / 1000m;

		if (volume <= 0m)
			volume = BaseVolume;

		if (_losses > 1)
		{
			var reduction = volume * _losses / 3m;
			volume -= reduction;

			if (volume <= 0m)
				volume = BaseVolume;
		}

		volume = AdjustVolumeToInstrument(volume);

		return volume;
	}

	private decimal AdjustVolumeToInstrument(decimal volume)
	{
		var security = Security;

		if (security == null)
			return volume;

		var step = security.VolumeStep ?? 0m;

		if (step > 0m)
			volume = Math.Floor(volume / step) * step;

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

		var maxVolume = security.MaxVolume ?? 0m;
		if (maxVolume > 0m && volume > maxVolume)
			volume = maxVolume;

		return volume;
	}

	private void ResetState()
	{
		_entryPrice = 0m;
		_highestPrice = 0m;
		_lowestPrice = 0m;
	}
}