GitHub で見る

Invest System 4.5 戦略 (C#)

概要

Invest System 4.5 は、StockSharp 高レベル戦略 API に移植された MetaTrader 5 のエキスパートアドバイザーです。戦略は以前に完成した 4 時間足の方向に従って EUR/USD ペアを取引します。新しい 4 時間セッションの最初の数分間に 1 回のトレードが許可され、ポジションサイジングは実現したパフォーマンスと口座の成長に適応します。

コードは高レベル API のみに依存します:自動ローソク足サブスクリプションは 4 時間方向バイアスと低い時間軸のエントリーウィンドウの両方を監視するために使用され、組み込みの StartProtection ヘルパーは pips で表された静的テイクプロフィットとストップロスレベルを適用します。

取引ロジック

  1. 方向バイアス – 完成した 4 時間足のクローズごとに、戦略はローソク足が強気または弱気でクローズしたかを記録します。強気のローソク足は次のセッションのロングエントリーのみを有効にし、弱気のローソク足はショートのみを有効にします。ローソク足が開始価格と全く同じ価格でクローズした場合、前の方向が維持されます。
  2. エントリータイミング – 新しい 4 時間足が始まると、エントリーウィンドウが開きます。ウィンドウは設定可能な分数(デフォルト 15 分)有効です。戦略は低い時間軸のローソク足(デフォルト 1 分)を監視し、ウィンドウがアクティブな間にすべてのフィルターが満たされた場合に最大 1 件の成行注文を送信できます。
  3. 単一ポジション – 戦略は決してピラミッドを組みません。ポジションがすでに開いている場合、次の 4 時間セッションまで新しいシグナルは処理されません。注文が送信されると、MetaTrader の動作を再現するためにエントリーウィンドウは即座に閉じます。
  4. 損益追跡 – ポジションが完全にクローズされると、以下に説明する適応的なロットロジックを駆動するために実現 PnL が取得されます。

ポジションサイジングルール

元のエキスパートアドバイザーは二つの資金管理レイヤーを使用します:

  • 資本マイルストーン:初期口座残高は最初の更新で記録されます。資本が初期残高の 2×、3× … 6× を超えると、ベースロットサイズが比例して増加します。ステージ 1 は BaseLot から始まり、ステージ 2 はそれを 2 倍にし、ステージ 3 はそれを 3 倍にするなど。二次ロットサイズ(Lot2Lot3Lot4)は元の乗数(それぞれ ×2、×7、×14)を使用して導出されます。
  • プラン B エスカレーション:単一のグローバルボリューム値がトレード間で保持されます。
    • ベースロットでの負けトレードの後、ボリュームは第 2 のロット(Lot3)に引き上げられます。
    • 第 2 のロットで取引中にさらに損失が発生した場合、「プラン B」が有効になります。プラン B はベースロットが Lot2 になりアグレッシブロットが Lot4 になるように内部ロットオプションを再マッピングします。現在のボリュームはすぐには変更されませんが、その後の損失はすべて戦略をアグレッシブロットに押し込みます。口座が新しい資本の高値に達すると、プラン B は自動的にキャンセルされます。
    • 利益のあるトレードは常にアクティブなステージのベースロットに現在のボリュームをリセットします。 これらのルールは、注文を手動で反復したりコレクションを使用したりすることなく、MetaTrader バージョンのカスケードロットエスカレーションを忠実に再現します。

リスク管理

  • StartProtection は pip サイズから導出された絶対価格単位でストップロスとテイクプロフィットの両方を設定します。ストップとターゲットは戦略が開始されたときに一度だけ登録され、元の EA が各注文に値を添付するのと同様です。
  • 成行注文のみが使用されます。戦略自体はヘッジポジション、スケーリング、または部分的な出口を実行しません;出口は設定された保護注文を通じて発生します。

戦略パラメーター

パラメーター 説明 デフォルト 最適化範囲
StopLossPips pips 単位のストップロス距離。ストップを無効にするには 0 を使用。 240 120 – 360、ステップ 20
TakeProfitPips pips 単位のテイクプロフィット距離。ターゲットを無効にするには 0 を使用。 40 20 – 80、ステップ 10
EntryWindowMinutes 新しい 4 時間足が開いた後のエントリーウィンドウの長さ。 15 5 – 30、ステップ 5
SignalCandleType エントリーウィンドウを監視するために使用されるローソク足シリーズ(デフォルト 1 分)。 1 分足の時間軸
TrendCandleType 方向バイアスを構築するために使用される高い時間軸のローソク足(デフォルト 4 時間)。 4 時間足の時間軸
BaseLot ステージ 1 の初期ロットサイズ。他のロットサイズは自動的に導出されます。 0.1 0.05 – 0.3、ステップ 0.05

ファイル構造

2772_Invest_System_45/
├── CS/
│   └── InvestSystem45Strategy.cs
├── README.md
├── README_ru.md
└── README_zh.md

注意事項

  • 戦略は添付された証券が 4 時間足シリーズとより速い時間軸シリーズの両方を提供することを期待します。これらのサブスクリプションは OnStarted 内で自動的に作成されます。
  • pip サイズは Security.PriceStep から決定され、MetaTrader の pip 値の処理に合わせるために小数点以下 3 桁または 5 桁の引用(分数引用)に調整されます。
  • 元のロボットは口座残高の閾値を使用するため、StockSharp の実装は各エントリーローソク足の更新で Portfolio.CurrentValue を読み取ります。シミュレーションで実行する場合、ロットスケーリングが一貫性を保つようにポートフォリオモデルが現在の資本を更新することを確認してください。
  • 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;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Invest System 4.5 strategy converted from MetaTrader.
/// Trades in the direction of the previous 4-hour candle within the first minutes of the new session.
/// </summary>
public class InvestSystem45Strategy : Strategy
{
	private readonly StrategyParam<int> _stopLossPips;
	private readonly StrategyParam<int> _takeProfitPips;
	private readonly StrategyParam<int> _entryWindowMinutes;
	private readonly StrategyParam<DataType> _signalCandleType;
	private readonly StrategyParam<DataType> _trendCandleType;
	private readonly StrategyParam<decimal> _baseLot;

	private decimal _pipSize;
	private decimal _minBalance;
	private decimal _maxBalance;
	private int _lotStage;
	private bool _planBActive;

	private decimal _stageLot1;
	private decimal _stageLot2;
	private decimal _stageLot3;
	private decimal _stageLot4;
	private decimal _lotOption1;
	private decimal _lotOption2;
	private decimal _currentVolume;

	private bool _needsPostTradeAdjustment;
	private bool _hasOpenPosition;
	private decimal _pnlAtEntry;
	private decimal _lastTradePnL;

	private int _trendDirection;
	private DateTime? _entryWindowStart;
	private DateTime? _entryWindowEnd;
	private bool _entryWindowActive;

	private decimal _entryPrice;
	private decimal _stopPrice;
	private decimal _takePrice;

	/// <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>
	/// Minutes allowed for entries after a new trend candle opens.
	/// </summary>
	public int EntryWindowMinutes
	{
		get => _entryWindowMinutes.Value;
		set => _entryWindowMinutes.Value = value;
	}

	/// <summary>
	/// Candle type that drives entry timing.
	/// </summary>
	public DataType SignalCandleType
	{
		get => _signalCandleType.Value;
		set => _signalCandleType.Value = value;
	}

	/// <summary>
	/// Higher timeframe candle used to define trade direction.
	/// </summary>
	public DataType TrendCandleType
	{
		get => _trendCandleType.Value;
		set => _trendCandleType.Value = value;
	}

	/// <summary>
	/// Base lot size used to derive martingale steps.
	/// </summary>
	public decimal BaseLot
	{
		get => _baseLot.Value;
		set => _baseLot.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="InvestSystem45Strategy"/>.
	/// </summary>
	public InvestSystem45Strategy()
	{
		_stopLossPips = Param(nameof(StopLossPips), 240)
			.SetNotNegative()
			.SetDisplay("Stop Loss (pips)", "Stop loss distance in pips", "Risk")
			
			.SetOptimize(120, 360, 20);

		_takeProfitPips = Param(nameof(TakeProfitPips), 40)
			.SetNotNegative()
			.SetDisplay("Take Profit (pips)", "Take profit distance in pips", "Risk")
			
			.SetOptimize(20, 80, 10);

		_entryWindowMinutes = Param(nameof(EntryWindowMinutes), 15)
			.SetGreaterThanZero()
			.SetDisplay("Entry Window", "Minutes after 4H open when entries are allowed", "Timing")
			
			.SetOptimize(5, 30, 5);

		_signalCandleType = Param(nameof(SignalCandleType), TimeSpan.FromMinutes(5).TimeFrame())
			.SetDisplay("Signal Candles", "Candles used to time entries", "Timing");

		_trendCandleType = Param(nameof(TrendCandleType), TimeSpan.FromHours(4).TimeFrame())
			.SetDisplay("Trend Candles", "Higher timeframe candles for direction", "Timing");

		_baseLot = Param(nameof(BaseLot), 0.1m)
			.SetGreaterThanZero()
			.SetDisplay("Base Lot", "Starting lot size before scaling", "Risk")
			
			.SetOptimize(0.05m, 0.3m, 0.05m);
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		if (Security is null)
			yield break;

		yield return (Security, SignalCandleType);

		if (!SignalCandleType.Equals(TrendCandleType))
			yield return (Security, TrendCandleType);
	}

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

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

		ResetState();
		_pipSize = CalculatePipSize();
		// Recreate lot options according to current stage and plan mode.
		RecalculateLotOptions();

		var trendSubscription = SubscribeCandles(TrendCandleType);
		trendSubscription.Bind(ProcessTrendCandle).Start();

		var entrySubscription = SubscribeCandles(SignalCandleType);
		entrySubscription.Bind(ProcessEntryCandle).Start();

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

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

		if (Position != 0m)
		{
			// Record entry state to compute realized PnL later.
			if (!_hasOpenPosition)
			{
				_hasOpenPosition = true;
				_needsPostTradeAdjustment = true;
				_pnlAtEntry = PnL;
			}

			_entryWindowActive = false;
			return;
		}

		if (!_hasOpenPosition)
			return;

		_hasOpenPosition = false;
		_lastTradePnL = PnL - _pnlAtEntry;
		// Mirror MetaTrader profit calculation for Plan B rules.

		HandlePostTradeAdjustment();
	}

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

		// Store direction from the last completed 4H candle.
		if (candle.ClosePrice > candle.OpenPrice)
		{
			_trendDirection = 1;
		}
		else if (candle.ClosePrice < candle.OpenPrice)
		{
			_trendDirection = -1;
		}

		_entryWindowStart = candle.CloseTime;
		_entryWindowEnd = _entryWindowStart?.AddMinutes(EntryWindowMinutes);
		// Open a new entry window immediately at the next candle open.
		_entryWindowActive = true;
	}

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

		// Check SL/TP for open positions.
		if (Position > 0m && _entryPrice > 0m)
		{
			if (_stopPrice > 0m && candle.LowPrice <= _stopPrice)
			{
				SellMarket(Position);
				ResetTargets();
				return;
			}
			if (_takePrice > 0m && candle.HighPrice >= _takePrice)
			{
				SellMarket(Position);
				ResetTargets();
				return;
			}
		}
		else if (Position < 0m && _entryPrice > 0m)
		{
			if (_stopPrice > 0m && candle.HighPrice >= _stopPrice)
			{
				BuyMarket(Math.Abs(Position));
				ResetTargets();
				return;
			}
			if (_takePrice > 0m && candle.LowPrice <= _takePrice)
			{
				BuyMarket(Math.Abs(Position));
				ResetTargets();
				return;
			}
		}

		// Update balance-dependent scaling before evaluating signals.
		UpdateBalanceState();

		if (!_entryWindowActive || !_entryWindowStart.HasValue || !_entryWindowEnd.HasValue)
			return;

		var openTime = candle.OpenTime;
		if (openTime < _entryWindowStart.Value)
			return;

		if (openTime > _entryWindowEnd.Value)
		{
			_entryWindowActive = false;
			return;
		}

		if (_trendDirection == 0)
			return;

		if (Position != 0m)
			return;

		// Lazy initialize volume when strategy is ready.
		if (_currentVolume <= 0m)
			_currentVolume = _lotOption1;

		if (_currentVolume <= 0m)
			return;

		if (_trendDirection > 0)
		{
			BuyMarket(_currentVolume);
		}
		else
		{
			SellMarket(_currentVolume);
		}

		// Allow only one trade per 4H candle similar to MetaTrader logic.
		_entryWindowActive = false;
	}

	private void HandlePostTradeAdjustment()
	{
		if (!_needsPostTradeAdjustment)
			return;

		_needsPostTradeAdjustment = false;

		// Apply lot escalation rules after each closed trade.
		UpdateBalanceState();

		if (_lastTradePnL < 0m)
		{
			if (_currentVolume == _lotOption2 && !_planBActive)
			{
				_planBActive = true;
				RecalculateLotOptions();
			}
			else if (_currentVolume == _lotOption1)
			{
				_currentVolume = _lotOption2;
			}
			else
			{
				_currentVolume = _lotOption2;
			}
		}
		else if (_lastTradePnL > 0m)
		{
			_currentVolume = _lotOption1;
		}
	}

	private void UpdateBalanceState()
	{
		var balance = Portfolio?.CurrentValue;
		if (balance is null || balance.Value <= 0m)
			return;

		if (_minBalance <= 0m)
		{
			_minBalance = balance.Value;
			_maxBalance = balance.Value;
		}

		if (balance.Value > _maxBalance)
		{
			_maxBalance = balance.Value;
			if (_planBActive)
			{
				_planBActive = false;
				RecalculateLotOptions();
			}
		}

		var newStage = 1;
		if (_minBalance > 0m)
		{
			// Check for equity milestones to scale base lots.
			for (var stage = 6; stage >= 2; stage--)
			{
				if (balance.Value > _minBalance * stage)
				{
					newStage = stage;
					break;
				}
			}
		}

		if (newStage != _lotStage)
		{
			_lotStage = newStage;
			RecalculateLotOptions();
		}
	}

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

		if (decimals == 3 || decimals == 5)
			step *= 10m;

		return step;
	}

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

		if (Position != 0m && _entryPrice == 0m)
		{
			_entryPrice = trade.Trade.Price;
			var slDist = StopLossPips * _pipSize;
			var tpDist = TakeProfitPips * _pipSize;

			if (Position > 0m)
			{
				_stopPrice = slDist > 0m ? _entryPrice - slDist : 0m;
				_takePrice = tpDist > 0m ? _entryPrice + tpDist : 0m;
			}
			else
			{
				_stopPrice = slDist > 0m ? _entryPrice + slDist : 0m;
				_takePrice = tpDist > 0m ? _entryPrice - tpDist : 0m;
			}
		}

		if (Position == 0m)
			ResetTargets();
	}

	private void ResetTargets()
	{
		_entryPrice = 0m;
		_stopPrice = 0m;
		_takePrice = 0m;
	}

	private void ResetState()
	{
		_pipSize = 0m;
		_minBalance = 0m;
		_maxBalance = 0m;
		_lotStage = 1;
		_planBActive = false;
		_stageLot1 = 0m;
		_stageLot2 = 0m;
		_stageLot3 = 0m;
		_stageLot4 = 0m;
		_lotOption1 = 0m;
		_lotOption2 = 0m;
		_currentVolume = 0m;
		_needsPostTradeAdjustment = false;
		_hasOpenPosition = false;
		_pnlAtEntry = 0m;
		_lastTradePnL = 0m;
		_trendDirection = 0;
		_entryWindowStart = null;
		_entryWindowEnd = null;
		_entryWindowActive = false;
		_entryPrice = 0m;
		_stopPrice = 0m;
		_takePrice = 0m;
	}

	private void RecalculateLotOptions()
	{
		var baseLot = BaseLot * _lotStage;

		_stageLot1 = baseLot;
		_stageLot2 = baseLot * 2m;
		_stageLot3 = baseLot * 7m;
		_stageLot4 = baseLot * 14m;

		// Stage-specific lot multipliers replicate the original configuration.
		if (_planBActive)
		{
			_lotOption1 = _stageLot2;
			_lotOption2 = _stageLot4;
		}
		else
		{
			_lotOption1 = _stageLot1;
			_lotOption2 = _stageLot3;
		}

		if (_currentVolume <= 0m)
			_currentVolume = _lotOption1;
	}
}