Brandy v1.2-Strategie (C#)
Überblick
Die Brandy v1.2-Strategie ist eine direkte Konvertierung des MetaTrader 4 Expert Advisors „Brandy_v1_2.mq4“ in das StockSharp High-Level-Strategie-Framework. Das System wertet ein Paar verschobener einfacher gleitender Durchschnitte (SMAs) aus, die auf der Grundlage des Schlusskurses der konfigurierten Kerzenserie berechnet werden. Neue Positionen werden nur eröffnet, wenn sowohl der langfristige als auch der kurzfristige SMA ein synchronisiertes Momentum in die gleiche Richtung aufweisen, während bestehende Geschäfte mithilfe von Steigungsumkehrungen, festen Stop-Loss-Levels und einem optionalen Trailing-Stop-Modul verwaltet werden.
Das ursprüngliche MQL-Skript wurde genau einmal pro abgeschlossenem Balken ausgeführt. Dieser Port verarbeitet fertige StockSharp-Kerzen auf die gleiche Weise und stellt so sicher, dass alle Handelsentscheidungen auf geschlossenen Daten basieren, ohne sich auf teilweise geformte Balken zu verlassen.
Handelslogik
- Indikatorvorbereitung
- Es werden zwei SMAs berechnet: eine längere Basislinie (
LongPeriod) und eine kürzere Bestätigungslinie (ShortPeriod).
- Auf jeden Durchschnitt wird zweimal zugegriffen: auf den Wert des vorherigen Balkens (Verschiebung = 1) und einen anderen um jeweils
LongShift/ShortShift Balken verschobenen Wert. Dadurch werden die im ursprünglichen EA vorhandenen iMA(..., shift)-Aufrufe reproduziert.
- Eintrittsregeln
- Kaufen, wenn der Wert beider SMAs auf dem vorherigen Balken größer ist als ihre verschobenen Gegenstücke (beide Steigungen zeigen nach oben) und keine Position offen ist.
- Verkaufen, wenn der Wert beider SMAs auf dem vorherigen Balken niedriger ist als der ihrer verschobenen Gegenstücke (beide Steigungen zeigen nach unten) und keine Position offen ist.
- Es kann immer nur eine Position aktiv sein, was die
k == 0-Prüfung in der MQL-Quelle widerspiegelt.
- Ausgangsregeln
- Steigungsumkehr: Eine offene Long-Position wird liquidiert, wenn die Long-Position SMA nach unten geht (
longPrev < longShifted), während eine Short-Position gedeckt wird, wenn die Long-Position SMA nach oben geht (longPrev > longShifted).
- Fester Stop-Loss: Beim Einstieg speichert die Strategie einen anfänglichen Stop-Level, der um
StopLossPoints × PriceStep vom Einstiegspreis abweicht. Der Stop wird anhand des Hoch-/Tief-Bereichs der Kerze überprüft, was annähernd dem Tick-Level-Management des ursprünglichen Beraters entspricht.
- Trailing Stop: Bei
TrailingStopPoints ≥ 100 repliziert die Strategie die Trailing-Logik (Parameter ts). Sobald der variable Gewinn die Trailing-Distanz überschreitet, wird der Stop auf currentPrice ± trailingDistance gezogen, sofern das neue Niveau näher am Preis liegt als der bestehende Stop. Dieses Verhalten entspricht den OrderModify-Aufrufen im MQL-Experten.
Parameter
| Parameter |
Standard |
Beschreibung |
LongPeriod |
70 |
Länge des primären SMA (p1 in MQL). Muss > 0 sein. |
LongShift |
5 |
Auf den langen SMA-Vergleich (s1) wird eine Rückwärtsverschiebung angewendet. Kann Null sein. |
ShortPeriod |
20 |
Länge der Bestätigung SMA (p2). Muss > 0 sein. |
ShortShift |
5 |
Rückwärtsverschiebung für das kurze SMA (s2). Kann Null sein. |
StopLossPoints |
50 |
Feste Stoppdistanz in Preisschritten (sl). Auf 0 setzen, um den harten Stopp zu deaktivieren. |
TrailingStopPoints |
150 |
Nachlaufdistanz in Preisschritten (ts). Trailing wird nur aktiviert, wenn der Wert ≥ 100 ist, was den ursprünglichen Schwellenwert widerspiegelt. |
Volume |
0,1 |
Für Einträge verwendetes Bestellvolumen (lots). |
CandleType |
15-minütiger Zeitrahmen |
Von der Strategie verarbeitete Kerzenserie (vom Benutzer konfigurierbar). |
Preisschrittabhängigkeit
Beide Stoppparameter wirken in Instrumentenpunkten. Die Hilfsmethode wandelt sie über Security.PriceStep in absolute Preisdeltas um. Wenn die Datenquelle PriceStep nicht bereitstellt, greift die Strategie auf 0.0001 zurück, sodass die Logik weiterhin funktioniert, wenn auch mit einer ungefähren Konvertierung. Überprüfen Sie vor der Live-Nutzung immer die Symbolmetadaten in StockSharp.
Risikomanagement
- Hard Stop: intern gespeichert und anhand jeder fertigen Kerze validiert. Wenn der Preis den Stop überschreitet, schließt der entsprechende
SellMarket/BuyMarket-Anruf die gesamte Position.
- Trailing Stop: folgt den genauen Bedingungen des ursprünglichen EA und verschiebt den Stop nur, wenn der aktuelle Gewinn die Trailing-Distanz überschreitet und der bestehende Stop immer noch weiter als diese Distanz entfernt ist.
- Einzelne Position: Der Algorithmus bildet niemals eine Pyramide; Es gibt entweder eine einzelne Long-Position, eine einzelne Short-Position oder es ist flach.
Implementierungshinweise
- Der Status (Einstiegspreis, Stop-Level, SMA-Historien) wird am
OnReseted() automatisch zurückgesetzt, um saubere Backtests und Neustarts zu gewährleisten.
- Indikatorverläufe werden in kurzen Rollpuffern gespeichert, um die
iMA(..., shift)-Offsets ohne Aufruf von GetValue() zu reproduzieren.
- Alle Inline-Kommentare bleiben gemäß den Repository-Richtlinien auf Englisch.
- Es wird kein Python-Gegenstück bereitgestellt. Nur die C#-High-Level-Implementierung wird wie angefordert in
CS/BrandyV12Strategy.cs bereitgestellt.
Nutzung
- Platzieren Sie die Strategie in einer StockSharp-Lösung, wählen Sie das gewünschte Instrument aus und stellen Sie sicher, dass die Kerzendaten mit dem durch
CandleType angegebenen Zeitrahmen übereinstimmen.
- Konfigurieren Sie die Parameter in der Benutzeroberfläche oder per Code. Die Standardwerte replizieren die ursprünglichen MT4-Werte.
- Starten Sie die Strategie. Es abonniert die Kerzenserie, zeichnet beide SMAs auf dem Chart und verwaltet Trades automatisch.
Haftungsausschluss: Dieser Port ist für Bildungs- und Testzwecke gedacht. Überprüfen Sie immer das Verhalten bei historischen und Papierhandelssitzungen, bevor Sie es auf Live-Märkten einsetzen.
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()