GitHub で見る

少額デポジット向けMartin戦略

概要

この戦略はStockSharpで「Martin for small deposits」のアベレージングエキスパートを再現します。15本の完成した足を分析し、最新のクローズが14本前に記録されたクローズより低い場合(ロング)または高い場合(ショート)にのみポジションを開きます。すべての取引は高レベル戦略APIを使用してマーケットで実行され、ロジックは完成した足ごとに一度適用されます。

エントリーロジック

  • ローリングバッファが最後の15本の完成した足のクローズを保持します。
  • 開いているまたは保留中のポジションがない場合、戦略は最新のクローズを14本前のクローズと比較します。
  • 最新のクローズが低い場合、ロンググリッドが開始されます。高い場合、ショートグリッドが開始されます。
  • 最初の注文の取引ボリュームはInitial Volumeに等しくなります。同じ側の後続のエントリーはマーチンゲール乗数を使用し、インストゥルメントのボリュームステップに正規化されます。

ポジション管理

  • ポジションが存在する間、戦略は別のアベレージング取引を考慮する前にBars To Skip本の完成した足を待ちます。
  • 追加の注文は価格が現在の方向に対して少なくともStep (pips)(検出されたpipサイズを使用して価格単位に変換)動いた場合にのみ送信されます。
  • 各実行は内部統計を更新します:集計ボリューム、平均エントリー価格、最低(ロング)または最高(ショート)エントリー価格、最後のフィルの価格。
  • ボリュームはMax VolumeもExchangeで定義された最大ボリュームも超えません。正規化されたサイズが最小許容ボリュームを下回ると、注文はスキップされます。

エグジット条件

  • 未実現ネット利益(現在のクローズと平均エントリー価格の差にポジションボリュームを乗じたもの)がMin Profitを超えると、すべての未決済注文がフラット化されます。
  • **Take Profit (pips)**がゼロより大きく、価格が最後のエントリーから有利な方向にその距離に達すると、グリッド全体がクローズされます。
  • クローズ要求が追跡されます。エグジット注文が完全に埋まるまで新しい注文は送信されません。フラット状態に達した後、すべての内部カウンターがリセットされ、次のシグナルが新しいグリッドを開始します。

パラメーター

名前 デフォルト値 説明
Initial Volume 0.01 最初の取引の基本ロットサイズ。
Take Profit (pips) 65 完全なエグジットをトリガーする最後のフィルからのpipsでの距離。この確認を無効にするには0を使用してください。
Step (pips) 15 ポジションにアベレージングする前に必要な逆方向のpips移動。
Bars To Skip 45 アベレージング注文間に待機する完成した足の最小数。
Increase Factor 1.7 同じ側に新しい注文が追加されるたびに取引ボリュームに適用される乗数。
Max Volume 6 集計ボリュームの上限(市場制限による正規化前)。
Min Profit 10 ネット利益がこの額を超えたときにグリッド全体をクローズするために使用される利益目標。
Candle Type 1時間 足のサブスクリプションとシグナル計算に使用される時間軸。

実装メモ

  • pipサイズはSecurity.PriceStepと小数精度から導出されます。3桁または5桁で見積もられたインストゥルメントでは、コードはMQLのpipの概念に一致させるために価格ステップに10を乗じます。
  • 未実現利益は価格差から近似されており、元のエキスパートにあったスワップや手数料の調整は含まれません。
  • エグジット注文がアクティブな間は追加のアベレージング取引がスキップされ、元のMQLロジックの順次実行フローが保持されます。
  • **Step (pips)**がゼロの場合、戦略はアベレージングしません。**Take Profit (pips)**がゼロの場合、Min Profit条件のみがグリッドをクローズします。
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>
/// Martingale averaging strategy for small deposits.
/// </summary>
public class MartinForSmallDepositsStrategy : Strategy
{
	private readonly StrategyParam<decimal> _initialVolume;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _stepPips;
	private readonly StrategyParam<int> _barsToSkip;
	private readonly StrategyParam<decimal> _increaseFactor;
	private readonly StrategyParam<decimal> _maxVolume;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _positionVolume;
	private decimal _avgPrice;
	private decimal _extremePrice;
	private decimal _lastEntryPrice;
	private int _currentTradeCount;
	private int _currentDirection;
	private int _barsSinceLastEntry;
	private decimal _pendingOpenVolume;
	private int _pendingOpenDirection;
	private decimal _pendingCloseVolume;
	private int _pendingCloseDirection;
	private decimal _pipSize;
	private readonly decimal[] _closeHistory = new decimal[15];
	private int _closeHistoryCount;
	private int _latestIndex = -1;

