GitHub で見る

Martin 1 戦略

MetaTrader 5エキスパートアドバイザー「Martin 1」をStockSharpの高レベル戦略APIに変換したものです。アルゴリズムは継続的にエクスポージャーを維持し、ヘッジスタイルのマーチンゲールステップを使用してドローダウンから回復しながら、収益性の高いトレンドでピラミッドします。

トレードロジック

  1. 初期エクスポージャー – 戦略がフラットな場合、タイムフィルターに関係なくStartDirectionで定義された方向に即座にポジションを開きます。ベースの注文サイズはインスツルメントのボリュームステップに丸めた後、InitialVolumeから取得されます。
  2. 時間窓フィルターUseTradingHoursが有効の場合、ローソク足のタイムスタンプに含まれるエクスチェンジ時間を使用して、StartHourからEndHour(両端含む)の間のみスケーリングアクション(ピラミッドまたはヘッジング)が許可されます。
  3. 勝者のピラミッド – 各オープンポジションは完成したローソク足ごとに評価されます。ロングポジションの浮動利益がテイクプロフィット距離を超えてプラスのままである場合、現在のボリュームで追加のロング注文が送信されます。ショートポジションは対称的に動作します。新しい注文の価格は現在のローソク足のクローズと仮定されます。
  4. ヘッジングマーチンゲール – 開始方向がロングで、ロングポジションが(StopLossPips × pip サイズ × (乗数インデックス + 1))以上を失う場合、戦略は反対のショート注文を開きます。ヘッジを配置する前に、ボリュームはLotMultiplierで乗算され、許可されたステップに丸められ、乗数カウンターが増加します。同じロジックがショート開始方向に対して逆に適用されます。MaxMultiplicationsステップに達するとヘッジングは停止します。
  5. グローバル利益目標 – すべての残りポジションの未実現利益(PriceStep/StepPriceを使用してマネーに変換)が合計されます。MinProfitを超えると、各オープンポジションは反対方向の市場注文を発行することで閉じられ、マーチンゲール状態がリセットされます。

リスク・資金管理

  • pip サイズはインスツルメントのセキュリティ価格ステップから計算されます。3桁および5桁の相場はステップを10倍して、元のMetaTrader pip 調整をエミュレートします。
  • ボリュームは最も近いVolumeStepに切り捨てられます。丸められた値がステップを下回ると、注文はスキップされます。
  • マーチンゲールカウンターと現在のボリュームは、自然にまたはグローバル利益目標に達した後にブックがフラットになるたびにリセットされます。
  • 利益推定はコミッションとスワップを無視し、純粋に浮動PnLに依存していた元のスクリプトの動作を反映しています。

パラメーター

名前 説明 デフォルト値
CandleType すべての計算を駆動するローソク足タイプ。 1分時間軸
UseTradingHours 時間窓フィルターを有効または無効にします。 true
StartHour タイムフィルターが新しいスケーリングアクションを許可する開始時間(含む)。 2
EndHour スケーリングアクションが停止する終了時間(含む)。 21
LotMultiplier ヘッジを開く前に現在のボリュームに適用される係数。 1.6
MaxMultiplications 発動可能なヘッジングステップの最大数。 5
StartDirection 戦略がフラットになった後の最初の注文の方向。 Buy
MinProfit すべてのポジションを強制的に閉じる浮動利益(マネー単位)。 1.5
InitialVolume 最初の注文とリセット状態のベースボリューム。 0.1
StopLossPips 次のマーチンゲールヘッジを発動するpip距離。 40
TakeProfitPips ピラミッドエントリーを発動するpip距離。 100

