GitHub で見る

MT45 戦略

概要

MT45 戦略は、オリジナルの MetaTrader エキスパートアドバイザーを直接変換したものです。完了した各バーでロングとショートの市場ポジションを交互に切り替えながら、MQL 実装で使用されていた同じ固定のテイクプロフィットとストップロスの距離で各取引を保護します。ポジションサイジングはマーティンゲール方式の回復ルールに従い、次の取引は損失の結果の後にのみボリュームを増やします。

トレーディングロジック

  1. 戦略は Candle Type パラメーターで定義された単一のローソク足シリーズをサブスクライブし、バー内ノイズを避けるために完了したローソク足を待ちます。
  2. ポジションが開いておらず、前のエントリー注文が完全に処理された場合、アルゴリズムはこのターンに予定された方向(買い、次に売り、次に買い、...)でマーケット注文を送ります。
  3. 方向は対応する注文が執行された後にのみ切り替わり、各完了した取引が次のシグナルのために側面を反転させる MQL エキスパートの動作と一致することを保証します。
  4. ストップロスとテイクプロフィットの保護注文は StartProtection を通じて自動的に管理され、いずれかの距離に達したときに戦略が市場を退出します。

ポジションサイジング

  • Base Volume は初期ロットサイズを設定します。利益またはブレークイーブンの取引後に復元されます。
  • 損失の取引後、次のエントリーのボリュームは Martingale Multiplier で乗算されます。スケールされた値が Max Volume を超える場合、戦略は制御不能な成長を避けるためにベースボリュームに戻ります。
  • 実現損益は、出口価格と保存されたエントリー価格を比較することで測定され、元のエキスパートアドバイザーの Lot() 関数を再現します。

リスク管理

  • Stop PointsTake Points は価格ステップで表され、MetaTrader で使用されていた _Point 乗数を反映します。戦略は StartProtection を有効にする前に、インストゥルメントの PriceStep を通じてこれらの値を絶対価格距離に変換します。
  • 保護注文は各ポジションに自動的に添付され、ロングとショートの両方の取引に対称的に配置されます。

パラメーター

名前 説明 デフォルト
Stop Points 銘柄の価格ステップで表した保護ストップへの距離。 600
Take Points 銘柄の価格ステップで表したテイクプロフィット目標への距離。 700
Base Volume 勝利後の新規ポジションに使用するベースボリューム。 0.01
Martingale Multiplier 損失後に適用されるボリューム乗数。 2
Max Volume マーティンゲールスケーリングの最大許容ボリューム。 10
Candle Type バー完了の検出に使用するローソク足シリーズ(デフォルト:1分)。 1分

使用上の注意

  • 元のエキスパートのチャート時間軸と一致するローソク足の時間軸を選択してください。ロジックは完了したローソク足のみで動作します。
  • 戦略は、注文が保留中またはポジションがアクティブな間は別のエントリーをキューに入れません;常に既存の取引がストップロスまたはテイクプロフィットで閉じるのを待ちます。
  • 現在、この戦略の Python バージョンは存在せず、プロジェクトのガイドラインに従っています。
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>
/// Alternating long/short strategy converted from the original MT45 MQL expert.
/// </summary>
public class MT45Strategy : Strategy
{
	private readonly StrategyParam<decimal> _stopPoints;
	private readonly StrategyParam<decimal> _takePoints;
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _multiplier;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<DataType> _candleType;

	private Sides _nextSide;
	private Sides? _pendingSide;
	private bool _entryPending;
	private decimal _entryPrice;
	private decimal _lastTradeVolume;
	private decimal _nextVolume;
	private decimal _prevPosition;
	private decimal _pointValue;

