GitHub で見る

オープンクローズ2アンプStochastic戦略

概要

  • MetaTrader 4 エキスパート open_close2ampnstochastic_strategy のポートは、StockSharp のハイレベル API の上に再構築されました。
  • 古典的な Stochastic オシレーター (長さ 9、スムージング 3/3) と 2 本のバーの価格アクション フィルターを併用します。注文が送信される前に、現在のローソク足は前のローソク足の方向を継続する必要があります。
  • シングルポジション取引向けに設計されています。デフォルトのローソク足ソースは 1 時間ですが、CandleType パラメータを通じて任意の時間枠を接続できます。

信号ロジック

  1. エントリーガード – 一度にオープンできるポジションは 1 つだけです。戦略がフラットの場合、最後に完全に形成されたローソク足を評価します。
    • ロングエントリー Stochastic メインラインがシグナルラインより上にあり、かつ 最新のローソク足の始値と終値の両方が以前の値を下回っている場合 (下降圧力の継続とそれに続くオシレーターの強さ)。
    • ショートエントリー Stochastic メインラインがシグナルラインの下にあり、 かつ ローソク足が前の値よりも高い始値と終値を示している場合 (弱気オシレーターの確認で上向きに押し上げられる)。
  2. Exit ルール – ポジションが存在する間、同じ条件が逆に反映されます。
    • **メインラインがシグナルラインを下回り、新しいローソク足がより高い始値/終値を印刷する場合、ロングクローズ
    • メインラインがシグナルラインを上回り、新しいローソク足が始値/終値を下げるとショートで終了**。
  3. ドローダウン ガード – MT4 の緊急出口を再現します。浮動損失の大きさ (実現損益 + 現在のローソクベースの推定値) が MaximumRisk × account_margin に達すると、ストラテジーはポジションを直ちに清算します。 StockSharp は MetaTrader の AccountMargin を公開しないため、ポートは Portfolio.BlockedValue 経由で近似し、ブロックされたマージンが利用できない場合は Portfolio.CurrentValue にフォールバックします。

資金管理

  • BaseVolume は元の Lots 入力をミラーリングし、アカウント情報が利用できない場合に使用されます。
  • ポートフォリオ評価が存在する場合、生の注文サイズは Portfolio.CurrentValue × MaximumRisk / 1000 になり、元の AccountFreeMargin ベースのサイズ設定と一致します。
  • トレードに負けるたびに、次のポジションは losses / DecreaseFactor ずつ減ります。ストリークカウンターは利益のある取引後にリセットされます。結果のサイズは、MinimumVolume を下回ることはできません。デフォルトは、MQL スクリプトと同様に 0.1 ロットです。
  • 計算されたすべての出来高は、成行注文を送信する前に商品制限 (VolumeStepMinVolumeMaxVolume) に合わせて調整されます。

パラメーター

名前 種類 デフォルト 説明
BaseVolume 10進数 0.1 リスクベースのサイジングを計算できない場合のフォールバック注文サイズ。
MaximumRisk 10進数 0.3 動的サイジングとドローダウンガードの両方に使用される資本の割合。リスク計算を無効にするには、0 に設定します。
DecreaseFactor 10進数 100 連続負け後に適用される除数。値を大きくすると、減少が遅くなります。
MinimumVolume 10進数 0.1 計算された体積の絶対床。
StochasticLength 整数 9 Stochastic オシレーターのルックバック期間。
StochasticKLength 整数 3 %K ラインの平滑化期間。
StochasticDLength 整数 3 %D 信号線の平滑化期間。
CandleType DataType TimeFrame(1h) インジケーターと価格フィルターを駆動するために使用されるローソク足のソース。

実装メモ

  • 非常口に必要な変動損益は、最新のローソク足の終値と Strategy.PositionPrice で推定されます。これは、MetaTrader の AccountProfit の意図を反映していますが、実際のブローカー側の計算は異なる場合があります。
  • ブロックされた証拠金もポートフォリオ値もコネクタによって公開されていない場合、戦略が BaseVolume を使用して取引を行っている間、ドローダウン ガードはアイドル状態のままになります。
  • StartProtection() は起動時に有効になるため、StockSharp の保護メカニズム (ルーティングの停止/取得、再接続) は、MQL バージョンに存在するセーフティ ネットを反映します。

