Ver no GitHub

Estratégia Ordens Pendentes Por Horário

Esta estratégia recria o clássico expert MetaTrader "Pending orders by time" para StockSharp. Funciona com uma agenda discreta: todos os dias coloca ordens stop simétricas ao redor do mercado quando uma nova hora de sessão começa, e limpa todas as ordens mais as posições abertas em uma hora de fechamento especificada. A implementação mantém as entradas originais baseadas em pips, converte-as em unidades de preço nativas e usa a API de alto nível para gerenciar o risco.

Como funciona

  1. Gatilho baseado em tempo – Quando uma vela que termina na hora de abertura configurada é recebida, a estratégia envia um buy stop acima do ask e um sell stop abaixo do bid. Ambas as ordens são deslocadas pelo parâmetro Distance (pips) convertido em unidades de preço.
  2. Ordens protetorasStartProtection anexa automaticamente proteção de stop-loss e take-profit usando as distâncias em pips definidas nos parâmetros. ManageRisk também atua como salvaguarda, fechando qualquer posição residual se uma vela completada mostrar que os limites foram cruzados.
  3. Encerramento de sessão – Quando a hora de fechamento chega, a estratégia cancela quaisquer ordens pendentes restantes e sai forcosamente de operações abertas independentemente do lucro ou perda. Isso reproduz o comportamento do expert original de redefinir no final da sessão.
  4. Tamanho de pip com reconhecimento de dígitos – O multiplicador de pip emula a implementação MetaTrader multiplicando o passo de preço por dez para símbolos cotados com três ou cinco casas decimais (p. ex., JPY ou pares FX de 5 dígitos). Isso mantém as entradas legadas consistentes entre brokers.

O tipo de vela padrão são barras de 30 minutos para permanecer abaixo da restrição original de períodos menores que H1. Qualquer outro período pode ser usado, desde que os registros de hora resultantes correspondam às horas de sessão desejadas.

Parâmetros

Nome Descrição Padrão
Opening Hour Hora (0-23) quando a estratégia colocará o par de ordens stop. 9
Closing Hour Hora (0-23) quando todas as ordens são canceladas e posições são fechadas. 2
Distance (pips) Deslocamento, em pips, entre o preço atual e as entradas stop pendentes. 20
Stop Loss (pips) Distância em pips para o stop protetor assim que uma posição estiver aberta. 20
Take Profit (pips) Distância em pips para o alvo de lucro assim que uma posição estiver aberta. 500
Order Volume Quantidade usada ao colocar cada ordem stop pendente. 0.1
Candle Type Período que impulsiona a agenda horária. Período de 30 minutos

Todos os parâmetros podem ser otimizados. As entradas baseadas em pips são convertidas internamente usando o passo de preço do instrumento para que permaneçam portáteis entre símbolos FX com diferente precisão decimal.

Fluxo de trabalho diário

  1. A cada fechamento de vela a estratégia verifica se a distância de stop-loss ou take-profit foi atingida. Se sim, fecha a posição ativa a mercado.
  2. Quando a hora de fechamento é alcançada cancela quaisquer ordens pendentes não preenchidas e sai da posição, garantindo que o livro esteja plano antes da próxima sessão.
  3. Quando a hora de abertura é alcançada (e a estratégia está plana) cancela ordens antigas por precaução e envia um novo sell stop abaixo do bid e um buy stop acima do ask. As ordens são espelhadas ao redor do spread para que qualquer rompimento possa ser capturado.
  4. Ao longo da sessão a proteção a nível de plataforma criada por StartProtection mantém um stop-loss e take-profit anexados, agindo imediatamente se a ação do preço intrabar atingir os limites.

Notas de uso

  • Use instrumentos cujo tamanho de tick representa um único "ponto" para que o ajuste de pip reflita o expert original. Tamanhos de tick exóticos podem exigir ajuste manual dos parâmetros de distância.
  • A lógica assume um ciclo de trading por dia. Se usar dados intradiários com múltiplas correspondências de abertura/fechamento, ajuste as horas de acordo.
  • Como todas as ações ocorrem no fechamento da vela, selecione um tamanho de vela que corresponda à frequência com que deseja avaliar a agenda. Por exemplo, velas horárias fornecem a mesma cadência que a versão MetaTrader.
  • A estratégia só coloca novas ordens pendentes quando a posição está plana, evitando sobreexposição se uma operação de rompimento ainda estiver ativa durante a próxima hora de abertura.

Diferenças da versão MQL

  • As saídas protetoras são tratadas via StartProtection mais verificações explícitas, aproveitando a API de alto nível do StockSharp em vez da atribuição direta de stop-loss no ticket da ordem pendente.
  • Os preços bid/ask são lidos de Security.BestBid e Security.BestAsk. Se essas cotações estiverem indisponíveis, o fechamento da vela é usado como referência de fallback.
  • As ordens de mercado são usadas para liquidar posições na hora de fechamento por simplicidade e para evitar comportamentos específicos do broker.
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>
/// Places simulated symmetric stop entries at scheduled hours and manages them with daily resets.
/// </summary>
public class PendingOrdersByTimeStrategy : Strategy
{
	private readonly StrategyParam<int> _openingHour;
	private readonly StrategyParam<int> _closingHour;
	private readonly StrategyParam<decimal> _distancePips;
	private readonly StrategyParam<decimal> _stopLossPips;
	private readonly StrategyParam<decimal> _takeProfitPips;
	private readonly StrategyParam<DataType> _candleType;

