Multi Pair Closer повторяет исходный скрипт MetaTrader: стратегия наблюдает за корзиной валютных пар и закрывает все открытые позиции, когда суммарная плавающая прибыль достигает целевого значения или общая просадка превышает допустимый лимит. Реализация на StockSharp использует высокоуровневый API, чтобы отслеживать прибыль, соблюдать минимальное время удержания позиции и выполнять одновременное закрытие для нескольких инструментов.
Логика работы
Список контролируемых инструментов формируется из параметра WatchedSymbols (значения через запятую). Если поле пустое, используется основной Security стратегии.
Для каждого инструмента оформляется подписка на выбранный тип свечей (по умолчанию – минутные). Обработка выполняется только после закрытия свечи.
По каждому инструменту хранится:
Последняя рассчитанная прибыль через Positions[i].PnL.
Момент времени, когда позиция впервые стала ненулевой, чтобы соблюсти ограничение MinAgeSeconds.
После каждого обновления рассчитывается суммарная прибыль по корзине:
Если достигнут ProfitTarget, все позиции, «дожившие» до минимального возраста, закрываются через рыночные заявки (BuyMarket / SellMarket).
Если суммарный результат опускается ниже -MaxLoss, выполняется защитное закрытие по тем же правилам.
В логах выводится прибыль по каждому инструменту и общий итог — аналогично полю Comment в оригинальном скрипте.
Параметры
Параметр
Описание
Значение по умолчанию
WatchedSymbols
Идентификаторы инструментов для мониторинга (строка через запятую).
"GBPUSD,USDCAD,USDCHF,USDSEK"
ProfitTarget
Целевая прибыль корзины (в валюте портфеля), при достижении которой закрываются все позиции.
60
MaxLoss
Максимально допустимая просадка корзины (в валюте портфеля) перед принудительным закрытием.
60
Slippage
Параметр для совместимости с MQL-версией. Выходы выполняются рыночными ордерами, поэтому значение носит информационный характер.
10
MinAgeSeconds
Минимальное время удержания позиции (в секундах) перед возможным закрытием.
60
CandleType
Тип свечей, используемых для периодической проверки (по умолчанию 1-минутные).
1 minute
Особенности
Стратегия опирается на значения PnL, предоставляемые StockSharp, без дополнительного расчёта исторических данных.
Позиции, уже открытые к моменту старта, получают отметку времени запуска и будут закрыты лишь после истечения MinAgeSeconds.
Для закрытия используются рыночные ордера, что минимизирует риск неполного выхода. Параметр Slippage сохранён ради соответствия оригиналу.
Детализированное журналирование помогает контролировать результаты по каждому инструменту и по корзине в целом.
Требования
Необходим доступ к SecurityProvider или соединению, которое сможет найти все инструменты из WatchedSymbols.
Убедитесь, что объём заявок позволяет полностью перекрыть текущий объём позиции при принудительном закрытии.
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()