GitHub で見る

Autotrade 保留ストップ戦略

概要

この戦略はMetaTraderのエキスパートアドバイザー*Autotrade(barabashkakvn版)*のC#変換です。現在の市場価格周りに2つの対称的なストップエントリー注文を継続的に維持します。市場がフラット状態を保ち、ポジションが開いていない場合、戦略は両方の保留注文を更新します。ストップ注文が執行されると、ポジションは積極的に監視されます:価格行動が安定するか、絶対的な利益/損失しきい値に達すると決済がトリガーされます。実装はプロジェクトガイドラインで要求されているようにStockSharpの高レベルAPIを使用します。

オリジナルパラメーターとの対応

StockSharpパラメーター MQL5パラメーター 説明
IndentTicks InpIndent 現在の価格とストップエントリー注文の間の距離(価格ステップ単位)。
MinProfit MinProfit 静かな市場フェーズ中に決済するために必要な最小浮動利益(口座通貨)。
ExpirationMinutes ExpirationMinutes 保留ストップ注文がキャンセルされ再作成されるまでの有効期間。
AbsoluteFixation AbsoluteFixation ポジションのクローズを強制する絶対的な利益または損失レベル(通貨)。
StabilizationTicks InpStabilization 統合ゾーンとして扱われる前の足のボディの最大サイズ。
OrderVolume Lots バイストップとセルストップの両方に使用されるボリューム。
CandleType Period() ロジックを動かすローソク足シリーズ(デフォルトは1分時間軸)。

価格距離を表すすべての数値入力は、Security.PriceStep値を通じて「ポイント」から実際の価格ステップに変換されます。利益ベースのしきい値はSecurity.StepPriceを使用して計算され、デポジット通貨で動作するMQL利益計算を反映します。

トレーディングロジック

保留注文の展開

  1. 戦略は完成したローソク足(CandleStates.Finished)にのみ反応します。
  2. 最初のローソク足は履歴データ(前回のopen/close)を蓄積するために使用され、すぐに保留注文をスケジュールします。
  3. ポジションが開いていない場合、非アクティブな参照がクリアされ:
    • Close + IndentTicks * PriceStepでバイストップが配置されます。
    • Close - IndentTicks * PriceStepでセルストップが配置されます。
  4. 各保留注文はCloseTime + ExpirationMinutes分の有効期限タイムスタンプを受け取ります。その時間に達すると注文はキャンセルされ、次のローソク足で再作成されます。

ポジション管理

  1. いずれかのストップ注文が執行されると、StockSharpのネッティングベースの口座モデルでの望ましくないヘッジングを避けるために、反対の保留注文がキャンセルされます。
  2. 戦略は前のローソク足のボディ(|Open - Close|)を保存して、静かな市場状態を検出します。
  3. オープンポジションがある各ローソク足で:
    • 未実現利益はPositionAvgPriceとの価格差をSecurity.PriceStepSecurity.StepPriceでスケーリングして通貨で推定されます。
    • 利益がMinProfitを超えかつ前のローソク足のボディがStabilizationTicks * PriceStepを下回っている場合、ポジションは成行でクローズされます。
    • 安定化に関わらず、絶対的な利益または損失がAbsoluteFixationを超えた場合もポジションは成行でクローズされます。
  4. ポジションがフラットに戻ると、残りのすべての保留注文が削除されます。

追加の動作

  • 一度に1つのポジションのみが許可されます。注文ボリュームはOrderVolumeを使用してネットされます。
  • StockSharpはバックテスト中にMetaTraderと同じ方法でbid/askを公開しないため、完成したローソク足のクローズ価格が新しいストップ注文の参照レベルとして使用されます。
  • OrderVolumeがパラメーターまたは最適化を通じて調整されると、戦略はキャッシュされたVolume値を自動的に更新します。

