Estratégia Brandy v1.2 (C#)
Visão geral
A Estratégia Brandy v1.2 é uma conversão direta do consultor especialista MetaTrader 4 "Brandy_v1_2.mq4" na estrutura de estratégia de alto nível StockSharp. O sistema avalia um par de médias móveis simples deslocadas (SMAs) calculadas sobre o preço de fechamento da série de velas configurada. Novas posições são abertas apenas quando os SMAs de longo e curto prazo mostram impulso sincronizado na mesma direção, enquanto as negociações existentes são gerenciadas usando reversões de inclinação, níveis de stop-loss fixos e um módulo opcional de trailing stop.
O script MQL original foi executado exatamente uma vez por barra concluída. Esta porta processa StockSharp velas finalizadas da mesma maneira, garantindo que todas as decisões de negociação sejam baseadas em dados fechados, sem depender de barras parcialmente formadas.
Lógica de negociação
- Preparação de indicadores
- Dois SMAs são calculados: uma linha de base mais longa (
LongPeriod) e uma linha de confirmação mais curta (ShortPeriod).
- Cada média é acessada duas vezes: o valor da barra anterior (shift = 1) e outro valor deslocado por
LongShift/ShortShift barras respectivamente. Isso reproduz as chamadas iMA(..., shift) presentes no EA original.
- Regras de entrada
- Compre quando o valor da barra anterior de ambos os SMAs for maior do que suas contrapartes deslocadas (ambas as inclinações apontando para cima) e nenhuma posição estiver aberta.
- Venda quando o valor da barra anterior de ambos os SMAs for inferior aos seus homólogos deslocados (ambas as inclinações apontando para baixo) e nenhuma posição estiver aberta.
- Apenas uma posição pode estar ativa a qualquer momento, espelhando a verificação
k == 0 na fonte MQL.
- Regras de saída
- Reversão de inclinação: uma posição longa aberta é liquidada se a posição longa SMA cair (
longPrev < longShifted), enquanto uma posição curta é coberta quando a posição longa SMA subir (longPrev > longShifted).
- Stop-loss fixo: ao entrar, a estratégia armazena um nível de stop inicial compensado em
StopLossPoints × PriceStep do preço de entrada. O stop é verificado em relação à faixa máxima/mínima da vela, aproximando-se do gerenciamento do nível de tick do consultor original.
- Trailing stop: se
TrailingStopPoints ≥ 100, a estratégia replica a lógica final (parâmetro ts). Quando o lucro flutuante excede a distância final, o stop é puxado para currentPrice ± trailingDistance, desde que o novo nível esteja mais próximo do preço do que o stop existente. Este comportamento corresponde às chamadas OrderModify no especialista MQL.
Parâmetros
| Parâmetro |
Padrão |
Descrição |
LongPeriod |
70 |
Comprimento do SMA primário (p1 em MQL). Deve ser > 0. |
LongShift |
5 |
Deslocamento para trás aplicado à comparação longa SMA (s1). Pode ser zero. |
ShortPeriod |
20 |
Comprimento da confirmação SMA (p2). Deve ser > 0. |
ShortShift |
5 |
Mudança para trás para o SMA curto (s2). Pode ser zero. |
StopLossPoints |
50 |
Distância de parada fixa em etapas de preço (sl). Defina como 0 para desativar a parada brusca. |
TrailingStopPoints |
150 |
Distância final em etapas de preço (ts). O rastreamento é ativado somente quando o valor é ≥ 100, espelhando o limite original. |
Volume |
0,1 |
Volume do pedido usado para entradas (lots). |
CandleType |
Período de 15 minutos |
Série de velas processadas pela estratégia (configurável pelo usuário). |
Dependência da etapa de preço
Ambos os parâmetros de parada operam em pontos do instrumento. O método auxiliar os converte em deltas de preços absolutos por meio de Security.PriceStep. Se a fonte de dados não fornecer PriceStep, a estratégia volta para 0.0001 para que a lógica continue funcionando, embora com uma conversão aproximada. Sempre verifique os metadados do símbolo em StockSharp antes do uso ao vivo.
Gestão de risco
- Parada brusca: armazenada internamente e validada em cada vela finalizada. Quando o preço viola o stop, a chamada
SellMarket/BuyMarket correspondente fecha toda a posição.
- Trailing stop: segue as condições exatas do EA original, movendo o stop somente quando o lucro atual excede a distância final e o stop existente ainda está mais longe que essa distância.
- Posição única: o algoritmo nunca faz pirâmides; ele tem uma única posição longa, uma única posição curta ou é plano.
Notas de implementação
- O estado (preço de entrada, nível de stop, históricos de SMA) é redefinido automaticamente em
OnReseted(), garantindo backtests e reinicializações limpos.
- Os históricos dos indicadores são armazenados em buffers rolantes curtos para reproduzir os deslocamentos
iMA(..., shift) sem chamar GetValue().
- Todos os comentários embutidos permanecem em inglês, conforme exigido pelas diretrizes do repositório.
- Nenhuma contraparte Python é fornecida. Somente a implementação de alto nível C# é entregue em
CS/BrandyV12Strategy.cs conforme solicitado.
Uso
- Coloque a estratégia em uma solução StockSharp, selecione o instrumento desejado e certifique-se de que os dados da vela correspondam ao período especificado por
CandleType.
- Configure os parâmetros na UI ou por meio de código. Os padrões replicam os valores MT4 originais.
- Comece a estratégia. Ele assinará a série de velas, desenhará ambos os SMAs no gráfico e gerenciará as negociações automaticamente.
Isenção de responsabilidade: Esta porta destina-se a fins educacionais e de teste. Sempre valide o comportamento em sessões de negociação históricas e em papel antes de implantar em mercados reais.
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>
/// Trend-following strategy using displaced simple moving averages.
/// </summary>
public class BrandyV12Strategy : Strategy
{
private readonly StrategyParam<int> _longPeriod;
private readonly StrategyParam<int> _longShift;
private readonly StrategyParam<int> _shortPeriod;
private readonly StrategyParam<int> _shortShift;
private readonly StrategyParam<decimal> _stopLossPoints;
private readonly StrategyParam<decimal> _trailingStopPoints;
private readonly StrategyParam<DataType> _candleType;
private SimpleMovingAverage _longSma;
private SimpleMovingAverage _shortSma;
private readonly List<decimal> _longHistory = new();
private readonly List<decimal> _shortHistory = new();
private decimal? _entryPrice;
private decimal? _stopPrice;
/// <summary>
/// Initializes a new instance of <see cref="BrandyV12Strategy"/>.
/// </summary>
public BrandyV12Strategy()
{
_longPeriod = Param(nameof(LongPeriod), 70)
.SetGreaterThanZero()
.SetDisplay("Long SMA Period", "Period for the longer moving average.", "Indicators")
;
_longShift = Param(nameof(LongShift), 5)
.SetNotNegative()
.SetDisplay("Long SMA Shift", "Backward shift applied to the longer SMA.", "Indicators")
;
_shortPeriod = Param(nameof(ShortPeriod), 20)
.SetGreaterThanZero()
.SetDisplay("Short SMA Period", "Period for the shorter moving average.", "Indicators")
;
_shortShift = Param(nameof(ShortShift), 5)
.SetNotNegative()
.SetDisplay("Short SMA Shift", "Backward shift applied to the shorter SMA.", "Indicators")
;
_stopLossPoints = Param(nameof(StopLossPoints), 50m)
.SetNotNegative()
.SetDisplay("Stop Loss (points)", "Initial stop-loss distance expressed in price steps.", "Risk")
;
_trailingStopPoints = Param(nameof(TrailingStopPoints), 150m)
.SetNotNegative()
.SetDisplay("Trailing Stop (points)", "Trailing stop distance in price steps. Activates when >= 100.", "Risk")
;
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(2).TimeFrame())
.SetDisplay("Candle Type", "Candle series processed by the strategy.", "General");
}
/// <summary>
/// Period for the longer simple moving average.
/// </summary>
public int LongPeriod
{
get => _longPeriod.Value;
set => _longPeriod.Value = value;
}
/// <summary>
/// Backward shift used when evaluating the longer SMA.
/// </summary>
public int LongShift
{
get => _longShift.Value;
set => _longShift.Value = value;
}
/// <summary>
/// Period for the shorter simple moving average.
/// </summary>
public int ShortPeriod
{
get => _shortPeriod.Value;
set => _shortPeriod.Value = value;
}
/// <summary>
/// Backward shift used when evaluating the shorter SMA.
/// </summary>
public int ShortShift
{
get => _shortShift.Value;
set => _shortShift.Value = value;
}
/// <summary>
/// Stop-loss distance in points (price steps).
/// </summary>
public decimal StopLossPoints
{
get => _stopLossPoints.Value;
set => _stopLossPoints.Value = value;
}
/// <summary>
/// Trailing stop distance in points (price steps).
/// Trailing activates only when the configured value is at least 100.
/// </summary>
public decimal TrailingStopPoints
{
get => _trailingStopPoints.Value;
set => _trailingStopPoints.Value = value;
}
/// <summary>
/// Candle type processed by the strategy.
/// </summary>
public DataType CandleType
{
get => _candleType.Value;
set => _candleType.Value = value;
}
/// <inheritdoc />
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
{
return [(Security, CandleType)];
}
/// <inheritdoc />
protected override void OnReseted()
{
base.OnReseted();
_longSma = null;
_shortSma = null;
_longHistory.Clear();
_shortHistory.Clear();
_entryPrice = null;
_stopPrice = null;
}
/// <inheritdoc />
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_longSma = new SMA { Length = LongPeriod };
_shortSma = new SMA { Length = ShortPeriod };
var subscription = SubscribeCandles(CandleType);
subscription
.Bind(_longSma, _shortSma, ProcessCandle)
.Start();
var area = CreateChartArea();
if (area != null)
{
DrawCandles(area, subscription);
DrawIndicator(area, _longSma);
DrawIndicator(area, _shortSma);
DrawOwnTrades(area);
}
}
private void ProcessCandle(ICandleMessage candle, decimal longValue, decimal shortValue)
{
if (candle.State != CandleStates.Finished)
return;
if (_longSma?.IsFormed != true || _shortSma?.IsFormed != true)
return;
var longCapacity = Math.Max(LongShift, 1) + 2;
var shortCapacity = Math.Max(ShortShift, 1) + 2;
UpdateHistory(_longHistory, longValue, longCapacity);
UpdateHistory(_shortHistory, shortValue, shortCapacity);
if (!TryGetShiftedValue(_longHistory, 1, out var longPrev) ||
!TryGetShiftedValue(_longHistory, LongShift, out var longShifted) ||
!TryGetShiftedValue(_shortHistory, 1, out var shortPrev) ||
!TryGetShiftedValue(_shortHistory, ShortShift, out var shortShifted))
{
return;
}
if (ManageExistingPosition(candle, longPrev, longShifted))
return;
if (!IsFormedAndOnlineAndAllowTrading())
return;
if (Position == 0)
{
var bullish = longPrev > longShifted && shortPrev > shortShifted;
var bearish = longPrev < longShifted && shortPrev < shortShifted;
if (bullish)
{
EnterLong(candle);
}
else if (bearish)
{
EnterShort(candle);
}
}
}
private bool ManageExistingPosition(ICandleMessage candle, decimal longPrev, decimal longShifted)
{
if (Position > 0)
{
if (longPrev < longShifted)
{
SellMarket(Position);
ResetPositionState();
return true;
}
if (UpdateLongStops(candle))
{
SellMarket(Position);
ResetPositionState();
return true;
}
}
else if (Position < 0)
{
if (longPrev > longShifted)
{
BuyMarket(Math.Abs(Position));
ResetPositionState();
return true;
}
if (UpdateShortStops(candle))
{
BuyMarket(Math.Abs(Position));
ResetPositionState();
return true;
}
}
return false;
}
private void EnterLong(ICandleMessage candle)
{
var volume = Volume;
if (volume <= 0m)
return;
BuyMarket(volume);
var step = GetPoint();
var price = candle.ClosePrice;
_entryPrice = price;
_stopPrice = StopLossPoints > 0m ? price - StopLossPoints * step : null;
}
private void EnterShort(ICandleMessage candle)
{
var volume = Volume;
if (volume <= 0m)
return;
SellMarket(volume);
var step = GetPoint();
var price = candle.ClosePrice;
_entryPrice = price;
_stopPrice = StopLossPoints > 0m ? price + StopLossPoints * step : null;
}
private bool UpdateLongStops(ICandleMessage candle)
{
if (_entryPrice is not decimal entry)
return false;
var step = GetPoint();
if (step <= 0m)
return false;
if (_stopPrice is null && StopLossPoints > 0m)
{
_stopPrice = entry - StopLossPoints * step;
}
if (TrailingStopPoints >= 100m)
{
var trailingDistance = TrailingStopPoints * step;
if (trailingDistance > 0m)
{
var currentPrice = candle.ClosePrice;
if (currentPrice - entry > trailingDistance)
{
var newStop = currentPrice - trailingDistance;
if (_stopPrice is not decimal existing || currentPrice - existing > trailingDistance)
{
_stopPrice = newStop;
}
}
}
}
if (_stopPrice is not decimal stop)
return false;
return candle.LowPrice <= stop;
}
private bool UpdateShortStops(ICandleMessage candle)
{
if (_entryPrice is not decimal entry)
return false;
var step = GetPoint();
if (step <= 0m)
return false;
if (_stopPrice is null && StopLossPoints > 0m)
{
_stopPrice = entry + StopLossPoints * step;
}
if (TrailingStopPoints >= 100m)
{
var trailingDistance = TrailingStopPoints * step;
if (trailingDistance > 0m)
{
var currentPrice = candle.ClosePrice;
if (entry - currentPrice > trailingDistance)
{
var newStop = currentPrice + trailingDistance;
if (_stopPrice is not decimal existing || existing - currentPrice > trailingDistance)
{
_stopPrice = newStop;
}
}
}
}
if (_stopPrice is not decimal stop)
return false;
return candle.HighPrice >= stop;
}
private void ResetPositionState()
{
_entryPrice = null;
_stopPrice = null;
}
private static void UpdateHistory(List<decimal> history, decimal value, int capacity)
{
history.Add(value);
if (history.Count > capacity)
{
history.RemoveAt(0);
}
}
private static bool TryGetShiftedValue(List<decimal> history, int shift, out decimal value)
{
value = 0m;
if (shift < 0)
return false;
var index = history.Count - 1 - shift;
if (index < 0 || index >= history.Count)
return false;
value = history[index];
return true;
}
private decimal GetPoint()
{
var step = Security?.PriceStep;
if (step is decimal priceStep && priceStep > 0m)
return priceStep;
return 0.0001m;
}
}
import clr
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Indicators")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates
from StockSharp.Algo.Strategies import Strategy
from StockSharp.Algo.Indicators import SimpleMovingAverage
class brandy_v12_strategy(Strategy):
def __init__(self):
super(brandy_v12_strategy, self).__init__()
self._long_period = self.Param("LongPeriod", 70) \
.SetDisplay("Long SMA Period", "Period for the longer moving average", "Indicators")
self._long_shift = self.Param("LongShift", 5) \
.SetDisplay("Long SMA Shift", "Backward shift applied to the longer SMA", "Indicators")
self._short_period = self.Param("ShortPeriod", 20) \
.SetDisplay("Short SMA Period", "Period for the shorter moving average", "Indicators")
self._short_shift = self.Param("ShortShift", 5) \
.SetDisplay("Short SMA Shift", "Backward shift applied to the shorter SMA", "Indicators")
self._stop_loss_points = self.Param("StopLossPoints", 50.0) \
.SetDisplay("Stop Loss (points)", "Initial stop-loss distance expressed in price steps", "Risk")
self._trailing_stop_points = self.Param("TrailingStopPoints", 150.0) \
.SetDisplay("Trailing Stop (points)", "Trailing stop distance in price steps. Activates when >= 100", "Risk")
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(2))) \
.SetDisplay("Candle Type", "Candle series processed by the strategy", "General")
self._long_history = []
self._short_history = []
self._entry_price = None
self._stop_price = None
@property
def LongPeriod(self):
return self._long_period.Value
@property
def LongShift(self):
return self._long_shift.Value
@property
def ShortPeriod(self):
return self._short_period.Value
@property
def ShortShift(self):
return self._short_shift.Value
@property
def StopLossPoints(self):
return self._stop_loss_points.Value
@property
def TrailingStopPoints(self):
return self._trailing_stop_points.Value
@property
def CandleType(self):
return self._candle_type.Value
def OnStarted2(self, time):
super(brandy_v12_strategy, self).OnStarted2(time)
self._long_sma = SimpleMovingAverage()
self._long_sma.Length = self.LongPeriod
self._short_sma = SimpleMovingAverage()
self._short_sma.Length = self.ShortPeriod
subscription = self.SubscribeCandles(self.CandleType)
subscription.Bind(self._long_sma, self._short_sma, self.ProcessCandle).Start()
def ProcessCandle(self, candle, long_value, short_value):
if candle.State != CandleStates.Finished:
return
long_value = float(long_value)
short_value = float(short_value)
if not self._long_sma.IsFormed or not self._short_sma.IsFormed:
return
long_capacity = max(self.LongShift, 1) + 2
short_capacity = max(self.ShortShift, 1) + 2
self._update_history(self._long_history, long_value, long_capacity)
self._update_history(self._short_history, short_value, short_capacity)
long_prev = self._get_shifted(self._long_history, 1)
long_shifted = self._get_shifted(self._long_history, self.LongShift)
short_prev = self._get_shifted(self._short_history, 1)
short_shifted = self._get_shifted(self._short_history, self.ShortShift)
if long_prev is None or long_shifted is None or short_prev is None or short_shifted is None:
return
if self._manage_existing_position(candle, long_prev, long_shifted):
return
if not self.IsFormedAndOnlineAndAllowTrading():
return
if self.Position == 0:
bullish = long_prev > long_shifted and short_prev > short_shifted
bearish = long_prev < long_shifted and short_prev < short_shifted
if bullish:
self._enter_long(candle)
elif bearish:
self._enter_short(candle)
def _manage_existing_position(self, candle, long_prev, long_shifted):
if self.Position > 0:
if long_prev < long_shifted:
self.SellMarket(self.Position)
self._reset_position_state()
return True
if self._update_long_stops(candle):
self.SellMarket(self.Position)
self._reset_position_state()
return True
elif self.Position < 0:
if long_prev > long_shifted:
self.BuyMarket(Math.Abs(self.Position))
self._reset_position_state()
return True
if self._update_short_stops(candle):
self.BuyMarket(Math.Abs(self.Position))
self._reset_position_state()
return True
return False
def _enter_long(self, candle):
vol = self.Volume
if vol <= 0:
return
self.BuyMarket(vol)
step = self._get_point()
price = float(candle.ClosePrice)
self._entry_price = price
sl = float(self.StopLossPoints)
self._stop_price = price - sl * step if sl > 0 else None
def _enter_short(self, candle):
vol = self.Volume
if vol <= 0:
return
self.SellMarket(vol)
step = self._get_point()
price = float(candle.ClosePrice)
self._entry_price = price
sl = float(self.StopLossPoints)
self._stop_price = price + sl * step if sl > 0 else None
def _update_long_stops(self, candle):
if self._entry_price is None:
return False
step = self._get_point()
if step <= 0:
return False
entry = self._entry_price
sl_pts = float(self.StopLossPoints)
if self._stop_price is None and sl_pts > 0:
self._stop_price = entry - sl_pts * step
trail_pts = float(self.TrailingStopPoints)
if trail_pts >= 100:
trailing_dist = trail_pts * step
if trailing_dist > 0:
current_price = float(candle.ClosePrice)
if current_price - entry > trailing_dist:
new_stop = current_price - trailing_dist
if self._stop_price is None or current_price - self._stop_price > trailing_dist:
self._stop_price = new_stop
if self._stop_price is None:
return False
return float(candle.LowPrice) <= self._stop_price
def _update_short_stops(self, candle):
if self._entry_price is None:
return False
step = self._get_point()
if step <= 0:
return False
entry = self._entry_price
sl_pts = float(self.StopLossPoints)
if self._stop_price is None and sl_pts > 0:
self._stop_price = entry + sl_pts * step
trail_pts = float(self.TrailingStopPoints)
if trail_pts >= 100:
trailing_dist = trail_pts * step
if trailing_dist > 0:
current_price = float(candle.ClosePrice)
if entry - current_price > trailing_dist:
new_stop = current_price + trailing_dist
if self._stop_price is None or self._stop_price - current_price > trailing_dist:
self._stop_price = new_stop
if self._stop_price is None:
return False
return float(candle.HighPrice) >= self._stop_price
def _reset_position_state(self):
self._entry_price = None
self._stop_price = None
def _update_history(self, history, value, capacity):
history.append(value)
while len(history) > capacity:
history.pop(0)
def _get_shifted(self, history, shift):
if shift < 0:
return None
index = len(history) - 1 - shift
if index < 0 or index >= len(history):
return None
return history[index]
def _get_point(self):
if self.Security is not None:
ps = self.Security.PriceStep
if ps is not None and float(ps) > 0:
return float(ps)
return 0.0001
def OnReseted(self):
super(brandy_v12_strategy, self).OnReseted()
self._long_history = []
self._short_history = []
self._entry_price = None
self._stop_price = None
def CreateClone(self):
return brandy_v12_strategy()