GitHub で見る

複数ペア一括決済戦略

概要

複数ペア一括決済戦略は、通貨ペアのバスケットを監視し、合算された浮動利益が目標に達するか、または累積損失がセーフティ閾値を超えた場合にすべてのオープンポジションを清算する元の MetaTrader スクリプトを反映しています。この変換は StockSharp の高レベル API を活用して利益を追跡し、最小保有時間を強制し、1 つのアクションで複数の銘柄のポジションを閉じます。

ロジック

  1. コンマ区切りの WatchedSymbols パラメーターから監視する銘柄を解決します。リストが空の場合は、メインの Security が使用されます。
  2. 各銘柄に対して選択されたローソク足タイプ(デフォルト:1 分タイムフレーム)を購読します。完成した各ローソク足が利益評価をトリガーします。
  3. 各銘柄について戦略は以下を保存します:
    • 最後に計算された利益(Positions[i].PnL)。
    • ポジションが最初に非ゼロになったときのタイムスタンプ(MinAgeSeconds 要件を尊重するため)。
  4. 各更新後、すべての監視シンボルの純利益が計算されます:
    • ProfitTarget に達すると、最小年齢より古いすべてのポジションは BuyMarket / SellMarket 注文を使用してフラット化されます。
    • 純利益が -MaxLoss を下回ると、同じ清算ロジックが保護ストップとして適用されます。
  5. 詳細なログが各評価後の銘柄ごとの利益と現在のバスケット結果を要約します。

パラメーター

パラメーター 説明 デフォルト
WatchedSymbols 監視する銘柄識別子のコンマ区切りリスト。空の場合、戦略は割り当てられた Security にフォールバックします。 "GBPUSD,USDCAD,USDCHF,USDSEK"
ProfitTarget すべての監視ポジションのグローバルクローズをトリガーするために必要な純利益(ポートフォリオ通貨)。 60
MaxLoss 戦略がバスケットを強制決済する前の最大許容損失(ポートフォリオ通貨)。 60
Slippage 元のスクリプトの許容スリッページを反映する互換性パラメーター。成行注文が決済に使用されるため、値は情報的です。 10
MinAgeSeconds 戦略が閉じることを許可される前のポジションの最小存続時間。 60
CandleType 定期的な監視に使用されるローソク足タイプ(デフォルト:1 分ローソク足)。 1 minute

注意事項

  • 戦略は浮動利益を測定するために StockSharp が提供する Positions[i].PnL に依存します。取引履歴を引き出したり価格を手動で計算したりしません。
  • 戦略が開始する前に開かれたポジションは、最初に見られたタイムスタンプとして開始時間を継承します。これらは戦略の開始から MinAgeSeconds 間隔が経過した後にのみ閉じられます。
  • 決済は即時清算の確率を最大化するために成行注文で実行されます。Slippage は MQL バージョンとの同等性のためにログに記録されますが、価格計算には適用されません。
  • ログ出力は、各シンボルの利益に続いてバスケット合計を印刷することで MetaTrader の「Comment」ウィンドウを再現します。

要件

  • 有効な SecurityProvider を割り当てるか、要求された識別子がコネクターを通じて利用可能であることを確認してください。
  • 成行注文がポジションを完全にフラット化できるよう、銘柄ごとに十分なボリューム設定を提供してください。
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>
/// Closes the current position when floating PnL reaches a profit target or maximum loss.
/// Simplified from the multi-pair closer utility to work with a single security.
/// </summary>
public class MultiPairCloserStrategy : Strategy
{
	private readonly StrategyParam<decimal> _profitTarget;
	private readonly StrategyParam<decimal> _maxLoss;
	private readonly StrategyParam<int> _minAgeSeconds;
	private readonly StrategyParam<DataType> _candleType;
	private readonly StrategyParam<int> _smaPeriod;

	private SimpleMovingAverage _sma;
	private decimal _entryPrice;
	private DateTimeOffset? _entryTime;

	/// <summary>
	/// Profit target in price units.
	/// </summary>
	public decimal ProfitTarget
	{
		get => _profitTarget.Value;
		set => _profitTarget.Value = value;
	}

	/// <summary>
	/// Maximum tolerated loss in price units.
	/// </summary>
	public decimal MaxLoss
	{
		get => _maxLoss.Value;
		set => _maxLoss.Value = value;
	}

	/// <summary>
	/// Minimum age of an open position in seconds before exit is permitted.
	/// </summary>
	public int MinAgeSeconds
	{
		get => _minAgeSeconds.Value;
		set => _minAgeSeconds.Value = value;
	}

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

	/// <summary>
	/// SMA period for entry signals.
	/// </summary>
	public int SmaPeriod
	{
		get => _smaPeriod.Value;
		set => _smaPeriod.Value = value;
	}

	/// <summary>
	/// Initializes strategy parameters.
	/// </summary>
	public MultiPairCloserStrategy()
	{
		_profitTarget = Param(nameof(ProfitTarget), 5m)
			.SetNotNegative()
			.SetDisplay("Profit Target", "Close position when floating profit reaches this value", "Risk Management");

		_maxLoss = Param(nameof(MaxLoss), 10m)
			.SetNotNegative()
			.SetDisplay("Maximum Loss", "Close position when floating loss reaches this value", "Risk Management");

		_minAgeSeconds = Param(nameof(MinAgeSeconds), 60)
			.SetNotNegative()
			.SetDisplay("Min Age (s)", "Minimum holding time before exit is allowed", "Execution");

		_candleType = Param(nameof(CandleType), TimeSpan.FromMinutes(30).TimeFrame())
			.SetDisplay("Candle Type", "Candle series for monitoring", "General");

		_smaPeriod = Param(nameof(SmaPeriod), 20)
			.SetGreaterThanZero()
			.SetDisplay("SMA Period", "Moving average period for entry signal", "Indicators");
	}

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

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();
		_sma = null;
		_entryPrice = 0m;
		_entryTime = null;
	}

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

		_sma = new SimpleMovingAverage { Length = SmaPeriod };

		SubscribeCandles(CandleType)
			.Bind(_sma, ProcessCandle)
			.Start();
	}

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

		if (!IsFormed)
			return;

		var price = candle.ClosePrice;
		var time = candle.CloseTime;

		// Check exit conditions for open position
		if (Position != 0 && _entryPrice > 0m)
		{
			var pnl = Position > 0
				? price - _entryPrice
				: _entryPrice - price;

			var canClose = MinAgeSeconds <= 0 ||
				(_entryTime.HasValue && (time - _entryTime.Value).TotalSeconds >= MinAgeSeconds);

			if (canClose)
			{
				if ((ProfitTarget > 0m && pnl >= ProfitTarget) ||
					(MaxLoss > 0m && pnl <= -MaxLoss))
				{
					if (Position > 0)
						SellMarket(Math.Abs(Position));
					else
						BuyMarket(Math.Abs(Position));

					_entryPrice = 0m;
					_entryTime = null;
					return;
				}
			}
		}

		// Entry logic: trend following with SMA
		if (Position == 0)
		{
			if (price > smaValue)
			{
				BuyMarket();
				_entryPrice = price;
				_entryTime = time;
			}
			else if (price < smaValue)
			{
				SellMarket();
				_entryPrice = price;
				_entryTime = time;
			}
		}
	}
}