Ver no GitHub

Estratégia Flutuante

A Estratégia Flutuante é um port do StockSharp do expert advisor do MetaTrader "Fluctuate". Reproduz o comportamento similar a uma grade do original usando a API de alto nível: uma assinatura de velas impulsiona todas as decisões, as entradas no mercado são realizadas com BuyMarket / SellMarket, e as ordens de recuperação são colocadas com stop orders. A exposição comprada e vendida é rastreada separadamente para imitar a contabilidade de posição estilo hedging usada no MetaTrader, enquanto a posição real do StockSharp permanece líquida.

Ideia central

  1. Cada vez que uma nova vela fecha, a estratégia compara os dois últimos preços de fechamento. Um fechamento mais alto abre uma compra a mercado, um fechamento mais baixo abre uma venda a mercado. Se ambos os fechamentos forem iguais a barra é ignorada.
  2. Cada posição executada recebe um stop-loss e take-profit fixos (expressos em pips). A estratégia também registra o preço exato de execução e o volume líquido adicionado pelo trade.
  3. Após uma entrada, uma stop order oposta é ativada a StepPips de distância do último preço de execução (mais um pequeno buffer de spread). Seu volume é derivado do trade anterior e do LotCoefficient, opcionalmente usando a exposição acumulada quando MultiplyLotCoefficient = true.
  4. Quando a stop order é ativada, ela cancela a ordem pendente anterior, atualiza as estatísticas de exposição interna e imediatamente programa uma nova stop order de recuperação na outra direção. Isso reproduz o loop de averaging/martingale presente na implementação MQL.
  5. A proteção de trailing eleva (ou abaixa) o stop uma vez que o preço se move pelo menos TrailingStopPips + TrailingStepPips a favor da posição. Isso emula o EA original que exigia um buffer de lucro adicional antes de apertar o stop.

Fluxo de trading

  • Detecção de sinais. O feed de velas é assinado via SubscribeCandles. Apenas velas terminadas são processadas. A estratégia se recusa a negociar fora da janela de tempo [StartHour, EndHour) ou quando o guardião de capital é ativado.
  • Dimensionamento inicial de posição. Dependendo de PositionSizingMode, o primeiro trade em uma sequência usa um lote fixo (FixedVolume) ou um lote baseado em risco (RiskPercent). No modo de risco, o risco permitido (porcentagem do capital atual) é dividido pela perda monetária que ocorreria se o stop-loss fosse ativado. Passo de preço e preço de passo são usados para converter pips em moeda.
  • Contabilidade de exposição. Acumuladores separados rastreiam volume comprado e vendido, preço médio e o preço extremo atingido desde a entrada. Isso permite que a estratégia mantenha ambos os lados "abertos" internamente, embora o StockSharp use netting.
  • Ordens de recuperação. Após cada execução, o algoritmo calcula o volume da próxima stop order:
    • Quando MultiplyLotCoefficient = false, o novo volume equivale a LastVolume × LotCoefficient.
    • Quando true, a exposição absoluta total é multiplicada por LotCoefficient.
    • O volume é normalizado para as restrições da bolsa (passo, volume mínimo e máximo) e rejeitado quando excederia MaxTotalVolume ou o número de posições ativas mais ordens excederia MaxPositions.
  • Alvo de lucro e guardião de capital. PnL não realizado agregado é calculado traduzindo diferenças de preço em moeda usando PriceStep/StepPrice. Se atingir ProfitTarget, todas as posições são fechadas e as ordens pendentes são canceladas. O trading também é suspenso quando o capital cai abaixo de MinEquityPercent do saldo inicial.
  • Lógica de trailing. Para posições compradas, o preço mais alto visto desde a entrada é registrado. Uma vez que supera o preço de entrada em TrailingStopPips + TrailingStepPips, um trailing stop é definido TrailingStopPips atrás da máxima. Posições vendidas aplicam a regra simétrica com o preço mais baixo. As atualizações de trailing substituem o stop-loss fixo.

Detalhes de gestão de risco

  • Stop / take profit. Ambos são opcionais (definir o valor em pips como zero para desabilitar). Eles são recalculados para a exposição comprada ou vendida agregada sempre que um novo trade adiciona volume.
  • Máx. posições. Conta o número de lados abertos (comprado + vendido) mais a stop order de recuperação ativa. Quando o limite é atingido, a estratégia se recusa a enviar novas stop orders.
  • Volume total máximo. Limita a soma do volume aberto absoluto e o volume da ordem de recuperação ativa.
  • CloseAllAtStart. Interruptor de segurança opcional para zerar o livro antes de a estratégia começar a negociar.