実装上の注意

  • ProcessCandleは高レベルローソク足サブスクリプションパイプライン(SubscribeCandles().Bind(...))を使用し、プラットフォームガイドラインに準拠して完成したローソク足のみで動作します。
  • ヘッジされたエクスポージャーは2つのFIFOリストで内部的に追跡されるため、戦略はネッティングアカウントでもMetaTraderのヘッジング動作をエミュレートできます。
  • 利益変換はSecurity.PriceStepSecurity.StepPriceに依存します。これらの値が利用できない場合、価格差がフォールバックとして取引されたボリュームで直接乗算されます。
  • 戦略は継続的に取引します;タイムフィルターを無効にするか、広い時間を設定すると、アルゴリズムは元の常時オンのエキスパートアドバイザーのように動作します。
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>
/// Hedging martingale strategy converted from the MetaTrader script "Martin 1".
/// Adds pyramid entries in profit and opens opposite hedges with increased volume after drawdowns.
/// </summary>
public class Martin1Strategy : Strategy
{
	private sealed class PositionRecord
	{
		public decimal Volume { get; set; }
		public decimal EntryPrice { get; set; }
	}

	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<bool> _useTradingHours;
	private readonly StrategyParam<int> _startHour;
	private readonly StrategyParam<int> _endHour;
	private readonly StrategyParam<decimal> _lotMultiplier;
	private readonly StrategyParam<int> _maxMultiplications;
	private readonly StrategyParam<Sides> _startDirection;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;

	private readonly List<PositionRecord> _longPositions = new();
	private readonly List<PositionRecord> _shortPositions = new();

	private decimal _currentVolume;
	private int _multiplicationCount;

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

	/// <summary>
	/// Enable the trading hour filter.
	/// </summary>
	public bool UseTradingHours
	{
		get => _useTradingHours.Value;
		set => _useTradingHours.Value = value;
	}

	/// <summary>
	/// Inclusive hour (exchange time) when the trading window opens.
	/// </summary>
	public int StartHour
	{
		get => _startHour.Value;
		set => _startHour.Value = value;
	}

	/// <summary>
	/// Inclusive hour (exchange time) when the trading window closes.
	/// </summary>
	public int EndHour
	{
		get => _endHour.Value;
		set => _endHour.Value = value;
	}

	/// <summary>
	/// Multiplier applied to the current volume after each hedging step.
	/// </summary>
	public decimal LotMultiplier
	{
		get => _lotMultiplier.Value;
		set => _lotMultiplier.Value = value;
	}

	/// <summary>
	/// Maximum number of hedging multiplications that can be triggered.
	/// </summary>
	public int MaxMultiplications
	{
		get => _maxMultiplications.Value;
		set => _maxMultiplications.Value = value;
	}

	/// <summary>
	/// Direction of the very first position that is opened when flat.
	/// </summary>
	public Sides StartDirection
	{
		get => _startDirection.Value;
		set => _startDirection.Value = value;
	}

	/// <summary>
	/// Minimum floating profit that triggers closing all positions.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// Base order volume used for the initial trade.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

	/// <summary>
	/// Stop-loss distance expressed in pips.
	/// </summary>
	public int StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	/// <summary>
	/// Take-profit distance expressed in pips.
	/// </summary>
	public int TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	/// <summary>
	/// Initializes a new instance of the <see cref="Martin1Strategy"/> class.
	/// </summary>
	public Martin1Strategy()
	{
		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used to evaluate conditions", "General");

		_useTradingHours = Param(nameof(UseTradingHours), false)
			.SetDisplay("Use Trading Hours", "Restrict entries to a time window", "General");

		_startHour = Param(nameof(StartHour), 2)
			.SetRange(0, 23)
			.SetDisplay("Start Hour", "Hour to start monitoring for new trades", "General");

		_endHour = Param(nameof(EndHour), 21)
			.SetRange(0, 23)
			.SetDisplay("End Hour", "Hour to stop opening hedges/pyramids", "General");

		_lotMultiplier = Param(nameof(LotMultiplier), 1.6m)
			.SetGreaterThanZero()
			.SetDisplay("Lot Multiplier", "Factor applied to volume after a loss", "Money Management")
			
			.SetOptimize(1.1m, 3m, 0.1m);

		_maxMultiplications = Param(nameof(MaxMultiplications), 5)
			.SetGreaterThanZero()
			.SetDisplay("Max Multiplications", "Maximum hedging steps", "Money Management")
			
			.SetOptimize(1, 10, 1);

		_startDirection = Param(nameof(StartDirection), Sides.Buy)
			.SetDisplay("Start Direction", "Side of the initial order", "Trading");

		_minProfit = Param(nameof(MinProfit), 1.5m)
			.SetDisplay("Min Profit", "Floating profit target to flatten", "Risk")
			
			.SetOptimize(0.5m, 10m, 0.5m);

		_initialVolume = Param(nameof(InitialVolume), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Initial Volume", "Baseline order size", "Money Management");

		_stopLossPips = Param(nameof(StopLossPips), 400)
			.SetGreaterThanZero()
			.SetDisplay("Stop Loss (pips)", "Distance before hedging the opposite side", "Risk");

		_takeProfitPips = Param(nameof(TakeProfitPips), 1000)
			.SetGreaterThanZero()
			.SetDisplay("Take Profit (pips)", "Distance to pyramid in the same direction", "Risk");
	}

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

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

		_longPositions.Clear();
		_shortPositions.Clear();
		_multiplicationCount = 0;
		_currentVolume = AdjustVolume(InitialVolume);
	}

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

