Estrategia Brandy v1.2 (C#)
Descripción general
La Estrategia Brandy v1.2 es una conversión directa del MetaTrader 4 asesor experto "Brandy_v1_2.mq4" al marco de estrategia de alto nivel StockSharp. El sistema evalúa un par de promedios móviles simples (SMA) desplazados calculados sobre el precio de cierre de la serie de velas configuradas. Las nuevas posiciones se abren solo cuando las SMA a largo y corto plazo muestran un impulso sincronizado en la misma dirección, mientras que las operaciones existentes se gestionan mediante inversiones de pendiente, niveles fijos de stop-loss y un módulo de trailing stop opcional.
El script original MQL se ejecutó exactamente una vez por barra completada. Este puerto procesa StockSharp velas terminadas de la misma manera, lo que garantiza que todas las decisiones comerciales se basen en datos cerrados sin depender de barras parcialmente formadas.
Lógica de trading
- Preparación de indicadores
- Se calculan dos SMA: una línea de base más larga (
LongPeriod) y una línea de confirmación más corta (ShortPeriod).
- A cada promedio se accede dos veces: el valor de la barra anterior (shift = 1) y otro valor desplazado por
LongShift/ShortShift barras respectivamente. Esto reproduce las llamadas iMA(..., shift) presentes en el EA original.
- Reglas de entrada
- Compre cuando el valor de la barra anterior de ambas SMA sea mayor que sus contrapartes desplazadas (ambas pendientes apuntando hacia arriba) y no haya ninguna posición abierta.
- Vender cuando el valor de la barra anterior de ambas SMA sea menor que sus contrapartes desplazadas (ambas pendientes apuntando hacia abajo) y no haya ninguna posición abierta.
- Solo puede haber una posición activa en cualquier momento, reflejando la verificación
k == 0 en la fuente MQL.
- Reglas de salida
- Inversión de pendiente: una posición larga abierta se liquida si el SMA largo baja (
longPrev < longShifted), mientras que una posición corta se cubre cuando el SMA largo sube (longPrev > longShifted).
- Stop-loss fijo: al entrar, la estrategia almacena un nivel de stop inicial compensado en
StopLossPoints × PriceStep del precio de entrada. El stop se compara con el rango alto/bajo de la vela, aproximando la gestión del nivel de tick del asesor original.
- Trailing stop: si
TrailingStopPoints ≥ 100, la estrategia replica la lógica de seguimiento (parámetro ts). Una vez que el beneficio flotante excede la distancia de seguimiento, el stop se lleva a currentPrice ± trailingDistance, siempre que el nuevo nivel esté más cerca del precio que el stop existente. Este comportamiento coincide con las llamadas OrderModify en el experto MQL.
Parámetros
| Parámetro |
Predeterminado |
Descripción |
LongPeriod |
70 |
Longitud del SMA principal (p1 en MQL). Debe ser > 0. |
LongShift |
5 |
Desplazamiento hacia atrás aplicado a la comparación larga SMA (s1). Puede ser cero. |
ShortPeriod |
20 |
Duración de la confirmación SMA (p2). Debe ser > 0. |
ShortShift |
5 |
Desplazamiento hacia atrás para el corto SMA (s2). Puede ser cero. |
StopLossPoints |
50 |
Distancia de parada fija en pasos de precio (sl). Establezca en 0 para desactivar la parada brusca. |
TrailingStopPoints |
150 |
Distancia de seguimiento en pasos de precio (ts). El seguimiento se activa solo cuando el valor es ≥ 100, reflejando el umbral original. |
Volume |
0.1 |
Volumen de pedido utilizado para las entradas (lots). |
CandleType |
plazo de 15 minutos |
Serie de velas procesadas por la estrategia (configurable por el usuario). |
Dependencia del paso del precio
Ambos parámetros de parada operan en puntos del instrumento. El método auxiliar los convierte en deltas de precios absolutos mediante Security.PriceStep. Si la fuente de datos no proporciona PriceStep, la estrategia vuelve a recurrir a 0.0001 por lo que la lógica sigue funcionando, aunque con una conversión aproximada. Verifique siempre los metadatos del símbolo en StockSharp antes de su uso en vivo.
Gestión del riesgo
- Parada completa: almacenado internamente y validado con cada vela terminada. Cuando el precio viola el tope, la llamada
SellMarket/BuyMarket correspondiente cierra toda la posición.
- Trailing stop: sigue las condiciones exactas del EA original, moviendo el stop solo cuando la ganancia actual excede la distancia de seguimiento y el stop existente aún está más lejos que esa distancia.
- Posición única: el algoritmo nunca hace pirámides; tiene una única posición larga, una única posición corta o es plano.
Notas de implementación
- El estado (precio de entrada, nivel de parada, SMA historiales) se restablece automáticamente en
OnReseted(), lo que garantiza pruebas retrospectivas y reinicios limpios.
- Los historiales de los indicadores se almacenan en buffers cortos para reproducir las compensaciones
iMA(..., shift) sin llamar a GetValue().
- Todos los comentarios en línea permanecen en inglés según lo exigen las pautas del repositorio.
- No se proporciona ninguna contraparte de Python. Solo la implementación de alto nivel de C# se entrega en
CS/BrandyV12Strategy.cs según lo solicitado.
Uso
- Coloque la estrategia en una solución StockSharp, seleccione el instrumento deseado y asegúrese de que los datos de la vela coincidan con el período de tiempo especificado por
CandleType.
- Configure los parámetros en la interfaz de usuario o mediante código. Los valores predeterminados replican los valores originales de MT4.
- Inicia la estrategia. Se suscribirá a la serie de velas, dibujará ambas SMA en el gráfico y gestionará las operaciones automáticamente.
Descargo de responsabilidad: Este port está destinado a fines educativos y de prueba. Valide siempre el comportamiento en sesiones de negociación históricas y en papel antes de implementarlo en mercados reales.
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()