オリジナルエキスパートとの違い

  • MetaTrader ロットの丸めは、StockSharp を通じて利用可能な商品メタデータを使用してエミュレートされます。ポジションのサイジングがブローカーの制約と一致するように、取引された証券の VolumeStep/MinVolume の値を確認します。
  • MT4 コードは、Volume[0] でガードしながらティックごとに評価しました。ポートは完了したローソク足のみを処理するため、シグナルの重複が防止され、StockSharp 戦略に推奨されるパターンです。
  • アカウントの指標は近似値です。厳格な証拠金制限に依存している場合は、ブローカーの正確な計算式と一致するように MaximumRisk を調整するか、ガードをオーバーライドしてください。
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>
/// Port of the MetaTrader strategy open_close2ampnstochastic_strategy.
/// Replicates the price-action filters combined with a Stochastic crossover and includes the original money management rules.
/// </summary>
public class OpenClose2AmpnStochasticStrategy : Strategy
{
	private readonly StrategyParam<decimal> _baseVolume;
	private readonly StrategyParam<decimal> _maximumRisk;
	private readonly StrategyParam<decimal> _decreaseFactor;
	private readonly StrategyParam<decimal> _minimumVolume;
	private readonly StrategyParam<int> _stochasticLength;
	private readonly StrategyParam<int> _stochasticKLength;
	private readonly StrategyParam<int> _stochasticDLength;
	private readonly StrategyParam<DataType> _candleType;

	private StochasticOscillator _stochastic = null!;

	private decimal? _previousOpen;
	private decimal? _previousClose;

	private decimal _averageEntryPrice;
	private decimal _entryVolume;
	private int _entryDirection;
	private int _lossStreak;

	/// <summary>
	/// Base position size used when portfolio data is unavailable.
	/// </summary>
	public decimal BaseVolume
	{
		get => _baseVolume.Value;
		set => _baseVolume.Value = value;
	}

	/// <summary>
	/// Fraction of account value used for risk sizing and the drawdown guard.
	/// </summary>
	public decimal MaximumRisk
	{
		get => _maximumRisk.Value;
		set => _maximumRisk.Value = value;
	}

	/// <summary>
	/// Scaling factor that reduces position size after consecutive losing trades.
	/// </summary>
	public decimal DecreaseFactor
	{
		get => _decreaseFactor.Value;
		set => _decreaseFactor.Value = value;
	}

	/// <summary>
	/// Minimum tradable volume that mirrors the original 0.1 lot floor.
	/// </summary>
	public decimal MinimumVolume
	{
		get => _minimumVolume.Value;
		set => _minimumVolume.Value = value;
	}

	/// <summary>
	/// Stochastic oscillator look-back period.
	/// </summary>
	public int StochasticLength
	{
		get => _stochasticLength.Value;
		set => _stochasticLength.Value = value;
	}

	/// <summary>
	/// %K smoothing period of the Stochastic oscillator.
	/// </summary>
	public int StochasticKLength
	{
		get => _stochasticKLength.Value;
		set => _stochasticKLength.Value = value;
	}

	/// <summary>
	/// %D smoothing period of the Stochastic oscillator.
	/// </summary>
	public int StochasticDLength
	{
		get => _stochasticDLength.Value;
		set => _stochasticDLength.Value = value;
	}

	/// <summary>
	/// Candle type used for indicator calculations.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public OpenClose2AmpnStochasticStrategy()
	{
		_baseVolume = Param(nameof(BaseVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Base Volume", "Fallback order volume when risk sizing is unavailable", "Money Management");

		_maximumRisk = Param(nameof(MaximumRisk), 0.3m)
		.SetNotNegative()
		.SetDisplay("Maximum Risk", "Fraction of equity used for sizing and the drawdown guard", "Money Management");

		_decreaseFactor = Param(nameof(DecreaseFactor), 100m)
		.SetNotNegative()
		.SetDisplay("Decrease Factor", "Divisor applied after losing trades to shrink the next position", "Money Management");

		_minimumVolume = Param(nameof(MinimumVolume), 0.1m)
		.SetGreaterThanZero()
		.SetDisplay("Minimum Volume", "Lowest volume allowed after money management adjustments", "Money Management");

		_stochasticLength = Param(nameof(StochasticLength), 9)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic Length", "Number of periods used by the Stochastic oscillator", "Indicators");

		_stochasticKLength = Param(nameof(StochasticKLength), 3)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %K", "Smoothing applied to the %K line", "Indicators");

		_stochasticDLength = Param(nameof(StochasticDLength), 3)
		.SetGreaterThanZero()
		.SetDisplay("Stochastic %D", "Smoothing applied to the %D signal line", "Indicators");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
		.SetDisplay("Candle Type", "Time-frame used for processing", "General");
	}

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

		_previousOpen = null;
		_previousClose = null;
		_averageEntryPrice = 0m;
		_entryVolume = 0m;
		_entryDirection = 0;
		_lossStreak = 0;
	}

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

		// Build the Stochastic oscillator that mirrors the original (9,3,3) setup.
		_stochastic = new StochasticOscillator
		{
			K = { Length = StochasticKLength },
			D = { Length = StochasticDLength },
		};

		// Subscribe to candle data and bind the indicator values.
		var subscription = SubscribeCandles(CandleType);
		subscription
		.BindEx(_stochastic, ProcessCandle)
		.Start();

		// Draw indicator data if a chart is available.
		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawIndicator(area, _stochastic);
			DrawOwnTrades(area);
		}

