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
Resolve the watched instruments from the comma-separated WatchedSymbols parameter. If the list is empty, the main Security is used.
Subscribe to the selected candle type (default: 1-minute time frame) for each instrument. Every finished candle triggers a profit evaluation.
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.
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.
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;
}
}
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from StockSharp.Algo.Indicators import SimpleMovingAverage
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Messages import DataType, CandleStates
from System import TimeSpan, Math
class multi_pair_closer_strategy(Strategy):
def __init__(self):
super(multi_pair_closer_strategy, self).__init__()
self._profit_target = self.Param("ProfitTarget", 5.0)
self._max_loss = self.Param("MaxLoss", 10.0)
self._min_age_seconds = self.Param("MinAgeSeconds", 60)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromMinutes(30)))
self._sma_period = self.Param("SmaPeriod", 20)
self._sma = None
self._entry_price = 0.0
self._entry_time = None
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(multi_pair_closer_strategy, self).OnStarted2(time)
self._sma = SimpleMovingAverage()
self._sma.Length = self._sma_period.Value
self.SubscribeCandles(self.CandleType).Bind(self._sma, self._process_candle).Start()
def _process_candle(self, candle, sma_val):
if candle.State != CandleStates.Finished:
return
if not self.IsFormed:
return
price = float(candle.ClosePrice)
time = candle.CloseTime
sma_value = float(sma_val)
if self.Position != 0 and self._entry_price > 0:
if self.Position > 0:
pnl = price - self._entry_price
else:
pnl = self._entry_price - price
can_close = self._min_age_seconds.Value <= 0 or (
self._entry_time is not None and (time - self._entry_time).TotalSeconds >= self._min_age_seconds.Value)
if can_close:
if (self._profit_target.Value > 0 and pnl >= self._profit_target.Value) or \
(self._max_loss.Value > 0 and pnl <= -self._max_loss.Value):
if self.Position > 0:
self.SellMarket(abs(self.Position))
else:
self.BuyMarket(abs(self.Position))
self._entry_price = 0.0
self._entry_time = None
return
if self.Position == 0:
if price > sma_value:
self.BuyMarket()
self._entry_price = price
self._entry_time = time
elif price < sma_value:
self.SellMarket()
self._entry_price = price
self._entry_time = time
def OnReseted(self):
super(multi_pair_closer_strategy, self).OnReseted()
self._sma = None
self._entry_price = 0.0
self._entry_time = None
def CreateClone(self):
return multi_pair_closer_strategy()