Ver no GitHub

Estratégia de Fechamento de Múltiplos Pares

Visão geral

A Estratégia de Fechamento de Múltiplos Pares espelha o script original do MetaTrader que supervisiona uma cesta de pares de moedas e liquida todas as posições abertas assim que o lucro flutuante combinado atinge um alvo ou a perda acumulada excede um limite de segurança. A conversão aproveita a API de alto nível do StockSharp para rastrear lucros, impor um tempo mínimo de manutenção e fechar posições em vários instrumentos em uma ação.

Lógica

  1. Resolver os instrumentos observados do parâmetro WatchedSymbols separado por vírgulas. Se a lista estiver vazia, o Security principal é usado.
  2. Inscrever-se no tipo de candle selecionado (padrão: período de 1 minuto) para cada instrumento. Cada candle terminado aciona uma avaliação de lucro.
  3. Para cada instrumento a estratégia armazena:
    • O último lucro calculado (Positions[i].PnL).
    • O timestamp quando uma posição se tornou diferente de zero pela primeira vez para respeitar o requisito MinAgeSeconds.
  4. Após cada atualização, o lucro líquido em todos os símbolos observados é calculado:
    • Se ProfitTarget for atingido, todas as posições com mais idade que o mínimo são achatadas usando ordens BuyMarket / SellMarket.
    • Se o lucro líquido cair abaixo de -MaxLoss, a mesma lógica de liquidação é aplicada como stop protetor.
  5. Registros detalhados resumem o lucro por instrumento e o resultado atual da cesta após cada avaliação.

Parâmetros

Parâmetro Descrição Padrão
WatchedSymbols Lista de identificadores de instrumentos separada por vírgulas para supervisionar. Quando vazia, a estratégia recorre ao Security atribuído. "GBPUSD,USDCAD,USDCHF,USDSEK"
ProfitTarget Lucro líquido (em moeda do portfólio) necessário para acionar um fechamento global de todas as posições observadas. 60
MaxLoss Perda máxima aceitável (em moeda do portfólio) antes de a estratégia fechar forçosamente a cesta. 60
Slippage Parâmetro de compatibilidade que reflete o slippage permitido do script original. Ordens de mercado são usadas para saídas, portanto o valor é informativo. 10
MinAgeSeconds Tempo de vida mínimo de uma posição antes de a estratégia poder fechá-la. 60
CandleType Tipo de candle usado para supervisão periódica (padrão: candles de 1 minuto). 1 minute

Notas

  • A estratégia depende de Positions[i].PnL fornecido pelo StockSharp para medir o lucro flutuante. Ela não busca histórico de trades nem calcula preços manualmente.
  • Posições abertas antes do início da estratégia herdam o tempo de início como seu primeiro timestamp visto. Elas serão fechadas apenas após o intervalo MinAgeSeconds decorrido desde o início da estratégia.
  • As saídas são executadas com ordens de mercado para maximizar a probabilidade de liquidação imediata. Slippage é registrado por paridade com a versão MQL, mas não é aplicado aos cálculos de preço.
  • A saída do registro replica a janela "Comment" do MetaTrader, imprimindo o lucro de cada símbolo seguido do total geral da cesta.

Requisitos

  • Atribuir um SecurityProvider válido ou garantir que os identificadores solicitados estejam disponíveis através do conector.
  • Fornecer configuração de volume suficiente por instrumento para que as ordens de mercado possam achatar completamente a posição.
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;
			}
		}
	}
}