		// Enable built-in protection helpers (stop orders, etc.).
		StartProtection(null, null);
	}

	private void ProcessCandle(ICandleMessage candle, IIndicatorValue stochasticValue)
	{
		// Process signals only once per finished candle.
		if (candle.State != CandleStates.Finished)
			return;

		if (!stochasticValue.IsFinal)
			return;

		var stochastic = (StochasticOscillatorValue)stochasticValue;
		if (stochastic.K is not decimal main || stochastic.D is not decimal signal)
			return;

		// Evaluate the emergency drawdown guard before new signals.
		if (Position != 0m && ApplyRiskGuard(candle.ClosePrice))
		{
			UpdatePreviousPrices(candle);
			return;
		}

		var previousOpen = _previousOpen;
		var previousClose = _previousClose;

		var canTrade = IsFormedAndOnlineAndAllowTrading();

		if (previousOpen is decimal prevOpen && previousClose is decimal prevClose)
		{
			if (Position == 0m && canTrade)
			{
				var longSignal = main > signal && candle.OpenPrice < prevOpen && candle.ClosePrice < prevClose;
				var shortSignal = main < signal && candle.OpenPrice > prevOpen && candle.ClosePrice > prevClose;

				if (longSignal)
				{
					var volume = CalculateTradeVolume(candle.ClosePrice);
					if (volume > 0m)
					{
						BuyMarket(volume);
						LogInfo($"Enter long: main={main:F2}, signal={signal:F2}, open={candle.OpenPrice}, close={candle.ClosePrice}, volume={volume}");
					}
				}
				else if (shortSignal)
				{
					var volume = CalculateTradeVolume(candle.ClosePrice);
					if (volume > 0m)
					{
						SellMarket(volume);
						LogInfo($"Enter short: main={main:F2}, signal={signal:F2}, open={candle.OpenPrice}, close={candle.ClosePrice}, volume={volume}");
					}
				}
			}
			else if (Position > 0m)
			{
				var exitLong = main < signal && candle.OpenPrice > prevOpen && candle.ClosePrice > prevClose;
				if (exitLong)
				{
					ClosePosition(candle.ClosePrice);
					LogInfo($"Exit long: main={main:F2}, signal={signal:F2}");
				}
			}
			else if (Position < 0m)
			{
				var exitShort = main > signal && candle.OpenPrice < prevOpen && candle.ClosePrice < prevClose;
				if (exitShort)
				{
					ClosePosition(candle.ClosePrice);
					LogInfo($"Exit short: main={main:F2}, signal={signal:F2}");
				}
			}
		}

		UpdatePreviousPrices(candle);
	}

	private bool ApplyRiskGuard(decimal closePrice)
	{
		if (MaximumRisk <= 0m)
			return false;

		var floatingPnL = CalculateFloatingPnL(closePrice);
		if (floatingPnL >= 0m)
			return false;

		var marginBase = GetMarginBase();
		if (marginBase <= 0m)
			return false;

		var limit = marginBase * MaximumRisk;
		if (Math.Abs(floatingPnL) < limit)
			return false;

		LogInfo($"Risk guard triggered: floatingPnL={floatingPnL}, limit={limit}. Closing position.");
		ClosePosition(closePrice);
		return true;
	}

	private decimal CalculateTradeVolume(decimal price)
	{
		var volume = BaseVolume;

		// Derive the lot size from account value similar to the original EA.
		var accountValue = Portfolio?.CurrentValue;
		if (accountValue is decimal value && value > 0m && price > 0m && MaximumRisk > 0m)
		{
			var riskVolume = Math.Round(value * MaximumRisk / 1000m, 2, MidpointRounding.AwayFromZero);
			if (riskVolume > 0m)
				volume = riskVolume;
		}

		// Apply loss streak reduction once at least two losses occurred, matching the MT4 script.
		if (DecreaseFactor > 0m && _lossStreak > 1)
		{
			var reduction = volume * _lossStreak / DecreaseFactor;
			volume -= reduction;
		}

		if (volume < MinimumVolume)
			volume = MinimumVolume;

		return AdjustVolume(volume);
	}

	private decimal AdjustVolume(decimal volume)
	{
		if (Security is null)
			return volume;

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

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

		var maxVolume = Security.MaxVolume;
		if (maxVolume.HasValue && maxVolume.Value > 0m && volume > maxVolume.Value)
			volume = maxVolume.Value;

		return volume;
	}

	private void ClosePosition(decimal closePrice)
	{
		var volume = Math.Abs(Position);
		if (volume <= 0m)
		{
			ResetEntryState();
			return;
		}

		if (Position > 0m)
			SellMarket(volume);
		else
			BuyMarket(volume);

		// Estimate profit using the stored average entry price.
		if (_entryDirection != 0 && _averageEntryPrice > 0m)
		{
			var profit = _entryDirection > 0 ? closePrice - _averageEntryPrice : _averageEntryPrice - closePrice;
			if (profit < 0m)
				_lossStreak++;
			else if (profit > 0m)
				_lossStreak = 0;
		}

		ResetEntryState();
	}

	private void UpdatePreviousPrices(ICandleMessage candle)
	{
		_previousOpen = candle.OpenPrice;
		_previousClose = candle.ClosePrice;
	}

	private decimal CalculateFloatingPnL(decimal price)
	{
		if (Position == 0m)
			return 0m;

		var entryPrice = _averageEntryPrice;
		if (entryPrice == 0m)
			return 0m;

		var priceMove = price - entryPrice;
		var priceStep = Security?.PriceStep ?? 0m;
		var stepPrice = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? 0m;

		if (priceStep > 0m && stepPrice > 0m)
		{
			var steps = priceMove / priceStep;
			return steps * stepPrice * Position;
		}

		return priceMove * Position;
	}

	private decimal GetMarginBase()
	{
		if (Portfolio == null)
			return 0m;

		if (Portfolio.BlockedValue is decimal blocked && blocked > 0m)
			return blocked;

		if (Portfolio.CurrentValue is decimal value && value > 0m)
			return value;

		return 0m;
	}

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

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

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

		if (trade.Order.Side == Sides.Buy)
		{
			if (Position > 0m)
			{
				RegisterEntry(price, volume, 1);
			}
			else if (Position == 0m && _entryDirection == -1)
			{
				EvaluateClosedTrade(price);
			}
		}
		else if (trade.Order.Side == Sides.Sell)
		{
			if (Position < 0m)
			{
				RegisterEntry(price, volume, -1);
			}
			else if (Position == 0m && _entryDirection == 1)
			{
				EvaluateClosedTrade(price);
			}
		}
	}

	private void RegisterEntry(decimal price, decimal volume, int direction)
	{
		if (volume <= 0m)
			return;

		if (_entryDirection != direction)
		{
			_entryDirection = direction;
			_averageEntryPrice = price;
			_entryVolume = volume;
			return;
		}

		var totalVolume = _entryVolume + volume;
		if (totalVolume <= 0m)
		{
			ResetEntryState();
			return;
		}

		_averageEntryPrice = (_averageEntryPrice * _entryVolume + price * volume) / totalVolume;
		_entryVolume = totalVolume;
	}

	private void EvaluateClosedTrade(decimal exitPrice)
	{
		if (_entryDirection == 0 || _averageEntryPrice <= 0m)
		{
			ResetEntryState();
			return;
		}

		var profit = _entryDirection > 0 ? exitPrice - _averageEntryPrice : _averageEntryPrice - exitPrice;
		if (profit < 0m)
		{
			_lossStreak++;
		}
		else if (profit > 0m)
		{
			_lossStreak = 0;
		}

		ResetEntryState();
	}

	private void ResetEntryState()
	{
		_averageEntryPrice = 0m;
		_entryVolume = 0m;
		_entryDirection = 0;
	}
}