	/// <summary>
	/// Initializes a new instance of the <see cref="MartinForSmallDepositsStrategy"/> class.
	/// </summary>
	public MartinForSmallDepositsStrategy()
	{
		_initialVolume = Param(nameof(InitialVolume), 0.01m)
			.SetDisplay("Initial Volume", "Base lot size for the first order", "Position Sizing")
			;

		_takeProfitPips = Param(nameof(TakeProfitPips), 200)
			.SetDisplay("Take Profit (pips)", "Take profit distance from the latest entry", "Risk")
			;

		_stepPips = Param(nameof(StepPips), 100)
			.SetDisplay("Step (pips)", "Adverse price move required to add a new trade", "Position Sizing")
			;

		_barsToSkip = Param(nameof(BarsToSkip), 100)
			.SetDisplay("Bars To Skip", "Number of finished candles to wait before averaging", "Timing")
			;

		_increaseFactor = Param(nameof(IncreaseFactor), 1.7m)
			.SetDisplay("Increase Factor", "Multiplier applied to the volume of each new order", "Position Sizing")
			;

		_maxVolume = Param(nameof(MaxVolume), 6m)
			.SetDisplay("Max Volume", "Maximum allowed aggregated volume", "Risk")
			;

		_minProfit = Param(nameof(MinProfit), 10m)
			.SetDisplay("Min Profit", "Net profit threshold to close all positions", "Risk")
			;

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe used for signal generation", "General");
	}

	/// <summary>
	/// Base lot size for the first trade in the sequence.
	/// </summary>
	public decimal InitialVolume
	{
		get => _initialVolume.Value;
		set => _initialVolume.Value = value;
	}

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

	/// <summary>
	/// Price move in pips that triggers an averaging order.
	/// </summary>
	public int StepPips
	{
		get => _stepPips.Value;
		set => _stepPips.Value = value;
	}

	/// <summary>
	/// Number of candles to wait between additional averaging trades.
	/// </summary>
	public int BarsToSkip
	{
		get => _barsToSkip.Value;
		set => _barsToSkip.Value = value;
	}

	/// <summary>
	/// Multiplier for the martingale position sizing.
	/// </summary>
	public decimal IncreaseFactor
	{
		get => _increaseFactor.Value;
		set => _increaseFactor.Value = value;
	}

	/// <summary>
	/// Maximum allowed aggregated volume across all open trades.
	/// </summary>
	public decimal MaxVolume
	{
		get => _maxVolume.Value;
		set => _maxVolume.Value = value;
	}

	/// <summary>
	/// Profit target that closes the whole grid.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// Candle type used to build signals.
	/// </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();