実装上の注意と相違点

  • 利益計算はSecurity.PriceStepSecurity.StepPriceに依存します。これらのフィールドが銘柄のメタデータに記入されていることを確認してください。そうでない場合、値1がフォールバックとして使用されます。
  • オリジナルのMQLバージョンは一時的なヘッジング(反対方向への複数注文)を許可していました。StockSharpポートは、プラットフォームのネッティングモデルに準拠するために、執行後すぐに未使用のストップをキャンセルします。
  • 保留注文の有効期限はローソク足のCloseTimeを使用します。履歴データにクローズタイムスタンプがない場合は、それらを提供するようにフィードを調整するか、コードを適切に拡張してください。
  • CandleTypeを調整することで、戦略は任意のローソク足データタイプで動作します。デフォルトのローソク足は時間軸ベースです(TimeSpan.FromMinutes(1).TimeFrame())。

使用上の推奨事項

  1. MetaTraderで使用していたチャート期間に一致するローソク足シリーズを設定してください。
  2. 銘柄のティックサイズとティック値に関連してIndentTicksStabilizationTicks、および利益しきい値を設定してください。
  3. ポートフォリオが希望通りにヘッジングまたはネッティングを使用していることを確認してください。戦略はネッティングを前提とし、ストップ注文を再設定する前にポジションを閉じます。
  4. StockSharp DesignerまたはBacktesterでの最適化に提供されたパラメーターを使用して、異なる市場への動作を適応させてください。
  5. ログ出力を監視してください:コードは新しい注文を送信する前に完成したローソク足と市場の可用性(IsFormedAndOnlineAndAllowTrading())に依存します。

リスク免責事項

自動取引には相当なリスクが伴います。徹底的にバックテストを行い、歴史的データでパラメーターを検証し、ライブアカウントに戦略を展開する前にブローカー固有の要件(ストップ注文の最小距離など)を確認してください。

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>
/// Conversion of the MQL Autotrade strategy that places symmetric stop orders around the market.
/// Pending stop entries are refreshed on every candle while no position is open.
/// Positions are closed when the market calms down or when absolute profit/loss thresholds are reached.
/// </summary>
public class AutotradePendingStopsStrategy : Strategy
{
	private readonly StrategyParam<int> _indentTicks;
	private readonly StrategyParam<decimal> _minProfit;
	private readonly StrategyParam<int> _expirationMinutes;
	private readonly StrategyParam<decimal> _absoluteFixation;
	private readonly StrategyParam<int> _stabilizationTicks;
	private readonly StrategyParam<decimal> _orderVolume;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _prevOpen;
	private decimal _prevClose;
	private bool _hasPrevCandle;

	private decimal _tickSize = 1m;
	private decimal _tickValue = 1m;

	/// <summary>
	/// Distance in price steps from the current market to the pending stop entries.
	/// </summary>
	public int IndentTicks
	{
		get => _indentTicks.Value;
		set => _indentTicks.Value = value;
	}

	/// <summary>
	/// Minimal profit in account currency required to exit when price action stabilizes.
	/// </summary>
	public decimal MinProfit
	{
		get => _minProfit.Value;
		set => _minProfit.Value = value;
	}

	/// <summary>
	/// Lifetime of pending stop orders in minutes.
	/// </summary>
	public int ExpirationMinutes
	{
		get => _expirationMinutes.Value;
		set => _expirationMinutes.Value = value;
	}

	/// <summary>
	/// Absolute profit or loss that forces the position to close.
	/// </summary>
	public decimal AbsoluteFixation
	{
		get => _absoluteFixation.Value;
		set => _absoluteFixation.Value = value;
	}

	/// <summary>
	/// Maximum size of the previous candle body that is treated as consolidation.
	/// </summary>
	public int StabilizationTicks
	{
		get => _stabilizationTicks.Value;
		set => _stabilizationTicks.Value = value;
	}

	/// <summary>
	/// Order volume used for entries.
	/// </summary>
	public decimal OrderVolume
	{
		get => _orderVolume.Value;
		set
		{
			_orderVolume.Value = value;
			Volume = value;
		}
	}

