GitHub で見る

戦略 OpenPendingorderAfterPositionGetStopLoss

概要

OpenPendingorderAfterPositionGetStopLoss 戦略は、同じ名前の MetaTrader 5 エキスパート アドバイザーを StockSharp の高レベル API に移植します。選択した時間枠における Stochastic %K ラインの傾きを継続的に評価します。 %K が下落すると、市場より下で売りストップ注文を出し、%K が上昇すると、市場より上で買いストップ注文を出します。記入された各エントリーは、保護的なストップロスおよびテイクプロフィット注文を即座に受け取ります。ストップロスでポジションがクローズされると、戦略は対応する未決注文を自動的に再インストールするため、次のローソク足を待たずにブレイクアウト取引のグリッドが復元されます。

取引ルール

  • 構成されたタイムフレームの完了したローソク足をサブスクライブし、古典的な Stochastic オシレーター (KPeriodDPeriodSlowing) を計算します。
  • 現在の %K 値を 2 バー前の値と比較します。
    • %K(current) < %K(two bars ago) → 最高入札額を下回る売りストップを送信します。
    • %K(current) > %K(two bars ago) → ベストアスクを上回る買いストップを送信します。
  • 未決注文は、現在のスプレッドにユーザー定義の MinStopDistancePoints バッファを加えたものによって市場からオフセットされ、元の MQL ロジックと一致します。
  • 未決注文が約定すると、戦略は保護的なストップロス (ストップ注文) とオプションのテイクプロフィット (指値注文) を送信します。
  • 保護ストップロスが発動すると、対応する未決注文が最新の市場価格を使用して即座に再作成されます。
  • プロテクト注文は、テイクプロフィットによってポジションがクローズされるか、戦略が停止すると自動的にキャンセルされます。

パラメーター

名前 説明
OrderVolume 未決注文ごとのロット単位の取引量。
StopLossPoints シンボルポイントでのストップロス距離。無効にするには 0 に設定します。
TakeProfitPoints シンボルポイントでのテイクプロフィット距離。無効にするには 0 に設定します。
MinStopDistancePoints 未決注文を出す前にスプレッドに追加される最小価格バッファー (ポイント単位)。
MaxPositions 方向ごとの同時ポジションの最大数 (ネッティング口座は実質的に 0 または 1 を使用します)。
KPeriod %K 計算に使用されるバーの数。
DPeriod スムージング %D ラインの長さ。
Slowing 比較の前に追加の平滑化係数が %K に適用されます。
PendingExpiry 未決のストップ注文のオプションの存続期間。期限切れの注文は次のローソク足でキャンセルされます。
CandleType ローソク足のサブスクリプションとインジケーターの計算に使用される時間枠。

実装メモ

  • すべての注文管理は、AGENTS.md の要求に応じて、BuyStopSellStopSellLimitBuyLimit などの高レベルのヘルパーに依存します。
  • インジケーターの値は、SubscribeCandles().BindEx(...) コールバック内で直接消費され、GetValue の呼び出しは回避されます。
  • この戦略は、元の Expert Advisor の OnTradeTransaction ロジックをエミュレートして、MyTrade イベントを監視して保護命令をインストールおよび削除します。
  • コード内のコメントは英語で記述され、インデントはタブで行われ、リポジトリのガイドラインに準拠しています。
namespace StockSharp.Samples.Strategies;

using System;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.Messages;

/// <summary>
/// Trades based on the slope of the Stochastic %K line.
/// Buys when %K is rising, sells when %K is falling.
/// Uses take-profit and stop-loss protection.
/// </summary>
public class OpenPendingorderAfterPositionGetStopLossStrategy : Strategy
{
	private readonly StrategyParam<int> _kPeriod;
	private readonly StrategyParam<int> _dPeriod;
	private readonly StrategyParam<int> _slowing;
	private readonly StrategyParam<decimal> _stopLossPct;
	private readonly StrategyParam<decimal> _takeProfitPct;
	private readonly StrategyParam<int> _signalCooldownCandles;
	private readonly StrategyParam<DataType> _candleType;