Parâmetros

Nome Descrição Padrão
CandleType Período principal usado para detecção de sinais. Período de 1 minuto
StopLossPips Distância entre o preço de entrada e o stop-loss (pips). 0 desativa o stop. 50
TakeProfitPips Distância entre o preço de entrada e o take-profit (pips). 0 desativa o take-profit. 50
TrailingStopPips Distância do trailing stop (pips). Requer TrailingStepPips > 0. 5
TrailingStepPips Lucro adicional necessário antes que o trailing stop avance (pips). 5
StepPips Distância entre o último preço de execução e o stop de recuperação oposto (pips). 30
LotCoefficient Multiplicador aplicado ao volume anterior (ou exposição total). 2.0
MultiplyLotCoefficient Quando true, o volume da nova ordem é calculado a partir da exposição total em vez do último trade. false
MaxPositions Número máximo de lados abertos simultâneos mais a ordem pendente ativa. 9
MaxTotalVolume Limite para a soma do volume aberto e o volume da ordem de recuperação. 50
ProfitTarget Lucro não realizado (em moeda da conta) que desencadeia uma saída completa. 0 desativa o alvo. 50
MinEquityPercent Percentual mínimo de capital (vs. saldo inicial) necessário para continuar negociando. Abaixo deste limiar apenas saídas são permitidas. 30
CloseAllAtStart Fechar todas as posições e cancelar ordens quando a estratégia inicia. false
StartHour Hora de início da janela de trading (inclusiva, horário da bolsa). 10
EndHour Hora de fim da janela de trading (exclusiva, horário da bolsa). 20
PositionSizingMode FixedVolume para lotes estáticos, RiskPercent para dimensionamento por percentual de capital. FixedVolume
VolumeOrRisk Tamanho de lote fixo (quando FixedVolume) ou percentual de risco (quando RiskPercent). 1.0

Notas de implementação

  • Os preços da stop order usam uma aproximação de spread mínimo (PriceStep quando disponível) porque o MetaTrader exigia que a ordem estivesse fora do nível de congelamento. Ajustar StepPips se o spread real for mais amplo.
  • A estratégia cancela qualquer ordem de recuperação restante sempre que um novo trade é executado. Isso corresponde ao EA original que excluía todas as ordens pendentes após uma execução.
  • Como os portfólios do StockSharp são líquidos, a exposição com hedge é simulada internamente. A posição real do corretor sempre refletirá a quantidade líquida.
  • O dimensionamento de posição baseado em risco requer valores válidos de PriceStep e StepPrice da descrição do instrumento.

Dicas de uso

  1. Selecionar um tipo de vela apropriado que corresponda ao período de teste do EA original (tipicamente M5 ou M15) para melhor fidelidade.
  2. Verificar os limites de volume da bolsa: se o volume de recuperação normalizado se tornar zero, a estratégia deixará de adicionar novas pernas.
  3. Quando PositionSizingMode = RiskPercent, garantir que o portfólio contenha informações de capital atualizadas; caso contrário a estratégia recorre ao tamanho de lote fixo.
  4. Combinar com StrategyProtection incorporado do StockSharp (habilitado via StartProtection()) para adicionar salvaguardas adicionais no nível de conta, se necessário.
using System;
using System.Collections.Generic;

using Ecng.Common;

using StockSharp.Algo.Indicators;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;

namespace StockSharp.Samples.Strategies;

/// <summary>
/// Fluctuate strategy using EMA crossover with stop-loss and take-profit.
/// Buys when fast EMA crosses above slow EMA, sells on reverse cross.
/// </summary>
public class FluctuateStrategy : Strategy
{
	private readonly StrategyParam<int> _fastPeriod;
	private readonly StrategyParam<int> _slowPeriod;
	private readonly StrategyParam<int> _stopLossPoints;
	private readonly StrategyParam<int> _takeProfitPoints;

	private ExponentialMovingAverage _fast;
	private ExponentialMovingAverage _slow;

	private decimal _prevFast;
	private decimal _prevSlow;
	private decimal _entryPrice;
	private int _cooldown;