	/// <summary>
	/// Candle type used to drive the strategy logic.
	/// </summary>
	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public AutotradePendingStopsStrategy()
	{
		_indentTicks = Param(nameof(IndentTicks), 200)
		.SetGreaterThanZero()
		.SetDisplay("Indent Ticks", "Distance in ticks between price and pending stop orders", "Entries");

		_minProfit = Param(nameof(MinProfit), 2m)
		.SetGreaterThanZero()
		.SetDisplay("Min Profit", "Minimum profit to close during low volatility", "Risk");

		_expirationMinutes = Param(nameof(ExpirationMinutes), 41)
		.SetGreaterThanZero()
		.SetDisplay("Order Expiration", "Lifetime of pending stops in minutes", "Entries");

		_absoluteFixation = Param(nameof(AbsoluteFixation), 43m)
		.SetGreaterThanZero()
		.SetDisplay("Absolute Fixation", "Profit or loss in currency that forces exit", "Risk");

		_stabilizationTicks = Param(nameof(StabilizationTicks), 25)
		.SetGreaterThanZero()
		.SetDisplay("Stabilization Ticks", "Maximum candle body considered as flat market", "Exits");

		_orderVolume = Param(nameof(OrderVolume), 1m)
		.SetGreaterThanZero()
		.SetDisplay("Order Volume", "Default volume for both stop orders", "General");

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(4).TimeFrame())
		.SetDisplay("Candle Type", "Time frame that drives order refresh", "General");

		Volume = _orderVolume.Value;
	}

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

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

		// Reset runtime state when the strategy is reloaded.
		_prevOpen = 0m;
		_prevClose = 0m;
		_hasPrevCandle = false;
		_entryPrice = 0m;
	}

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

		Volume = _orderVolume.Value;

		// Cache price step and tick value for fast profit calculations.
		_tickSize = Security.PriceStep ?? 1m;
		_tickValue = GetSecurityValue<decimal?>(Level1Fields.StepPrice) ?? _tickSize;

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

	private void ProcessCandle(ICandleMessage candle)
	{
		// Only act on completed candles to stay aligned with the original MQL logic.
		if (candle.State != CandleStates.Finished)
		return;

		if (!_hasPrevCandle)
		{
			// Store the first candle so that stabilization checks have history.
			_prevOpen = candle.OpenPrice;
			_prevClose = candle.ClosePrice;
			_hasPrevCandle = true;

			EnsurePendingOrders(candle);
			return;
		}

		UpdatePendingOrdersLifetime(candle);

		if (Position == 0)
		{
			// Refresh pending orders as soon as the market is flat.
			EnsurePendingOrders(candle);
		}
		else
		{
			// Manage the active position and close it when required.
			ManageOpenPosition(candle);
		}

		// Keep the previous candle body for stabilization checks on the next bar.
		_prevOpen = candle.OpenPrice;
		_prevClose = candle.ClosePrice;
	}

	private decimal _entryPrice;

	private void EnsurePendingOrders(ICandleMessage candle)
	{
		if (!IsFormedAndOnlineAndAllowTrading())
		return;

		var indent = IndentTicks * _tickSize;
		var buyPrice = candle.ClosePrice + indent;
		var sellPrice = candle.ClosePrice - indent;

		// Simulate stop-order breakout: if high breaches buy level, go long
		if (candle.HighPrice >= buyPrice && Position <= 0)
		{
			if (Position < 0)
				BuyMarket(Math.Abs(Position));
			BuyMarket(OrderVolume);
			_entryPrice = buyPrice;
		}
		// if low breaches sell level, go short
		else if (candle.LowPrice <= sellPrice && Position >= 0)
		{
			if (Position > 0)
				SellMarket(Math.Abs(Position));
			SellMarket(OrderVolume);
			_entryPrice = sellPrice;
		}
	}

	private void UpdatePendingOrdersLifetime(ICandleMessage candle)
	{
		// No pending orders in simplified version - nothing to expire.
	}

	private void ManageOpenPosition(ICandleMessage candle)
	{
		var entryPrice = _entryPrice;
		if (entryPrice == 0)
			return;

		var priceDiff = Position > 0 ? candle.ClosePrice - entryPrice : entryPrice - candle.ClosePrice;
		var prevBodySize = Math.Abs(_prevClose - _prevOpen);

		// Exit if profitable and market consolidating, or if loss exceeds threshold
		var exitByProfit = priceDiff > 0 && prevBodySize < candle.ClosePrice * 0.001m;
		var exitByLoss = priceDiff < -candle.ClosePrice * 0.005m;

		if (Position > 0 && (exitByProfit || exitByLoss))
		{
			SellMarket();
		}
		else if (Position < 0 && (exitByProfit || exitByLoss))
		{
			BuyMarket();
		}
	}

}