		_positionVolume = 0m;
		_avgPrice = 0m;
		_extremePrice = 0m;
		_lastEntryPrice = 0m;
		_currentTradeCount = 0;
		_currentDirection = 0;
		_barsSinceLastEntry = 0;
		_pendingOpenVolume = 0m;
		_pendingOpenDirection = 0;
		_pendingCloseVolume = 0m;
		_pendingCloseDirection = 0;
		_pipSize = 0m;
		Array.Clear(_closeHistory, 0, _closeHistory.Length);
		_closeHistoryCount = 0;
		_latestIndex = -1;
	}

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

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

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

		// No bound indicators - skip formation check

		UpdateCloseHistory(candle.ClosePrice);

		var pipSize = EnsurePipSize();
		if (pipSize <= 0m)
			return;

		var stepDistance = StepPips > 0 ? StepPips * pipSize : 0m;
		var takeProfitDistance = TakeProfitPips > 0 ? TakeProfitPips * pipSize : 0m;

		var hasPosition = _positionVolume > 0m || Position != 0m || _pendingOpenDirection != 0 || _pendingCloseDirection != 0;

		if (!hasPosition)
		{
			if (!IsHistoryReady())
				return;

			var referenceClose = GetReferenceClose();
			if (candle.ClosePrice < referenceClose)
			{
				TryOpenBuy(candle.ClosePrice);
			}
			else if (candle.ClosePrice > referenceClose)
			{
				TryOpenSell(candle.ClosePrice);
			}

			return;
		}

		if (_pendingCloseDirection != 0)
			return;

		if (_positionVolume <= 0m || _currentDirection == 0)
			return;

		_barsSinceLastEntry++;

		var price = candle.ClosePrice;
		var openPnL = CalculateOpenProfit(price);

		if (openPnL > MinProfit)
		{
			CloseAllPositions();
			return;
		}

		if (_currentDirection > 0)
		{
			if (takeProfitDistance > 0m && price >= _lastEntryPrice + takeProfitDistance)
			{
				CloseAllPositions();
				return;
			}

			if (_barsSinceLastEntry <= BarsToSkip)
				return;

			if (stepDistance > 0m && _extremePrice - price > stepDistance)
				TryOpenBuy(price);
		}
		else if (_currentDirection < 0)
		{
			if (takeProfitDistance > 0m && price <= _lastEntryPrice - takeProfitDistance)
			{
				CloseAllPositions();
				return;
			}

			if (_barsSinceLastEntry <= BarsToSkip)
				return;

			if (stepDistance > 0m && price - _extremePrice > stepDistance)
				TryOpenSell(price);
		}
	}

	private void TryOpenBuy(decimal price)
	{
		if (_pendingOpenDirection != 0 && _pendingOpenDirection != 1)
			return;

		var volume = GetNextVolume(1);
		if (volume <= 0m)
			return;

		BuyMarket(volume);
		_pendingOpenDirection = 1;
		_pendingOpenVolume += volume;
	}

	private void TryOpenSell(decimal price)
	{
		if (_pendingOpenDirection != 0 && _pendingOpenDirection != -1)
			return;

		var volume = GetNextVolume(-1);
		if (volume <= 0m)
			return;

		SellMarket(volume);
		_pendingOpenDirection = -1;
		_pendingOpenVolume += volume;
	}

	private void CloseAllPositions()
	{
		if (_pendingCloseDirection != 0)
			return;

		var volume = Position;

		if (volume > 0m)
		{
			SellMarket(volume);
			_pendingCloseDirection = -1;
			_pendingCloseVolume += volume;
		}
		else if (volume < 0m)
		{
			var closeVolume = -volume;
			BuyMarket(closeVolume);
			_pendingCloseDirection = 1;
			_pendingCloseVolume += closeVolume;
		}
		else if (_positionVolume > 0m)
		{
			if (_currentDirection > 0)
			{
				SellMarket(_positionVolume);
				_pendingCloseDirection = -1;
				_pendingCloseVolume += _positionVolume;
			}
			else if (_currentDirection < 0)
			{
				BuyMarket(_positionVolume);
				_pendingCloseDirection = 1;
				_pendingCloseVolume += _positionVolume;
			}
		}
	}

	/// <inheritdoc />
	protected override void OnOwnTradeReceived(MyTrade trade)
	{
		if (trade.Order == null)
			return;

		var price = trade.Trade.Price;
		var volume = trade.Trade.Volume;

		if (trade.Order.Side == Sides.Buy)
		{
			if (_pendingCloseDirection == 1)
			{
				ApplyClose(volume);
				_pendingCloseVolume -= volume;
				if (_pendingCloseVolume <= 0m)
					_pendingCloseDirection = 0;
				return;
			}

			if (_pendingOpenDirection == 1)
			{
				ApplyLongOpen(price, volume);
				_pendingOpenVolume -= volume;
				if (_pendingOpenVolume <= 0m)
					_pendingOpenDirection = 0;
				return;
			}

			if (_currentDirection < 0)
				ApplyClose(volume);
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			if (_pendingCloseDirection == -1)
			{
				ApplyClose(volume);
				_pendingCloseVolume -= volume;
				if (_pendingCloseVolume <= 0m)
					_pendingCloseDirection = 0;
				return;
			}

			if (_pendingOpenDirection == -1)
			{
				ApplyShortOpen(price, volume);
				_pendingOpenVolume -= volume;
				if (_pendingOpenVolume <= 0m)
					_pendingOpenDirection = 0;
				return;
			}

			if (_currentDirection > 0)
				ApplyClose(volume);
		}
	}

	private void ApplyLongOpen(decimal price, decimal volume)
	{
		var previousVolume = _positionVolume;
		_positionVolume += volume;
		_avgPrice = previousVolume == 0m ? price : ((_avgPrice * previousVolume) + (price * volume)) / _positionVolume;
		_extremePrice = previousVolume == 0m ? price : Math.Min(_extremePrice, price);
		_lastEntryPrice = price;
		_currentDirection = 1;
		_currentTradeCount++;
		_barsSinceLastEntry = 0;
	}

	private void ApplyShortOpen(decimal price, decimal volume)
	{
		var previousVolume = _positionVolume;
		_positionVolume += volume;
		_avgPrice = previousVolume == 0m ? price : ((_avgPrice * previousVolume) + (price * volume)) / _positionVolume;
		_extremePrice = previousVolume == 0m ? price : Math.Max(_extremePrice, price);
		_lastEntryPrice = price;
		_currentDirection = -1;
		_currentTradeCount++;
		_barsSinceLastEntry = 0;
	}

	private void ApplyClose(decimal volume)
	{
		_positionVolume -= volume;
		if (_positionVolume <= 0m)
		{
			ResetPositionState();
		}
	}

	private void ResetPositionState()
	{
		_positionVolume = 0m;
		_avgPrice = 0m;
		_extremePrice = 0m;
		_lastEntryPrice = 0m;
		_currentTradeCount = 0;
		_currentDirection = 0;
		_barsSinceLastEntry = 0;
		_pendingOpenDirection = 0;
		_pendingOpenVolume = 0m;
		_pendingCloseDirection = 0;
		_pendingCloseVolume = 0m;
	}

	private decimal CalculateOpenProfit(decimal price)
	{
		if (_currentDirection > 0)
			return (price - _avgPrice) * _positionVolume;

		if (_currentDirection < 0)
			return (_avgPrice - price) * _positionVolume;

		return 0m;
	}

	private decimal GetNextVolume(int direction)
	{
		var baseVolume = InitialVolume;
		if (baseVolume <= 0m)
			return 0m;

		var depth = _currentDirection == direction ? _currentTradeCount : 0;
		decimal factor;
		if (IncreaseFactor <= 0m || depth == 0)
		{
			factor = 1m;
		}
		else
		{
			var raw = Math.Pow((double)IncreaseFactor, depth);
			if (double.IsInfinity(raw) || double.IsNaN(raw) || raw > (double)decimal.MaxValue)
				return 0m;
			factor = (decimal)raw;
		}
		var volume = baseVolume * factor;

		if (MaxVolume > 0m && volume > MaxVolume)
			volume = MaxVolume;

		volume = NormalizeVolume(volume);

		return volume;
	}

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

		var security = Security;
		if (security == null)
			return 0m;

		if (security.VolumeStep is decimal step && step > 0m)
		{
			var steps = decimal.Truncate(volume / step);
			volume = steps * step;
		}

		if (security.MinVolume is decimal min && volume < min)
			return 0m;

		if (security.MaxVolume is decimal max && volume > max)
			volume = max;

		return volume;
	}

	private decimal EnsurePipSize()
	{
		if (_pipSize > 0m)
			return _pipSize;

		var security = Security;
		if (security == null)
			return 0m;

		var step = security.PriceStep ?? 0m;
		if (step == 0m)
		{
			var decimals = security.Decimals;
			if (decimals != null)
			{
				step = (decimal)Math.Pow(10, -decimals.Value);
			}
		}

		if (step == 0m)
			step = 0.01m;

		var decimalsCount = security.Decimals ?? 0;
		_pipSize = (decimalsCount == 3 || decimalsCount == 5) ? step * 10m : step;

		if (_pipSize == 0m)
			_pipSize = step > 0m ? step : 0.01m;

		return _pipSize;
	}

	private void UpdateCloseHistory(decimal closePrice)
	{
		if (_closeHistory.Length == 0)
			return;

		_latestIndex = (_latestIndex + 1) % _closeHistory.Length;
		_closeHistory[_latestIndex] = closePrice;

		if (_closeHistoryCount < _closeHistory.Length)
			_closeHistoryCount++;
	}

	private bool IsHistoryReady()
	{
		return _closeHistoryCount >= _closeHistory.Length;
	}

	private decimal GetReferenceClose()
	{
		var index = (_latestIndex + 1) % _closeHistory.Length;
		return _closeHistory[index];
	}
}