	/// <summary>
	/// Fast EMA period.
	/// </summary>
	public int FastPeriod
	{
		get => _fastPeriod.Value;
		set => _fastPeriod.Value = value;
	}

	/// <summary>
	/// Slow EMA period.
	/// </summary>
	public int SlowPeriod
	{
		get => _slowPeriod.Value;
		set => _slowPeriod.Value = value;
	}

	/// <summary>
	/// Stop-loss distance in price steps.
	/// </summary>
	public int StopLossPoints
	{
		get => _stopLossPoints.Value;
		set => _stopLossPoints.Value = value;
	}

	/// <summary>
	/// Take-profit distance in price steps.
	/// </summary>
	public int TakeProfitPoints
	{
		get => _takeProfitPoints.Value;
		set => _takeProfitPoints.Value = value;
	}

	/// <summary>
	/// Initialize <see cref="FluctuateStrategy"/>.
	/// </summary>
	public FluctuateStrategy()
	{
		_fastPeriod = Param(nameof(FastPeriod), 50)
			.SetGreaterThanZero()
			.SetDisplay("Fast Period", "Fast EMA period", "Indicator");

		_slowPeriod = Param(nameof(SlowPeriod), 200)
			.SetGreaterThanZero()
			.SetDisplay("Slow Period", "Slow EMA period", "Indicator");

		_stopLossPoints = Param(nameof(StopLossPoints), 200)
			.SetNotNegative()
			.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk");

		_takeProfitPoints = Param(nameof(TakeProfitPoints), 400)
			.SetNotNegative()
			.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk");
	}

	/// <inheritdoc />
	public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
	{
		yield return (Security, TimeSpan.FromMinutes(5).TimeFrame());
	}

	/// <inheritdoc />
	protected override void OnReseted()
	{
		base.OnReseted();

		_fast = null;
		_slow = null;
		_prevFast = 0;
		_prevSlow = 0;
		_entryPrice = 0;
		_cooldown = 0;
	}

	/// <inheritdoc />
	protected override void OnStarted2(DateTime time)
	{
		base.OnStarted2(time);

		_fast = new ExponentialMovingAverage { Length = FastPeriod };
		_slow = new ExponentialMovingAverage { Length = SlowPeriod };

		var subscription = SubscribeCandles(TimeSpan.FromMinutes(5).TimeFrame());
		subscription.Bind(_fast, _slow, ProcessCandle);
		subscription.Start();
	}

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

		if (!_fast.IsFormed || !_slow.IsFormed)
		{
			_prevFast = fastValue;
			_prevSlow = slowValue;
			return;
		}

		if (_cooldown > 0)
		{
			_cooldown--;
			_prevFast = fastValue;
			_prevSlow = slowValue;
			return;
		}

		var close = candle.ClosePrice;
		var step = Security?.PriceStep ?? 1m;

		// Check SL/TP
		if (Position > 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close <= _entryPrice - StopLossPoints * step)
			{
				SellMarket();
				_entryPrice = 0;
				_cooldown = 50;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}

			if (TakeProfitPoints > 0 && close >= _entryPrice + TakeProfitPoints * step)
			{
				SellMarket();
				_entryPrice = 0;
				_cooldown = 50;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}
		}
		else if (Position < 0 && _entryPrice > 0)
		{
			if (StopLossPoints > 0 && close >= _entryPrice + StopLossPoints * step)
			{
				BuyMarket();
				_entryPrice = 0;
				_cooldown = 50;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}

			if (TakeProfitPoints > 0 && close <= _entryPrice - TakeProfitPoints * step)
			{
				BuyMarket();
				_entryPrice = 0;
				_cooldown = 50;
				_prevFast = fastValue;
				_prevSlow = slowValue;
				return;
			}
		}

		// EMA crossover buy signal
		if (_prevFast <= _prevSlow && fastValue > slowValue && Position <= 0)
		{
			if (Position < 0)
				BuyMarket();

			BuyMarket();
			_entryPrice = close;
			_cooldown = 50;
		}
		// EMA crossover sell signal
		else if (_prevFast >= _prevSlow && fastValue < slowValue && Position >= 0)
		{
			if (Position > 0)
				SellMarket();

			SellMarket();
			_entryPrice = close;
			_cooldown = 50;
		}

		_prevFast = fastValue;
		_prevSlow = slowValue;
	}
}