	private decimal _pipSize;
	private decimal? _pendingBuyPrice;
	private decimal? _pendingSellPrice;
	private decimal? _entryPrice;

	public int OpeningHour
	{
		get => _openingHour.Value;
		set => _openingHour.Value = value;
	}

	public int ClosingHour
	{
		get => _closingHour.Value;
		set => _closingHour.Value = value;
	}

	public decimal DistancePips
	{
		get => _distancePips.Value;
		set => _distancePips.Value = value;
	}

	public decimal StopLossPips
	{
		get => _stopLossPips.Value;
		set => _stopLossPips.Value = value;
	}

	public decimal TakeProfitPips
	{
		get => _takeProfitPips.Value;
		set => _takeProfitPips.Value = value;
	}

	public DataType CandleType
	{
		get => _candleType.Value;
		set => _candleType.Value = value;
	}

	public PendingOrdersByTimeStrategy()
	{
		_openingHour = Param(nameof(OpeningHour), 2)
			.SetDisplay("Opening Hour", "Hour to activate pending orders", "Schedule")
			.SetRange(0, 23);

		_closingHour = Param(nameof(ClosingHour), 22)
			.SetDisplay("Closing Hour", "Hour to cancel orders and flat positions", "Schedule")
			.SetRange(0, 23);

		_distancePips = Param(nameof(DistancePips), 500m)
			.SetDisplay("Distance (pips)", "Offset for entry stop orders", "Orders")
			.SetGreaterThanZero();

		_stopLossPips = Param(nameof(StopLossPips), 500m)
			.SetDisplay("Stop Loss (pips)", "Protective stop distance", "Risk")
			.SetGreaterThanZero();

		_takeProfitPips = Param(nameof(TakeProfitPips), 2000m)
			.SetDisplay("Take Profit (pips)", "Profit target distance", "Risk")
			.SetGreaterThanZero();

		_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
			.SetDisplay("Candle Type", "Working timeframe for the schedule", "General");
	}

	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		return [(Security, CandleType)];
	}

	protected override void OnReseted()
	{
		base.OnReseted();

		_pipSize = 0m;
		_pendingBuyPrice = null;
		_pendingSellPrice = null;
		_entryPrice = null;
	}

	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_pipSize = CalculatePipSize();

		var subscription = SubscribeCandles(CandleType);
		subscription
			.Bind(ProcessCandle)
			.Start();

		var area = CreateChartArea();
		if (area != null)
		{
			DrawCandles(area, subscription);
			DrawOwnTrades(area);
		}
	}

	private decimal CalculatePipSize()
	{
		var step = Security?.PriceStep ?? 0.01m;

		if (step <= 0m)
			return 0.01m;

		return step;
	}

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

		var hour = candle.OpenTime.Hour;

		// Check pending stop entries
		CheckPendingEntries(candle);

		// Manage existing position
		ManageRisk(candle);

		if (hour == ClosingHour)
		{
			// Closing hour: cancel pending and exit any open trades.
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
			ExitPosition();
		}

		if (hour == OpeningHour && hour != ClosingHour && Position == 0m && !_pendingBuyPrice.HasValue)
		{
			// Opening hour: set up new pending entries.
			SetupPendingEntries(candle.ClosePrice);
		}
	}

	private void CheckPendingEntries(ICandleMessage candle)
	{
		if (_pendingBuyPrice is decimal buyPrice && candle.HighPrice >= buyPrice && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();
			BuyMarket();
			_entryPrice = buyPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
			return;
		}

		if (_pendingSellPrice is decimal sellPrice && candle.LowPrice <= sellPrice && Position >= 0)
		{
			if (Position > 0)
				SellMarket();
			SellMarket();
			_entryPrice = sellPrice;
			_pendingBuyPrice = null;
			_pendingSellPrice = null;
		}
	}

	private void ManageRisk(ICandleMessage candle)
	{
		if (_pipSize <= 0m || _entryPrice is not decimal entry)
			return;

		var takeProfitDistance = TakeProfitPips * _pipSize;
		var stopLossDistance = StopLossPips * _pipSize;

		if (Position > 0m)
		{
			if (takeProfitDistance > 0m && candle.HighPrice - entry >= takeProfitDistance)
			{
				SellMarket();
				_entryPrice = null;
				return;
			}

			if (stopLossDistance > 0m && entry - candle.LowPrice >= stopLossDistance)
			{
				SellMarket();
				_entryPrice = null;
			}
		}
		else if (Position < 0m)
		{
			if (takeProfitDistance > 0m && entry - candle.LowPrice >= takeProfitDistance)
			{
				BuyMarket();
				_entryPrice = null;
				return;
			}

			if (stopLossDistance > 0m && candle.HighPrice - entry >= stopLossDistance)
			{
				BuyMarket();
				_entryPrice = null;
			}
		}
	}

	private void ExitPosition()
	{
		if (Position > 0m)
			SellMarket();
		else if (Position < 0m)
			BuyMarket();
		_entryPrice = null;
	}

	private void SetupPendingEntries(decimal referencePrice)
	{
		if (_pipSize <= 0m)
			return;

		var distance = DistancePips * _pipSize;
		if (distance <= 0m)
			return;

		_pendingBuyPrice = referencePrice + distance;
		_pendingSellPrice = referencePrice - distance;
	}
}