Brandy v1.2 Strategy (C#)
Overview
The Brandy v1.2 Strategy is a direct conversion of the MetaTrader 4 expert advisor "Brandy_v1_2.mq4" into the StockSharp high-level strategy framework. The system evaluates a pair of displaced simple moving averages (SMAs) calculated on the closing price of the configured candle series. New positions are opened only when both the long-term and short-term SMAs show synchronized momentum in the same direction, while existing trades are managed using slope reversals, fixed stop-loss levels, and an optional trailing stop module.
The original MQL script executed exactly once per completed bar. This port processes finished StockSharp candles in the same fashion, ensuring that all trading decisions are based on closed data without relying on partially formed bars.
Trading Logic
- Indicator preparation
- Two SMAs are computed: a longer baseline (
LongPeriod) and a shorter confirmation line (ShortPeriod).
- Each average is accessed twice: the value from the previous bar (shift = 1) and another value displaced by
LongShift/ShortShift bars respectively. This reproduces the iMA(..., shift) calls present in the original EA.
- Entry rules
- Buy when the previous-bar value of both SMAs is greater than their shifted counterparts (both slopes pointing upward) and no position is open.
- Sell when the previous-bar value of both SMAs is lower than their shifted counterparts (both slopes pointing downward) and no position is open.
- Only one position can be active at any time, mirroring the
k == 0 check in the MQL source.
- Exit rules
- Slope reversal: an open long position is liquidated if the long SMA turns down (
longPrev < longShifted), while a short position is covered when the long SMA turns up (longPrev > longShifted).
- Fixed stop-loss: upon entering, the strategy stores an initial stop level offset by
StopLossPoints × PriceStep from the entry price. The stop is checked against the candle’s high/low range, approximating the tick-level management of the original advisor.
- Trailing stop: if
TrailingStopPoints ≥ 100, the strategy replicates the trailing logic (ts parameter). Once the floating profit exceeds the trailing distance, the stop is pulled to currentPrice ± trailingDistance, provided the new level is closer to price than the existing stop. This behavior matches the OrderModify calls in the MQL expert.
Parameters
| Parameter |
Default |
Description |
LongPeriod |
70 |
Length of the primary SMA (p1 in MQL). Must be > 0. |
LongShift |
5 |
Backward shift applied to the long SMA comparison (s1). Can be zero. |
ShortPeriod |
20 |
Length of the confirmation SMA (p2). Must be > 0. |
ShortShift |
5 |
Backward shift for the short SMA (s2). Can be zero. |
StopLossPoints |
50 |
Fixed stop distance in price steps (sl). Set to 0 to disable the hard stop. |
TrailingStopPoints |
150 |
Trailing distance in price steps (ts). Trailing activates only when the value is ≥ 100, mirroring the original threshold. |
Volume |
0.1 |
Order volume used for entries (lots). |
CandleType |
15-minute time frame |
Candle series processed by the strategy (user configurable). |
Price step dependency
Both stop parameters operate in instrument points. The helper method converts them to absolute price deltas via Security.PriceStep. If the data source does not supply PriceStep, the strategy falls back to 0.0001 so the logic continues to work, albeit with an approximate conversion. Always verify the symbol metadata in StockSharp before live usage.
Risk Management
- Hard stop: stored internally and validated against every finished candle. When price violates the stop, the corresponding
SellMarket/BuyMarket call closes the entire position.
- Trailing stop: follows the exact conditions of the original EA, moving the stop only when the current profit exceeds the trailing distance and the existing stop is still farther than that distance.
- Single position: the algorithm never pyramids; it either has a single long position, a single short position, or is flat.
Implementation Notes
- State (entry price, stop level, SMA histories) resets automatically on
OnReseted() ensuring clean backtests and restarts.
- Indicator histories are stored in short rolling buffers to reproduce the
iMA(..., shift) offsets without calling GetValue().
- All inline comments remain in English as required by the repository guidelines.
- No Python counterpart is provided. Only the C# high-level implementation is delivered in
CS/BrandyV12Strategy.cs as requested.
Usage
- Place the strategy into a StockSharp solution, select the desired instrument, and ensure the candle data matches the timeframe specified by
CandleType.
- Configure the parameters in the UI or via code. Defaults replicate the original MT4 values.
- Start the strategy. It will subscribe to the candle series, draw both SMAs on the chart, and manage trades automatically.
Disclaimer: This port is intended for educational and testing purposes. Always validate the behavior on historical and paper trading sessions before deploying to live markets.
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()