Estratégia de Gerenciamento de Trailing Straddle
Visão geral
A Estratégia de Gerenciamento de Trailing Straddle replica o comportamento do Expert Advisor original do MetaTrader 5 "Straddle&Trail". A estratégia coloca um par de ordens stop (um straddle) ao redor do preço atual antes de eventos de notícias programados ou imediatamente sob demanda. Uma vez que uma posição é acionada, o algoritmo gerencia transições de break-even, trailing stops e comandos opcionais de encerramento que cancelam ordens pendentes ou fecham posições abertas.
Esta implementação é construída sobre a API de alto nível do StockSharp. O posicionamento de ordens, o gerenciamento de posições e os controles de risco são implementados sem usar processamento de mensagens de baixo nível.
Lógica de negociação
Posicionamento do straddle
- Duas ordens stop (buy stop acima e sell stop abaixo) são criadas assim que a janela do evento programado é atingida ou instantaneamente se
PlaceStraddleImmediately estiver habilitado.
- Os preços das ordens são deslocados do bid/ask atual por
DistanceFromPrice (expresso em pips). O deslocamento é convertido em unidades de preço usando o passo de preço do instrumento.
- A estratégia evita recriar o straddle múltiplas vezes no mesmo dia, a menos que as ordens sejam ajustadas ou explicitamente canceladas.
Gerenciamento de ordens pré-evento
- Quando
AdjustPendingOrders está habilitado, as ordens stop são canceladas e reposicionadas a cada novo minuto para que permaneçam alinhadas com o preço atual.
- Os ajustes param
StopAdjustMinutes antes do evento para evitar perseguir o preço quando a volatilidade aumenta.
- Se
RemoveOppositeOrder estiver habilitado, a ordem stop restante é automaticamente cancelada assim que um lado do straddle é acionado e abre uma posição.
Gerenciamento de risco
- Os níveis iniciais de stop-loss e take-profit são calculados a partir de
StopLossPips e TakeProfitPips e são rastreados internamente.
- Quando o lucro aberto atinge
BreakevenTriggerPips, o nível de stop é movido para o preço de entrada mais BreakevenLockPips (ou o valor simétrico para operações vendidas).
- Se
TrailPips for maior que zero, um trailing stop segue o preço. O trailing pode começar imediatamente ou apenas após a condição de break-even, dependendo de TrailAfterBreakeven.
- As saídas de take-profit e stop são executadas com ordens a mercado para confiabilidade.
Encerramento manual
- Definir
ShutdownNow como true aciona uma limpeza imediata de acordo com a opção ShutdownMode. As ações possíveis incluem fechar posições compradas/vendidas e cancelar ordens pendentes compradas/vendidas.
Parâmetros
| Parâmetro |
Descrição |
ShutdownNow |
Aciona o procedimento de encerramento na próxima atualização de vela. Redefine automaticamente para false após a execução. |
ShutdownMode |
Define o que deve ser cancelado ou fechado (All, LongPositions, ShortPositions, PendingLong, PendingShort). |
DistanceFromPrice |
Distância entre o preço atual e cada ordem stop, medida em pips. |
StopLossPips |
Distância inicial de stop-loss para posições acionadas. Definir como 0 para desabilitar. |
TakeProfitPips |
Distância inicial de take-profit. Definir como 0 para desabilitar. |
TrailPips |
Distância do trailing stop. Definir como 0 para desabilitar o trailing. |
TrailAfterBreakeven |
Quando true, o trailing começa apenas após a condição de break-even ser satisfeita. |
BreakevenLockPips |
Lucro bloqueado quando o gatilho de break-even é ativado. |
BreakevenTriggerPips |
Limite de lucro que ativa a lógica de break-even. |
EventHour / EventMinute |
Horário do evento programado (horário do broker/servidor). Definir ambos como 0 para desabilitar o agendador de eventos. |
PreEventEntryMinutes |
Minutos antes do evento quando o straddle deve ser colocado. Ignorado quando o evento está desabilitado ou quando o posicionamento imediato está habilitado. |
StopAdjustMinutes |
Número de minutos antes do evento quando o ajuste automático de ordens pendentes para. |
RemoveOppositeOrder |
Cancela a ordem stop não preenchida quando a primeira parte do straddle é acionada. |
AdjustPendingOrders |
Habilita o recentramento automático de ordens pendentes enquanto aguarda o evento. |
PlaceStraddleImmediately |
Coloca o straddle logo após a estratégia iniciar, ignorando o agendamento de eventos. |
CandleType |
Assinatura de velas usada para rastreamento de tempo. Padrão: velas de 1 minuto. |
Volume – a propriedade Volume do StockSharp controla o tamanho da ordem. É definida como 1 por padrão e pode ser modificada antes de iniciar a estratégia.
Assinaturas de dados
A estratégia assina:
- A série de velas configurada (padrão 1 minuto) para executar o agendador, lógica de trailing e verificações de encerramento.
- O livro de ordens para acompanhar os preços mais recentes de bid/ask para alinhamento preciso de ordens stop.
Notas e limitações
- O gerenciamento de stop-loss e take-profit é executado via ordens a mercado em vez de modificar ordens de proteção do lado do broker. Isso espelha o comportamento original enquanto mantém a implementação simples.
- A estratégia usa o
PriceStep do instrumento para aproximar o tamanho do pip. Para instrumentos exóticos, ajuste os parâmetros adequadamente.
- O comando de encerramento é avaliado apenas quando novos dados de velas chegam. Para ação imediata, reduza o período das velas.
- A implementação em Python é intencionalmente omitida conforme solicitado.
Notas de conversão
- A lógica de break-even e trailing é portada linha por linha da versão MQL. A versão StockSharp mantém as mesmas relações numéricas, mas opera com preços decimais e usa saídas a mercado.
- O tratamento manual de trades (magic number
0 em MQL) não é reproduzido porque as estratégias do StockSharp gerenciam suas próprias posições. Toda a lógica de proteção se aplica apenas a trades gerados pela estratégia.
- A função
CalcMagic é desnecessária no StockSharp e, portanto, foi removida. O estado da estratégia é rastreado internamente pelo framework.
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>
/// Strategy that simulates a straddle approach: defines upper/lower breakout levels
/// from a consolidation range (ATR-based) and enters on breakouts with trailing stop.
/// </summary>
public class StraddleTrailStrategy : Strategy
{
private readonly StrategyParam<int> _atrPeriod;
private readonly StrategyParam<decimal> _atrMultiplier;
private readonly StrategyParam<decimal> _stopLossMult;
private readonly StrategyParam<decimal> _takeProfitMult;
private readonly StrategyParam<decimal> _trailMult;
private readonly StrategyParam<int> _cooldownBars;
private readonly StrategyParam<DataType> _candleType;
private decimal _entryPrice;
private decimal? _stopLevel;
private decimal? _takeLevel;
private int _barsSinceEntry;
private int _cooldownCounter;
public int AtrPeriod { get => _atrPeriod.Value; set => _atrPeriod.Value = value; }
public decimal AtrMultiplier { get => _atrMultiplier.Value; set => _atrMultiplier.Value = value; }
public decimal StopLossMult { get => _stopLossMult.Value; set => _stopLossMult.Value = value; }
public decimal TakeProfitMult { get => _takeProfitMult.Value; set => _takeProfitMult.Value = value; }
public decimal TrailMult { get => _trailMult.Value; set => _trailMult.Value = value; }
public int CooldownBars { get => _cooldownBars.Value; set => _cooldownBars.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public StraddleTrailStrategy()
{
_atrPeriod = Param(nameof(AtrPeriod), 14)
.SetGreaterThanZero()
.SetDisplay("ATR Period", "ATR calculation length", "ATR");
_atrMultiplier = Param(nameof(AtrMultiplier), 2.5m)
.SetGreaterThanZero()
.SetDisplay("ATR Multiplier", "Breakout distance multiplier", "ATR");
_stopLossMult = Param(nameof(StopLossMult), 1.0m)
.SetGreaterThanZero()
.SetDisplay("SL Multiplier", "Stop loss as ATR multiple", "Risk");
_takeProfitMult = Param(nameof(TakeProfitMult), 3.0m)
.SetGreaterThanZero()
.SetDisplay("TP Multiplier", "Take profit as ATR multiple", "Risk");
_trailMult = Param(nameof(TrailMult), 1.5m)
.SetGreaterThanZero()
.SetDisplay("Trail Multiplier", "Trailing distance as ATR multiple", "Risk");
_cooldownBars = Param(nameof(CooldownBars), 6)
.SetGreaterThanZero()
.SetDisplay("Cooldown", "Bars to wait after exit", "General");
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame())
.SetDisplay("Candle Type", "Candle subscription", "General");
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
protected override void OnReseted()
{
base.OnReseted();
_entryPrice = 0;
_stopLevel = null;
_takeLevel = null;
_barsSinceEntry = 0;
_cooldownCounter = 0;
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
var atr = new AverageTrueRange { Length = AtrPeriod };
var sma = new SimpleMovingAverage { Length = 20 };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(atr, sma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, sma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal atr, decimal sma)
{
if (candle.State != CandleStates.Finished)
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
var close = candle.ClosePrice;
// Manage existing position
if (Position != 0)
{
_barsSinceEntry++;
if (Position > 0)
{
// Trail stop up
var newTrail = close - TrailMult * atr;
if (_stopLevel == null || newTrail > _stopLevel)
_stopLevel = newTrail;
// Check stop or take
if (close <= _stopLevel || (_takeLevel != null && close >= _takeLevel))
{
SellMarket(Math.Abs(Position));
ResetPosition();
return;
}
}
else
{
// Trail stop down
var newTrail = close + TrailMult * atr;
if (_stopLevel == null || newTrail < _stopLevel)
_stopLevel = newTrail;
// Check stop or take
if (close >= _stopLevel || (_takeLevel != null && close <= _takeLevel))
{
BuyMarket(Math.Abs(Position));
ResetPosition();
return;
}
}
return;
}
// Cooldown after exit
if (_cooldownCounter > 0)
{
_cooldownCounter--;
return;
}
// Entry: breakout above/below SMA + ATR distance
var upperLevel = sma + AtrMultiplier * atr;
var lowerLevel = sma - AtrMultiplier * atr;
if (close > upperLevel)
{
BuyMarket();
_entryPrice = close;
_stopLevel = close - StopLossMult * atr;
_takeLevel = close + TakeProfitMult * atr;
_barsSinceEntry = 0;
}
else if (close < lowerLevel)
{
SellMarket();
_entryPrice = close;
_stopLevel = close + StopLossMult * atr;
_takeLevel = close - TakeProfitMult * atr;
_barsSinceEntry = 0;
}
}
private void ResetPosition()
{
_entryPrice = 0;
_stopLevel = null;
_takeLevel = null;
_barsSinceEntry = 0;
_cooldownCounter = CooldownBars;
}
}
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.Strategies import Strategy
from StockSharp.Algo.Indicators import AverageTrueRange, SimpleMovingAverage
class straddle_trail_strategy(Strategy):
def __init__(self):
super(straddle_trail_strategy, self).__init__()
self._atr_period = self.Param("AtrPeriod", 14) \
.SetGreaterThanZero() \
.SetDisplay("ATR Period", "ATR calculation length", "ATR")
self._atr_multiplier = self.Param("AtrMultiplier", 2.5) \
.SetGreaterThanZero() \
.SetDisplay("ATR Multiplier", "Breakout distance multiplier", "ATR")
self._stop_loss_mult = self.Param("StopLossMult", 1.0) \
.SetGreaterThanZero() \
.SetDisplay("SL Multiplier", "Stop loss as ATR multiple", "Risk")
self._take_profit_mult = self.Param("TakeProfitMult", 3.0) \
.SetGreaterThanZero() \
.SetDisplay("TP Multiplier", "Take profit as ATR multiple", "Risk")
self._trail_mult = self.Param("TrailMult", 1.5) \
.SetGreaterThanZero() \
.SetDisplay("Trail Multiplier", "Trailing distance as ATR multiple", "Risk")
self._cooldown_bars = self.Param("CooldownBars", 6) \
.SetGreaterThanZero() \
.SetDisplay("Cooldown", "Bars to wait after exit", "General")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1))) \
.SetDisplay("Candle Type", "Candle subscription", "General")
self._entry_price = 0.0
self._stop_level = None
self._take_level = None
self._bars_since_entry = 0
self._cooldown_counter = 0
@property
def AtrPeriod(self):
return self._atr_period.Value
@AtrPeriod.setter
def AtrPeriod(self, value):
self._atr_period.Value = value
@property
def AtrMultiplier(self):
return self._atr_multiplier.Value
@AtrMultiplier.setter
def AtrMultiplier(self, value):
self._atr_multiplier.Value = value
@property
def StopLossMult(self):
return self._stop_loss_mult.Value
@StopLossMult.setter
def StopLossMult(self, value):
self._stop_loss_mult.Value = value
@property
def TakeProfitMult(self):
return self._take_profit_mult.Value
@TakeProfitMult.setter
def TakeProfitMult(self, value):
self._take_profit_mult.Value = value
@property
def TrailMult(self):
return self._trail_mult.Value
@TrailMult.setter
def TrailMult(self, value):
self._trail_mult.Value = value
@property
def CooldownBars(self):
return self._cooldown_bars.Value
@CooldownBars.setter
def CooldownBars(self, value):
self._cooldown_bars.Value = value
@property
def CandleType(self):
return self._candle_type.Value
@CandleType.setter
def CandleType(self, value):
self._candle_type.Value = value
def OnStarted2(self, time):
super(straddle_trail_strategy, self).OnStarted2(time)
atr = AverageTrueRange()
atr.Length = self.AtrPeriod
sma = SimpleMovingAverage()
sma.Length = 20
subscription = self.SubscribeCandles(self.CandleType)
subscription \
.Bind(atr, sma, self.process_candle) \
.Start()
area = self.CreateChartArea()
if area is not None:
self.DrawCandles(area, subscription)
self.DrawIndicator(area, sma)
self.DrawOwnTrades(area)
def process_candle(self, candle, atr_val, sma_val):
if candle.State != CandleStates.Finished:
return
close = float(candle.ClosePrice)
atr = float(atr_val)
sma = float(sma_val)
if self.Position != 0:
self._bars_since_entry += 1
if self.Position > 0:
new_trail = close - float(self.TrailMult) * atr
if self._stop_level is None or new_trail > self._stop_level:
self._stop_level = new_trail
if close <= self._stop_level or (self._take_level is not None and close >= self._take_level):
self.SellMarket(abs(self.Position))
self._reset_position()
return
else:
new_trail = close + float(self.TrailMult) * atr
if self._stop_level is None or new_trail < self._stop_level:
self._stop_level = new_trail
if close >= self._stop_level or (self._take_level is not None and close <= self._take_level):
self.BuyMarket(abs(self.Position))
self._reset_position()
return
return
if self._cooldown_counter > 0:
self._cooldown_counter -= 1
return
upper_level = sma + float(self.AtrMultiplier) * atr
lower_level = sma - float(self.AtrMultiplier) * atr
if close > upper_level:
self.BuyMarket()
self._entry_price = close
self._stop_level = close - float(self.StopLossMult) * atr
self._take_level = close + float(self.TakeProfitMult) * atr
self._bars_since_entry = 0
elif close < lower_level:
self.SellMarket()
self._entry_price = close
self._stop_level = close + float(self.StopLossMult) * atr
self._take_level = close - float(self.TakeProfitMult) * atr
self._bars_since_entry = 0
def _reset_position(self):
self._entry_price = 0.0
self._stop_level = None
self._take_level = None
self._bars_since_entry = 0
self._cooldown_counter = self.CooldownBars
def OnReseted(self):
super(straddle_trail_strategy, self).OnReseted()
self._entry_price = 0.0
self._stop_level = None
self._take_level = None
self._bars_since_entry = 0
self._cooldown_counter = 0
def CreateClone(self):
return straddle_trail_strategy()