	/// <summary>
	/// Stop-loss distance expressed in price steps.
	/// </summary>
	public decimal StopPoints
	{
		get => _stopPoints.Value;
		set => _stopPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in price steps.
	/// </summary>
	public decimal TakePoints
	{
		get => _takePoints.Value;
		set => _takePoints.Value = value;
	}

	/// <summary>
	/// Base trading volume used after profitable trades.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

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

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

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

	public MT45Strategy()
	{
		_stopPoints = Param(nameof(StopPoints), 600m)
			.SetDisplay("Stop Points", "Distance to stop loss measured in price steps", "Risk")
			
			.SetOptimize(100m, 1500m, 50m);

		_takePoints = Param(nameof(TakePoints), 700m)
			.SetDisplay("Take Points", "Distance to take profit measured in price steps", "Risk")
			
			.SetOptimize(100m, 2000m, 50m);

		_baseVolume = Param(nameof(BaseVolume), 1m)
			.SetDisplay("Base Volume", "Initial trade volume used by the strategy", "Trading");

		_multiplier = Param(nameof(MartingaleMultiplier), 2m)
			.SetDisplay("Martingale Multiplier", "Volume multiplier applied after a losing trade", "Trading")
			
			.SetOptimize(1m, 5m, 0.5m);

		_maxVolume = Param(nameof(MaxVolume), 10m)
			.SetDisplay("Max Volume", "Upper limit for martingale scaling", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Candle series used to trigger new trades", "General");

		_nextSide = Sides.Buy;
		_nextVolume = BaseVolume;
		_lastTradeVolume = BaseVolume;
		Volume = BaseVolume;
	}

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

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

		_nextSide = Sides.Buy;
		_pendingSide = null;
		_entryPending = false;
		_entryPrice = 0m;
		_lastTradeVolume = BaseVolume;
		_nextVolume = BaseVolume;
		_prevPosition = 0m;
		_pointValue = 0m;
		Volume = BaseVolume;
	}

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

		_pointValue = Security?.PriceStep ?? 1m;
		Volume = BaseVolume;

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

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

		StartProtection(
			takeProfit: CreatePriceUnit(TakePoints),
			stopLoss: CreatePriceUnit(StopPoints));
	}

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

		if (_entryPending || Position != 0)
			return;

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

		var side = _nextSide;
		_pendingSide = side;

		// Alternate between long and short trades every finished bar.
		if (side == Sides.Buy)
		{
			BuyMarket();
		}
		else
		{
			SellMarket();
		}

		_entryPending = true;
	}

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

		if (trade.Order == null || trade.Trade == null)
			return;

		var newPosition = Position;
		var previousPosition = _prevPosition;
		_prevPosition = newPosition;

		if (previousPosition == 0m && newPosition != 0m)
		{
			// Store entry price and volume for later profit calculation.
			_entryPrice = trade.Trade.Price;
			_lastTradeVolume = Math.Abs(newPosition);
			_entryPending = false;

			if (_pendingSide.HasValue)
			{
				_nextSide = Opposite(_pendingSide.Value);
				_pendingSide = null;
			}
		}
		else if (previousPosition != 0m && newPosition == 0m)
		{
			// Position closed: evaluate the result and adjust the next volume.
			var direction = previousPosition > 0m ? Sides.Buy : Sides.Sell;
			UpdateNextVolume(direction, trade.Trade.Price, Math.Abs(previousPosition));
			_entryPrice = 0m;
			_entryPending = false;
		}
	}

	private void UpdateNextVolume(Sides direction, decimal exitPrice, decimal volume)
	{
		if (volume <= 0m)
			return;

		var profit = direction == Sides.Buy
			? (exitPrice - _entryPrice) * volume
			: (_entryPrice - exitPrice) * volume;

		if (profit < 0m)
		{
			var scaled = _lastTradeVolume * MartingaleMultiplier;
			_nextVolume = scaled > MaxVolume ? BaseVolume : scaled;
		}
		else
		{
			_nextVolume = BaseVolume;
		}

		Volume = _nextVolume;
	}

	private Unit CreatePriceUnit(decimal points)
	{
		if (points <= 0m || _pointValue <= 0m)
			return default;

		return new Unit(points * _pointValue, UnitTypes.Absolute);
	}

	private static Sides Opposite(Sides side)
	{
		return side == Sides.Buy ? Sides.Sell : Sides.Buy;
	}
}