		if (UseTradingHours && StartHour >= EndHour)
			throw new InvalidOperationException("Start hour must be less than end hour when the filter is enabled.");

		_longPositions.Clear();
		_shortPositions.Clear();
		_multiplicationCount = 0;
		_currentVolume = AdjustVolume(InitialVolume);

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

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

		// No indicators to check.

		var closePrice = candle.ClosePrice;
		var totalProfit = CalculateOpenProfit(closePrice);
		var withinHours = !UseTradingHours || IsWithinTradingHours(candle.CloseTime);

		if (withinHours)
		{
			if (_longPositions.Count > 0)
				EvaluateLongPositions(closePrice);

			if (_shortPositions.Count > 0)
				EvaluateShortPositions(closePrice);
		}

		if (_longPositions.Count == 0 && _shortPositions.Count == 0)
		{
			ResetMartingale();
			OpenInitialPosition(closePrice);
			return;
		}

		if ((_longPositions.Count > 0 || _shortPositions.Count > 0) && totalProfit > MinProfit)
		{
			CloseAllPositions(closePrice);
			ResetMartingale();
		}
	}

	private void EvaluateLongPositions(decimal closePrice)
	{
		var takeProfitDistance = GetTakeProfitDistance();
		var stopLossDistance = GetStopLossDistance();
		var snapshot = _longPositions.ToArray();

		foreach (var position in snapshot)
		{
			var priceGain = closePrice - position.EntryPrice;
			var profit = ConvertPriceToMoney(priceGain, position.Volume);

			if (profit > 0m && priceGain > takeProfitDistance)
				ExecuteOrder(Sides.Buy, _currentVolume, closePrice);

			if (StartDirection == Sides.Buy && stopLossDistance > 0m)
			{
				var lossDistance = position.EntryPrice - closePrice;
				if (lossDistance > stopLossDistance * (_multiplicationCount + 1) &&
					_multiplicationCount + 1 <= MaxMultiplications)
				{
					var newVolume = AdjustVolume(_currentVolume * LotMultiplier);
					if (newVolume > 0m)
					{
						_multiplicationCount++;
						_currentVolume = newVolume;
						ExecuteOrder(Sides.Sell, newVolume, closePrice);
					}
				}
			}
		}
	}

	private void EvaluateShortPositions(decimal closePrice)
	{
		var takeProfitDistance = GetTakeProfitDistance();
		var stopLossDistance = GetStopLossDistance();
		var snapshot = _shortPositions.ToArray();

		foreach (var position in snapshot)
		{
			var priceGain = position.EntryPrice - closePrice;
			var profit = ConvertPriceToMoney(priceGain, position.Volume);

			if (profit > 0m && priceGain > takeProfitDistance)
				ExecuteOrder(Sides.Sell, _currentVolume, closePrice);

			if (StartDirection == Sides.Sell && stopLossDistance > 0m)
			{
				var lossDistance = closePrice - position.EntryPrice;
				if (lossDistance > stopLossDistance * (_multiplicationCount + 1) &&
					_multiplicationCount + 1 <= MaxMultiplications)
				{
					var newVolume = AdjustVolume(_currentVolume * LotMultiplier);
					if (newVolume > 0m)
					{
						_multiplicationCount++;
						_currentVolume = newVolume;
						ExecuteOrder(Sides.Buy, newVolume, closePrice);
					}
				}
			}
		}
	}

	private void OpenInitialPosition(decimal price)
	{
		var volume = _currentVolume;
		if (volume <= 0m)
			return;

		var side = StartDirection == Sides.Sell ? Sides.Sell : Sides.Buy;
		ExecuteOrder(side, volume, price);
	}

	private void CloseAllPositions(decimal price)
	{
		var longVolume = GetTotalVolume(_longPositions);
		if (longVolume > 0m)
		{
			ExecuteOrder(Sides.Sell, longVolume, price);
		}

		var shortVolume = GetTotalVolume(_shortPositions);
		if (shortVolume > 0m)
		{
			ExecuteOrder(Sides.Buy, shortVolume, price);
		}
	}

	private void ExecuteOrder(Sides side, decimal volume, decimal price)
	{
		if (volume <= 0m)
			return;

		Volume = volume;

		if (side == Sides.Buy)
			BuyMarket();
		else
			SellMarket();

		UpdatePositions(side, volume, price);
	}

	private void UpdatePositions(Sides side, decimal volume, decimal price)
	{
		if (volume <= 0m)
			return;

		if (side == Sides.Buy)
		{
			var remaining = volume;
			var index = 0;
			while (remaining > 0m && index < _shortPositions.Count)
			{
				var position = _shortPositions[index];
				var qty = Math.Min(position.Volume, remaining);
				position.Volume -= qty;
				remaining -= qty;
				if (position.Volume <= 0m)
				{
					_shortPositions.RemoveAt(index);
					continue;
				}

				index++;
			}

			if (remaining > 0m)
			{
				_longPositions.Add(new PositionRecord
				{
					Volume = remaining,
					EntryPrice = price
				});
			}
		}
		else
		{
			var remaining = volume;
			var index = 0;
			while (remaining > 0m && index < _longPositions.Count)
			{
				var position = _longPositions[index];
				var qty = Math.Min(position.Volume, remaining);
				position.Volume -= qty;
				remaining -= qty;
				if (position.Volume <= 0m)
				{
					_longPositions.RemoveAt(index);
					continue;
				}

				index++;
			}

			if (remaining > 0m)
			{
				_shortPositions.Add(new PositionRecord
				{
					Volume = remaining,
					EntryPrice = price
				});
			}
		}
	}

	private decimal CalculateOpenProfit(decimal currentPrice)
	{
		var profit = 0m;

		foreach (var position in _longPositions)
		{
			var diff = currentPrice - position.EntryPrice;
			profit += ConvertPriceToMoney(diff, position.Volume);
		}

		foreach (var position in _shortPositions)
		{
			var diff = position.EntryPrice - currentPrice;
			profit += ConvertPriceToMoney(diff, position.Volume);
		}

		return profit;
	}

	private decimal ConvertPriceToMoney(decimal priceDifference, decimal volume)
	{
		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = priceStep;

		if (priceStep <= 0m || stepPrice <= 0m)
			return priceDifference * volume;

		var steps = priceDifference / priceStep;
		return steps * stepPrice * volume;
	}

	private decimal GetStopLossDistance()
	{
		var pip = GetPipSize();
		return StopLossPips * pip;
	}

	private decimal GetTakeProfitDistance()
	{
		var pip = GetPipSize();
		return TakeProfitPips * pip;
	}

	private decimal GetPipSize()
	{
		var priceStep = Security?.PriceStep ?? 0m;
		if (priceStep <= 0m)
			return 1m;

		var step = priceStep;
		var digits = 0;
		while (step < 1m && digits < 10)
		{
			step *= 10m;
			digits++;
		}

		if (digits == 3 || digits == 5)
			return priceStep * 10m;

		return priceStep;
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (volume <= 0m)
			return 0m;

		var step = Security?.VolumeStep ?? 0m;
		if (step > 0m)
		{
			volume = Math.Floor(volume / step) * step;
			if (volume < step)
				return 0m;
		}

		return volume;
	}

	private static decimal GetTotalVolume(List<PositionRecord> positions)
	{
		var total = 0m;
		foreach (var position in positions)
			total += position.Volume;
		return total;
	}

	private bool IsWithinTradingHours(DateTime time)
	{
		var hour = time.Hour;
		return hour >= StartHour && hour <= EndHour;
	}

	private void ResetMartingale()
	{
		_multiplicationCount = 0;
		_currentVolume = AdjustVolume(InitialVolume);
	}
}