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
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.
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.
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.
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.
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
Selecionar um tipo de vela apropriado que corresponda ao período de teste do EA original (tipicamente M5 ou M15) para melhor fidelidade.
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.
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.
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;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Indicators import ExponentialMovingAverage
from StockSharp.Algo.Strategies import Strategy
class fluctuate_strategy(Strategy):
def __init__(self):
super(fluctuate_strategy, self).__init__()
self._fast_period = self.Param("FastPeriod", 50) \
.SetDisplay("Fast Period", "Fast EMA period", "Indicator")
self._slow_period = self.Param("SlowPeriod", 200) \
.SetDisplay("Slow Period", "Slow EMA period", "Indicator")
self._stop_loss_points = self.Param("StopLossPoints", 200) \
.SetDisplay("Stop Loss", "Stop-loss distance in price steps", "Risk")
self._take_profit_points = self.Param("TakeProfitPoints", 400) \
.SetDisplay("Take Profit", "Take-profit distance in price steps", "Risk")
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
@property
def fast_period(self):
return self._fast_period.Value
@property
def slow_period(self):
return self._slow_period.Value
@property
def stop_loss_points(self):
return self._stop_loss_points.Value
@property
def take_profit_points(self):
return self._take_profit_points.Value
def OnReseted(self):
super(fluctuate_strategy, self).OnReseted()
self._fast = None
self._slow = None
self._prev_fast = 0.0
self._prev_slow = 0.0
self._entry_price = 0.0
self._cooldown = 0
def OnStarted2(self, time):
super(fluctuate_strategy, self).OnStarted2(time)
self._fast = ExponentialMovingAverage()
self._fast.Length = self.fast_period
self._slow = ExponentialMovingAverage()
self._slow.Length = self.slow_period
subscription = self.SubscribeCandles(DataType.TimeFrame(TimeSpan.FromMinutes(5)))
subscription.Bind(self._fast, self._slow, self._process_candle)
subscription.Start()
def _process_candle(self, candle, fast_value, slow_value):
if candle.State != CandleStates.Finished:
return
fast_val = float(fast_value)
slow_val = float(slow_value)
if not self._fast.IsFormed or not self._slow.IsFormed:
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self._cooldown > 0:
self._cooldown -= 1
self._prev_fast = fast_val
self._prev_slow = slow_val
return
close = float(candle.ClosePrice)
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 1.0
# Check SL/TP
if self.Position > 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close <= self._entry_price - self.stop_loss_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close >= self._entry_price + self.take_profit_points * step:
self.SellMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
elif self.Position < 0 and self._entry_price > 0:
if self.stop_loss_points > 0 and close >= self._entry_price + self.stop_loss_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
if self.take_profit_points > 0 and close <= self._entry_price - self.take_profit_points * step:
self.BuyMarket()
self._entry_price = 0.0
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
return
# EMA crossover buy signal
if self._prev_fast <= self._prev_slow and fast_val > slow_val and self.Position <= 0:
if self.Position < 0:
self.BuyMarket()
self.BuyMarket()
self._entry_price = close
self._cooldown = 50
# EMA crossover sell signal
elif self._prev_fast >= self._prev_slow and fast_val < slow_val and self.Position >= 0:
if self.Position > 0:
self.SellMarket()
self.SellMarket()
self._entry_price = close
self._cooldown = 50
self._prev_fast = fast_val
self._prev_slow = slow_val
def CreateClone(self):
return fluctuate_strategy()