	private decimal? _lastK;
	private decimal? _prevK;
	private decimal _entryPrice;
	private int _candlesSinceTrade;

	public int KPeriod
	{
		get => _kPeriod.Value;
		set => _kPeriod.Value = value;
	}

	public int DPeriod
	{
		get => _dPeriod.Value;
		set => _dPeriod.Value = value;
	}

	public int Slowing
	{
		get => _slowing.Value;
		set => _slowing.Value = value;
	}

	public decimal StopLossPct
	{
		get => _stopLossPct.Value;
		set => _stopLossPct.Value = value;
	}

	public decimal TakeProfitPct
	{
		get => _takeProfitPct.Value;
		set => _takeProfitPct.Value = value;
	}

	public int SignalCooldownCandles
	{
		get => _signalCooldownCandles.Value;
		set => _signalCooldownCandles.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public OpenPendingorderAfterPositionGetStopLossStrategy()
	{
		_kPeriod = Param(nameof(KPeriod), 22)
			.SetDisplay("%K Period", "Number of bars for %K", "Indicators");

		_dPeriod = Param(nameof(DPeriod), 7)
			.SetDisplay("%D Period", "Smoothing period for %K", "Indicators");

		_slowing = Param(nameof(Slowing), 2)
			.SetDisplay("Slowing", "Additional smoothing factor", "Indicators");

		_stopLossPct = Param(nameof(StopLossPct), 2m)
			.SetDisplay("Stop Loss %", "Stop-loss as percentage of entry price", "Risk");

		_takeProfitPct = Param(nameof(TakeProfitPct), 3m)
			.SetDisplay("Take Profit %", "Take-profit as percentage of entry price", "Risk");

		_signalCooldownCandles = Param(nameof(SignalCooldownCandles), 4)
			.SetGreaterThanZero()
			.SetDisplay("Signal Cooldown", "Bars to wait between entries", "Trading");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(60).TimeFrame())
			.SetDisplay("Candle Type", "Timeframe for indicator", "General");
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_lastK = null;
		_prevK = null;
		_entryPrice = 0;
		_candlesSinceTrade = SignalCooldownCandles;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);
		_lastK = null;
		_prevK = null;
		_entryPrice = 0;
		_candlesSinceTrade = SignalCooldownCandles;

		var rsi = new RelativeStrengthIndex { Length = KPeriod };

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

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

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

		var close = candle.ClosePrice;

		if (_candlesSinceTrade < SignalCooldownCandles)
			_candlesSinceTrade++;

		// Check stop-loss / take-profit on existing position
		if (Position != 0 && _entryPrice > 0)
		{
			if (Position > 0)
			{
				var pnlPct = (close - _entryPrice) / _entryPrice * 100m;
				if (pnlPct <= -StopLossPct || pnlPct >= TakeProfitPct)
				{
					SellMarket();
					_entryPrice = 0;
					_prevK = currentK;
					_lastK = currentK;
					return;
				}
			}
			else if (Position < 0)
			{
				var pnlPct = (_entryPrice - close) / _entryPrice * 100m;
				if (pnlPct <= -StopLossPct || pnlPct >= TakeProfitPct)
				{
					BuyMarket();
					_entryPrice = 0;
					_prevK = currentK;
					_lastK = currentK;
					return;
				}
			}
		}

		// Need at least 2 values to determine signal transition.
		if (_lastK is not decimal prevK)
		{
			_lastK = currentK;
			return;
		}

		_prevK = _lastK;
		_lastK = currentK;

		var crossedUp = prevK <= 45m && currentK > 45m;
		var crossedDown = prevK >= 55m && currentK < 55m;

		if (crossedUp && Position <= 0 && _candlesSinceTrade >= SignalCooldownCandles)
		{
			BuyMarket();
			_entryPrice = close;
			_candlesSinceTrade = 0;
		}
		else if (crossedDown && Position >= 0 && _candlesSinceTrade >= SignalCooldownCandles)
		{
			SellMarket();
			_entryPrice = close;
			_candlesSinceTrade = 0;
		}
	}
}