Auf GitHub ansehen

Multi Pair Closer Strategy

Overview

The Multi Pair Closer Strategy mirrors the original MetaTrader script that supervises a basket of currency pairs and liquidates every open position once the combined floating profit hits a target or the accumulated loss breaches a safety threshold. The conversion leverages StockSharp's high-level API to track profits, enforce a minimum holding time, and close positions across several securities in one action.

Logic

  1. Resolve the watched instruments from the comma-separated WatchedSymbols parameter. If the list is empty, the main Security is used.
  2. Subscribe to the selected candle type (default: 1-minute time frame) for each instrument. Every finished candle triggers a profit evaluation.
  3. For each instrument the strategy stores:
    • The last computed profit (Positions[i].PnL).
    • The timestamp when a position first became non-zero to respect the MinAgeSeconds requirement.
  4. After each update the net profit across all watched symbols is calculated:
    • If ProfitTarget is reached, all positions older than the minimum age are flattened using BuyMarket / SellMarket orders.
    • If the net profit drops below -MaxLoss, the same liquidation logic is applied as a protective stop.
  5. Detailed logs summarise the profit per instrument and the current basket result after every evaluation.

Parameters

Parameter Description Default
WatchedSymbols Comma-separated list of security identifiers to supervise. When empty the strategy falls back to the assigned Security. "GBPUSD,USDCAD,USDCHF,USDSEK"
ProfitTarget Net profit (in portfolio currency) required to trigger a global close of all watched positions. 60
MaxLoss Maximum acceptable loss (in portfolio currency) before the strategy force-closes the basket. 60
Slippage Compatibility parameter that reflects the allowed slippage from the original script. Market orders are used for exits, so the value is informational. 10
MinAgeSeconds Minimum lifetime of a position before the strategy is allowed to close it. 60
CandleType Candle type used for periodic supervision (default: 1-minute candles). 1 minute

Notes

  • The strategy relies on Positions[i].PnL provided by StockSharp to measure floating profit. It does not pull trade history or compute prices manually.
  • Positions opened before the strategy starts inherit the start time as their first seen timestamp. They will be closed only after the MinAgeSeconds interval elapses from strategy start.
  • Exits are executed with market orders to maximise the probability of immediate liquidation. Slippage is logged for parity with the MQL version but is not applied to price calculations.
  • Logging output replicates the MetaTrader "Comment" window by printing each symbol's profit followed by the overall basket total.

Requirements

  • Assign a valid SecurityProvider or ensure the requested identifiers are available through the connector.
  • Provide sufficient volume configuration per security so that market orders can flatten the position completely.
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;
			}
		}
	}
}