Bruno-Strategie
Der Bruno-Expertenberater ist ein Trendfolgesystem, das ursprünglich für MetaTrader 5 geschrieben wurde. Der Port behält dieselbe Bestätigungskette: Average Directional Index (ADX) mit Richtungsbewegung, ein Paar exponentieller gleitender Durchschnitte (EMA 8/21), MACD (13, 34, 8), ein Stochastischer Oszillator (21, 3, 3) und die Steigung eines Parabolic SAR (0.055, 0.21). Jeder Filter, der mit der Richtung übereinstimmt, multipliziert die Ordergröße mit einem konfigurierbaren Faktor. Wenn sowohl Long- als auch Short-Signale auf derselben Kerze verstärkt werden, wird kein Trade durchgeführt, um widersprüchliche Orders zu vermeiden.
Handelslogik
- Richtungsbias
- Long-Druck wird verstärkt wenn
+DI > -DIund+DI > 20. - Short-Druck wird verstärkt wenn
+DI < -DIund+DI < 40.
- Long-Druck wird verstärkt wenn
- Momentum-Ausrichtung
- Long-Präferenz erfordert EMA(8) über EMA(21), Stochastisches %K über %D und %K unter der Überkauft-Schwelle (Standard 80).
- Short-Präferenz erfordert EMA(8) unter EMA(21), Stochastisches %K unter %D und %K über der Überverkauft-Schwelle (Standard 20).
- MACD-Filter
- Long-Bias: MACD-Histogramm über null und MACD-Hauptlinie über der Signallinie.
- Short-Bias: MACD-Histogramm unter null und MACD-Hauptlinie unter der Signallinie.
- Parabolic SAR-Steigung
- Long-Bias wird verstärkt wenn die vorherigen SAR-Werte steigen während EMA(8) > EMA(21).
- Short-Bias wird verstärkt wenn die vorherigen SAR-Werte fallen während EMA(8) < EMA(21).
Jede erfüllte Bedingung multipliziert die Basis-Lotgröße mit SignalMultiplier (Standard 1.6). Nur eine Seite kann gleichzeitig aktiv sein. Wenn ein abschließendes Signal generiert wird, schließt die Strategie jede entgegengesetzte Position, sendet die Marktorder mit dem multiplizierten Volumen und speichert den aktuellen Schlusskurs als Einstiegspreis.
Positionsmanagement
- Stop-Loss / Take-Profit – feste Abstände in angepassten Pips, entsprechend der MetaTrader-Version. Wenn ein Level intrabar getroffen wird, wird die Position sofort geschlossen.
- Trailing-Stop – aktiviert, sobald der schwebende Gewinn
TrailingStop + TrailingStepPips übersteigt. Der Stop wird dannTrailingStopPips hinter dem Preis platziert und rückt nur vor, wenn der Gewinn um mindestensTrailingStepweitere Pips zunimmt. - Konfliktbehandlung – wenn sowohl Long- als auch Short-Filter auf derselben Kerze ausgelöst werden, wird kein neuer Trade eingegangen.
Parameter
| Gruppe | Name | Beschreibung |
|---|---|---|
| Handel | BaseVolume |
Anfängliche Lotgröße vor Multiplikatoren. |
| Handel | SignalMultiplier |
Volumen-Multiplikator, der von jedem übereinstimmenden Filter angewendet wird. |
| Risikomanagement | StopLossPips / TakeProfitPips |
Schutzabstände in angepassten Pips. Auf null setzen zum Deaktivieren. |
| Risikomanagement | TrailingStopPips / TrailingStepPips |
Trailing-Abstand und Mindestschritt in angepassten Pips. |
| Indikatoren | AdxPeriod, AdxPositiveThreshold, AdxNegativeThreshold |
ADX-Länge und DI-Schwellenwerte. |
| Indikatoren | FastEmaPeriod, SlowEmaPeriod |
EMA-Längen für die Trendbestätigung. |
| Indikatoren | MacdFastPeriod, MacdSlowPeriod, MacdSignalPeriod |
MACD-Konfiguration. |
| Indikatoren | StochasticPeriod, StochasticKsmoothing, StochasticDsmoothing, StochasticOverbought, StochasticOversold |
Stochastischer Oszillator-Einstellungen. |
| Allgemein | CandleType |
Zeitrahmen für die gesamte Signalkette (Standard 1 Stunde). |
Hinweise
- Die angepasste Pip-Größe folgt der MetaTrader-Konvention: Instrumente mit 3 oder 5 Dezimalstellen werden mit 10 multipliziert.
- Parabolic SAR arbeitet mit Beschleunigungsschritt
0.055und Maximum0.21, entsprechend den Expertenberater-Standardwerten. - Der Port behält den ursprünglichen Geldmanagement-Stil (Volumen-Stacking) bei, aggregiert das Exposure jedoch in einer einzigen StockSharp-Position.
using System;
using System.Collections.Generic;
using System.Linq;
using Ecng.Common;
using StockSharp.Algo.Strategies;
using StockSharp.BusinessEntities;
using StockSharp.Messages;
namespace StockSharp.Samples.Strategies;
/// <summary>
/// Bruno multi-filter trend strategy.
/// </summary>
public class BrunoStrategy : Strategy
{
private readonly StrategyParam<decimal> _baseVolume;
private readonly StrategyParam<decimal> _signalMultiplier;
private readonly StrategyParam<int> _stopLossPips;
private readonly StrategyParam<int> _takeProfitPips;
private readonly StrategyParam<int> _trailingStopPips;
private readonly StrategyParam<int> _trailingStepPips;
private readonly StrategyParam<int> _adxPeriod;
private readonly StrategyParam<decimal> _adxPositiveThreshold;
private readonly StrategyParam<decimal> _adxNegativeThreshold;
private readonly StrategyParam<int> _fastEmaPeriod;
private readonly StrategyParam<int> _slowEmaPeriod;
private readonly StrategyParam<int> _macdFastPeriod;
private readonly StrategyParam<int> _macdSlowPeriod;
private readonly StrategyParam<int> _macdSignalPeriod;
private readonly StrategyParam<int> _stochasticPeriod;
private readonly StrategyParam<int> _stochasticKsmoothing;
private readonly StrategyParam<int> _stochasticDsmoothing;
private readonly StrategyParam<decimal> _stochasticOverbought;
private readonly StrategyParam<decimal> _stochasticOversold;
private readonly StrategyParam<DataType> _candleType;
private readonly List<Bar> _bars = [];
private readonly List<decimal> _rawK = [];
private readonly List<decimal> _smoothK = [];
private readonly List<decimal> _sarValues = [];
private decimal? _fastEma;
private decimal? _slowEma;
private decimal? _macdFast;
private decimal? _macdSlow;
private decimal? _macdSignal;
private SarState _sar;
private decimal _entryPrice;
private decimal? _stopPrice;
private decimal? _takePrice;
private decimal? _bestPrice;
private DateTimeOffset? _entryCandleTime;
public decimal BaseVolume { get => _baseVolume.Value; set => _baseVolume.Value = value; }
public decimal SignalMultiplier { get => _signalMultiplier.Value; set => _signalMultiplier.Value = value; }
public int StopLossPips { get => _stopLossPips.Value; set => _stopLossPips.Value = value; }
public int TakeProfitPips { get => _takeProfitPips.Value; set => _takeProfitPips.Value = value; }
public int TrailingStopPips { get => _trailingStopPips.Value; set => _trailingStopPips.Value = value; }
public int TrailingStepPips { get => _trailingStepPips.Value; set => _trailingStepPips.Value = value; }
public int AdxPeriod { get => _adxPeriod.Value; set => _adxPeriod.Value = value; }
public decimal AdxPositiveThreshold { get => _adxPositiveThreshold.Value; set => _adxPositiveThreshold.Value = value; }
public decimal AdxNegativeThreshold { get => _adxNegativeThreshold.Value; set => _adxNegativeThreshold.Value = value; }
public int FastEmaPeriod { get => _fastEmaPeriod.Value; set => _fastEmaPeriod.Value = value; }
public int SlowEmaPeriod { get => _slowEmaPeriod.Value; set => _slowEmaPeriod.Value = value; }
public int MacdFastPeriod { get => _macdFastPeriod.Value; set => _macdFastPeriod.Value = value; }
public int MacdSlowPeriod { get => _macdSlowPeriod.Value; set => _macdSlowPeriod.Value = value; }
public int MacdSignalPeriod { get => _macdSignalPeriod.Value; set => _macdSignalPeriod.Value = value; }
public int StochasticPeriod { get => _stochasticPeriod.Value; set => _stochasticPeriod.Value = value; }
public int StochasticKsmoothing { get => _stochasticKsmoothing.Value; set => _stochasticKsmoothing.Value = value; }
public int StochasticDsmoothing { get => _stochasticDsmoothing.Value; set => _stochasticDsmoothing.Value = value; }
public decimal StochasticOverbought { get => _stochasticOverbought.Value; set => _stochasticOverbought.Value = value; }
public decimal StochasticOversold { get => _stochasticOversold.Value; set => _stochasticOversold.Value = value; }
public DataType CandleType { get => _candleType.Value; set => _candleType.Value = value; }
public BrunoStrategy()
{
_baseVolume = Param(nameof(BaseVolume), 0.1m).SetGreaterThanZero();
_signalMultiplier = Param(nameof(SignalMultiplier), 1.6m).SetGreaterThanZero();
_stopLossPips = Param(nameof(StopLossPips), 50).SetNotNegative();
_takeProfitPips = Param(nameof(TakeProfitPips), 100).SetNotNegative();
_trailingStopPips = Param(nameof(TrailingStopPips), 30).SetNotNegative();
_trailingStepPips = Param(nameof(TrailingStepPips), 5).SetNotNegative();
_adxPeriod = Param(nameof(AdxPeriod), 14).SetGreaterThanZero();
_adxPositiveThreshold = Param(nameof(AdxPositiveThreshold), 20m);
_adxNegativeThreshold = Param(nameof(AdxNegativeThreshold), 40m);
_fastEmaPeriod = Param(nameof(FastEmaPeriod), 8).SetGreaterThanZero();
_slowEmaPeriod = Param(nameof(SlowEmaPeriod), 21).SetGreaterThanZero();
_macdFastPeriod = Param(nameof(MacdFastPeriod), 13).SetGreaterThanZero();
_macdSlowPeriod = Param(nameof(MacdSlowPeriod), 34).SetGreaterThanZero();
_macdSignalPeriod = Param(nameof(MacdSignalPeriod), 8).SetGreaterThanZero();
_stochasticPeriod = Param(nameof(StochasticPeriod), 21).SetGreaterThanZero();
_stochasticKsmoothing = Param(nameof(StochasticKsmoothing), 3).SetGreaterThanZero();
_stochasticDsmoothing = Param(nameof(StochasticDsmoothing), 3).SetGreaterThanZero();
_stochasticOverbought = Param(nameof(StochasticOverbought), 80m);
_stochasticOversold = Param(nameof(StochasticOversold), 20m);
_candleType = Param(nameof(CandleType), TimeSpan.FromHours(1).TimeFrame());
}
public override IEnumerable<(Security sec, DataType dt)> GetWorkingSecurities()
=> [(Security, CandleType)];
protected override void OnReseted()
{
base.OnReseted();
_bars.Clear();
_rawK.Clear();
_smoothK.Clear();
_sarValues.Clear();
_fastEma = _slowEma = _macdFast = _macdSlow = _macdSignal = null;
_sar = null;
ResetProtection();
}
protected override void OnStarted2(DateTime time)
{
base.OnStarted2(time);
_sar = new(0.055m, 0.21m);
SubscribeCandles(CandleType).Bind(ProcessCandle).Start();
}
private void ProcessCandle(ICandleMessage candle)
{
if (candle.State != CandleStates.Finished)
return;
_bars.Add(new Bar(candle.HighPrice, candle.LowPrice, candle.ClosePrice));
var keep = Math.Max(Math.Max(AdxPeriod + 2, MacdSlowPeriod + MacdSignalPeriod + 2), StochasticPeriod + 10);
if (_bars.Count > keep)
_bars.RemoveRange(0, _bars.Count - keep);
_fastEma = Ema(_fastEma, candle.ClosePrice, FastEmaPeriod);
_slowEma = Ema(_slowEma, candle.ClosePrice, SlowEmaPeriod);
_macdFast = Ema(_macdFast, candle.ClosePrice, MacdFastPeriod);
_macdSlow = Ema(_macdSlow, candle.ClosePrice, MacdSlowPeriod);
var macdMain = _macdFast.Value - _macdSlow.Value;
_macdSignal = Ema(_macdSignal, macdMain, MacdSignalPeriod);
var sar = _sar.Process(candle.HighPrice, candle.LowPrice, candle.ClosePrice);
if (sar is decimal sarValue)
{
_sarValues.Add(sarValue);
if (_sarValues.Count > 4)
_sarValues.RemoveAt(0);
}
UpdateStochastic();
if (Position != 0m && ApplyProtection(candle))
return;
if (_bars.Count < AdxPeriod + 1 || _smoothK.Count < StochasticDsmoothing || _sarValues.Count < 3)
return;
var (plusDi, minusDi) = CalculateDirectionalIndex();
var k = _smoothK[^1];
var d = _smoothK.Skip(_smoothK.Count - StochasticDsmoothing).Average();
var histogram = macdMain - _macdSignal.Value;
var longDirectional = plusDi > minusDi && plusDi > AdxPositiveThreshold;
var shortDirectional = plusDi < minusDi && plusDi < AdxNegativeThreshold;
var longMomentum = _fastEma > _slowEma && k > d && k < StochasticOverbought;
var shortMomentum = _fastEma < _slowEma && k < d && k > StochasticOversold;
var longMacd = histogram > 0m && macdMain > _macdSignal;
var shortMacd = histogram < 0m && macdMain < _macdSignal;
var longSar = _sarValues[^3] < _sarValues[^2] && _sarValues[^2] < _sarValues[^1] && _fastEma > _slowEma;
var shortSar = _sarValues[^3] > _sarValues[^2] && _sarValues[^2] > _sarValues[^1] && _fastEma < _slowEma;
var (longVolume, shortVolume) = CalculateSignalVolumes(
BaseVolume, SignalMultiplier,
longDirectional, shortDirectional,
longMomentum, shortMomentum,
longMacd, shortMacd,
longSar, shortSar);
var hasLong = longVolume > BaseVolume;
var hasShort = shortVolume > BaseVolume;
if (hasLong == hasShort)
return;
if (hasLong)
Enter(Sides.Buy, longVolume, candle);
else
Enter(Sides.Sell, shortVolume, candle);
}
private void Enter(Sides side, decimal targetVolume, ICandleMessage candle)
{
var opposite = side == Sides.Buy ? Math.Max(0m, -Position) : Math.Max(0m, Position);
var volume = NormalizeVolume(targetVolume + opposite);
if (volume <= 0m)
return;
if (side == Sides.Buy)
BuyMarket(volume);
else
SellMarket(volume);
_entryPrice = candle.ClosePrice;
_entryCandleTime = candle.OpenTime;
_bestPrice = candle.ClosePrice;
var pip = GetPipSize();
_stopPrice = StopLossPips > 0
? side == Sides.Buy ? _entryPrice - StopLossPips * pip : _entryPrice + StopLossPips * pip
: null;
_takePrice = TakeProfitPips > 0
? side == Sides.Buy ? _entryPrice + TakeProfitPips * pip : _entryPrice - TakeProfitPips * pip
: null;
}
private bool ApplyProtection(ICandleMessage candle)
{
if (_entryCandleTime is DateTimeOffset entryTime && candle.OpenTime <= entryTime)
return false;
var pip = GetPipSize();
if (Position > 0m)
{
_bestPrice = _bestPrice is decimal best ? Math.Max(best, candle.HighPrice) : candle.HighPrice;
if (TrailingStopPips > 0 && TrailingStepPips >= 0 &&
_bestPrice.Value - _entryPrice >= (TrailingStopPips + TrailingStepPips) * pip)
{
var candidate = _bestPrice.Value - TrailingStopPips * pip;
if (_stopPrice is null || candidate >= _stopPrice.Value + TrailingStepPips * pip)
_stopPrice = candidate;
}
if ((_stopPrice is decimal stop && candle.LowPrice <= stop) ||
(_takePrice is decimal take && candle.HighPrice >= take))
{
SellMarket(Math.Abs(Position));
ResetProtection();
return true;
}
}
else if (Position < 0m)
{
_bestPrice = _bestPrice is decimal best ? Math.Min(best, candle.LowPrice) : candle.LowPrice;
if (TrailingStopPips > 0 && TrailingStepPips >= 0 &&
_entryPrice - _bestPrice.Value >= (TrailingStopPips + TrailingStepPips) * pip)
{
var candidate = _bestPrice.Value + TrailingStopPips * pip;
if (_stopPrice is null || candidate <= _stopPrice.Value - TrailingStepPips * pip)
_stopPrice = candidate;
}
if ((_stopPrice is decimal stop && candle.HighPrice >= stop) ||
(_takePrice is decimal take && candle.LowPrice <= take))
{
BuyMarket(Math.Abs(Position));
ResetProtection();
return true;
}
}
return false;
}
private void UpdateStochastic()
{
if (_bars.Count < StochasticPeriod)
return;
var window = _bars.Skip(_bars.Count - StochasticPeriod).ToArray();
var high = window.Max(b => b.High);
var low = window.Min(b => b.Low);
var raw = high == low ? 50m : (_bars[^1].Close - low) / (high - low) * 100m;
_rawK.Add(raw);
if (_rawK.Count > StochasticKsmoothing + StochasticDsmoothing + 2)
_rawK.RemoveAt(0);
if (_rawK.Count < StochasticKsmoothing)
return;
_smoothK.Add(_rawK.Skip(_rawK.Count - StochasticKsmoothing).Average());
if (_smoothK.Count > StochasticDsmoothing + 2)
_smoothK.RemoveAt(0);
}
private (decimal plusDi, decimal minusDi) CalculateDirectionalIndex()
{
var tr = 0m;
var plus = 0m;
var minus = 0m;
var start = _bars.Count - AdxPeriod;
for (var i = start; i < _bars.Count; i++)
{
var current = _bars[i];
var previous = _bars[i - 1];
var up = current.High - previous.High;
var down = previous.Low - current.Low;
plus += up > down && up > 0m ? up : 0m;
minus += down > up && down > 0m ? down : 0m;
tr += Math.Max(current.High - current.Low,
Math.Max(Math.Abs(current.High - previous.Close), Math.Abs(current.Low - previous.Close)));
}
return tr <= 0m ? (0m, 0m) : (plus / tr * 100m, minus / tr * 100m);
}
internal static (decimal longVolume, decimal shortVolume) CalculateSignalVolumes(
decimal baseVolume,
decimal multiplier,
bool longDirectional,
bool shortDirectional,
bool longMomentum,
bool shortMomentum,
bool longMacd,
bool shortMacd,
bool longSar,
bool shortSar)
{
var longVolume = baseVolume;
var shortVolume = baseVolume;
if (longDirectional) longVolume *= multiplier;
if (shortDirectional) shortVolume *= multiplier;
if (longMomentum) longVolume *= multiplier;
if (shortMomentum) shortVolume *= multiplier;
if (longMacd) longVolume *= multiplier;
if (shortMacd) shortVolume *= multiplier;
if (longSar) longVolume *= multiplier;
if (shortSar) shortVolume *= multiplier;
return (longVolume, shortVolume);
}
private decimal NormalizeVolume(decimal volume)
{
if (Security?.MaxVolume is decimal max && max > 0m) volume = Math.Min(volume, max);
if (Security?.MinVolume is decimal min && min > 0m) volume = Math.Max(volume, min);
if (Security?.VolumeStep is decimal step && step > 0m) volume = Math.Floor(volume / step) * step;
return volume;
}
private decimal GetPipSize()
{
var step = Security?.PriceStep ?? 0m;
if (step <= 0m) return 0.0001m;
return step is 0.00001m or 0.001m ? step * 10m : step;
}
private static decimal Ema(decimal? previous, decimal value, int period)
{
if (previous is null) return value;
var alpha = 2m / (period + 1m);
return previous.Value + alpha * (value - previous.Value);
}
private void ResetProtection()
{
_entryPrice = 0m;
_stopPrice = null;
_takePrice = null;
_bestPrice = null;
_entryCandleTime = null;
}
private readonly record struct Bar(decimal High, decimal Low, decimal Close);
private sealed class SarState(decimal step, decimal maximum)
{
private bool _initialized;
private bool _up;
private decimal _sar;
private decimal _ep;
private decimal _af;
private decimal _previousHigh;
private decimal _previousLow;
private decimal _previousClose;
private decimal _olderHigh;
private decimal _olderLow;
private int _count;
public void Reset()
{
_initialized = false;
_up = false;
_sar = _ep = _af = 0m;
_previousHigh = _previousLow = _previousClose = 0m;
_olderHigh = _olderLow = 0m;
_count = 0;
}
public decimal? Process(decimal high, decimal low, decimal close)
{
_count++;
if (_count == 1)
{
_previousHigh = _olderHigh = high;
_previousLow = _olderLow = low;
_previousClose = close;
return null;
}
if (!_initialized)
{
_up = close >= _previousClose;
_sar = _up ? Math.Min(_previousLow, low) : Math.Max(_previousHigh, high);
_ep = _up ? Math.Max(_previousHigh, high) : Math.Min(_previousLow, low);
_af = step;
_initialized = true;
Shift(high, low, close);
return _sar;
}
var next = _sar + _af * (_ep - _sar);
if (_up)
{
next = Math.Min(next, Math.Min(_previousLow, _olderLow));
if (low < next)
{
_up = false;
next = _ep;
_ep = low;
_af = step;
}
else if (high > _ep)
{
_ep = high;
_af = Math.Min(maximum, _af + step);
}
}
else
{
next = Math.Max(next, Math.Max(_previousHigh, _olderHigh));
if (high > next)
{
_up = true;
next = _ep;
_ep = high;
_af = step;
}
else if (low < _ep)
{
_ep = low;
_af = Math.Min(maximum, _af + step);
}
}
_sar = next;
Shift(high, low, close);
return _sar;
}
private void Shift(decimal high, decimal low, decimal close)
{
_olderHigh = _previousHigh;
_olderLow = _previousLow;
_previousHigh = high;
_previousLow = low;
_previousClose = close;
}
}
}
import clr
import math
clr.AddReference("StockSharp.Messages")
clr.AddReference("StockSharp.Algo")
clr.AddReference("StockSharp.Algo.Strategies")
from System import TimeSpan, Math
from StockSharp.Messages import DataType, CandleStates, Sides
from StockSharp.Algo.Strategies import Strategy
class _sar_state:
def __init__(self, step=0.055, maximum=0.21):
self.step = step
self.maximum = maximum
self.reset()
def reset(self):
self.initialized = False
self.up = False
self.sar = self.ep = self.af = 0.0
self.previous_high = self.previous_low = self.previous_close = 0.0
self.older_high = self.older_low = 0.0
self.count = 0
def process(self, high, low, close):
self.count += 1
if self.count == 1:
self.previous_high = self.older_high = high
self.previous_low = self.older_low = low
self.previous_close = close
return None
if not self.initialized:
self.up = close >= self.previous_close
self.sar = min(self.previous_low, low) if self.up else max(self.previous_high, high)
self.ep = max(self.previous_high, high) if self.up else min(self.previous_low, low)
self.af = self.step
self.initialized = True
self._shift(high, low, close)
return self.sar
value = self.sar + self.af * (self.ep - self.sar)
if self.up:
value = min(value, self.previous_low, self.older_low)
if low < value:
self.up = False
value = self.ep
self.ep = low
self.af = self.step
elif high > self.ep:
self.ep = high
self.af = min(self.maximum, self.af + self.step)
else:
value = max(value, self.previous_high, self.older_high)
if high > value:
self.up = True
value = self.ep
self.ep = high
self.af = self.step
elif low < self.ep:
self.ep = low
self.af = min(self.maximum, self.af + self.step)
self.sar = value
self._shift(high, low, close)
return self.sar
def _shift(self, high, low, close):
self.older_high = self.previous_high
self.older_low = self.previous_low
self.previous_high = high
self.previous_low = low
self.previous_close = close
class bruno_strategy(Strategy):
def __init__(self):
super(bruno_strategy, self).__init__()
self._base_volume = self.Param("BaseVolume", 0.1).SetGreaterThanZero()
self._signal_multiplier = self.Param("SignalMultiplier", 1.6).SetGreaterThanZero()
self._stop_loss = self.Param("StopLossPips", 50).SetNotNegative()
self._take_profit = self.Param("TakeProfitPips", 100).SetNotNegative()
self._trailing_stop = self.Param("TrailingStopPips", 30).SetNotNegative()
self._trailing_step = self.Param("TrailingStepPips", 5).SetNotNegative()
self._adx_period = self.Param("AdxPeriod", 14).SetGreaterThanZero()
self._adx_positive = self.Param("AdxPositiveThreshold", 20.0)
self._adx_negative = self.Param("AdxNegativeThreshold", 40.0)
self._fast_ema_period = self.Param("FastEmaPeriod", 8).SetGreaterThanZero()
self._slow_ema_period = self.Param("SlowEmaPeriod", 21).SetGreaterThanZero()
self._macd_fast_period = self.Param("MacdFastPeriod", 13).SetGreaterThanZero()
self._macd_slow_period = self.Param("MacdSlowPeriod", 34).SetGreaterThanZero()
self._macd_signal_period = self.Param("MacdSignalPeriod", 8).SetGreaterThanZero()
self._stoch_period = self.Param("StochasticPeriod", 21).SetGreaterThanZero()
self._stoch_k_smoothing = self.Param("StochasticKsmoothing", 3).SetGreaterThanZero()
self._stoch_d_smoothing = self.Param("StochasticDsmoothing", 3).SetGreaterThanZero()
self._stoch_overbought = self.Param("StochasticOverbought", 80.0)
self._stoch_oversold = self.Param("StochasticOversold", 20.0)
self._candle_type = self.Param("CandleType", DataType.TimeFrame(TimeSpan.FromHours(1)))
self._bars = []
self._raw_k = []
self._smooth_k = []
self._sar_values = []
self._fast_ema = self._slow_ema = None
self._macd_fast = self._macd_slow = self._macd_signal = None
self._sar = None
self._entry_price = 0.0
self._stop_price = self._take_price = self._best_price = None
self._entry_candle_time = None
def GetWorkingSecurities(self):
return [(self.Security, self._candle_type.Value)]
def OnReseted(self):
super(bruno_strategy, self).OnReseted()
self._bars = []
self._raw_k = []
self._smooth_k = []
self._sar_values = []
self._fast_ema = self._slow_ema = None
self._macd_fast = self._macd_slow = self._macd_signal = None
self._sar = None
self._reset_protection()
def OnStarted2(self, time):
super(bruno_strategy, self).OnStarted2(time)
self._sar = _sar_state()
self.SubscribeCandles(self._candle_type.Value).Bind(self._process_candle).Start()
def _process_candle(self, candle):
if candle.State != CandleStates.Finished:
return
high = float(candle.HighPrice)
low = float(candle.LowPrice)
close = float(candle.ClosePrice)
self._bars.append((high, low, close))
keep = max(int(self._adx_period.Value) + 2,
int(self._macd_slow_period.Value) + int(self._macd_signal_period.Value) + 2,
int(self._stoch_period.Value) + 10)
if len(self._bars) > keep:
del self._bars[:-keep]
self._fast_ema = self._ema(self._fast_ema, close, int(self._fast_ema_period.Value))
self._slow_ema = self._ema(self._slow_ema, close, int(self._slow_ema_period.Value))
self._macd_fast = self._ema(self._macd_fast, close, int(self._macd_fast_period.Value))
self._macd_slow = self._ema(self._macd_slow, close, int(self._macd_slow_period.Value))
macd_main = self._macd_fast - self._macd_slow
self._macd_signal = self._ema(self._macd_signal, macd_main, int(self._macd_signal_period.Value))
sar = self._sar.process(high, low, close)
if sar is not None:
self._sar_values.append(sar)
if len(self._sar_values) > 4:
del self._sar_values[0]
self._update_stochastic()
if self.Position != 0 and self._apply_protection(candle):
return
if (len(self._bars) < int(self._adx_period.Value) + 1 or
len(self._smooth_k) < int(self._stoch_d_smoothing.Value) or
len(self._sar_values) < 3):
return
plus_di, minus_di = self._directional_index()
k = self._smooth_k[-1]
d_count = int(self._stoch_d_smoothing.Value)
d = sum(self._smooth_k[-d_count:]) / d_count
histogram = macd_main - self._macd_signal
long_directional = plus_di > minus_di and plus_di > float(self._adx_positive.Value)
short_directional = plus_di < minus_di and plus_di < float(self._adx_negative.Value)
long_momentum = self._fast_ema > self._slow_ema and k > d and k < float(self._stoch_overbought.Value)
short_momentum = self._fast_ema < self._slow_ema and k < d and k > float(self._stoch_oversold.Value)
long_macd = histogram > 0 and macd_main > self._macd_signal
short_macd = histogram < 0 and macd_main < self._macd_signal
long_sar = self._sar_values[-3] < self._sar_values[-2] < self._sar_values[-1] and self._fast_ema > self._slow_ema
short_sar = self._sar_values[-3] > self._sar_values[-2] > self._sar_values[-1] and self._fast_ema < self._slow_ema
long_volume, short_volume = self.calculate_signal_volumes(
float(self._base_volume.Value), float(self._signal_multiplier.Value),
long_directional, short_directional,
long_momentum, short_momentum,
long_macd, short_macd,
long_sar, short_sar)
base = float(self._base_volume.Value)
has_long = long_volume > base
has_short = short_volume > base
if has_long == has_short:
return
self._enter(Sides.Buy if has_long else Sides.Sell,
long_volume if has_long else short_volume, candle)
def _enter(self, side, target_volume, candle):
position = float(self.Position)
volume = self._normalize_volume(target_volume + abs(position) if
(position < 0 and side == Sides.Buy) or (position > 0 and side == Sides.Sell)
else target_volume)
if volume <= 0:
return
if side == Sides.Buy:
self.BuyMarket(volume)
else:
self.SellMarket(volume)
self._entry_price = float(candle.ClosePrice)
self._entry_candle_time = candle.OpenTime
self._best_price = self._entry_price
pip = self._pip_size()
sl = int(self._stop_loss.Value)
tp = int(self._take_profit.Value)
self._stop_price = (self._entry_price - sl * pip if side == Sides.Buy else self._entry_price + sl * pip) if sl > 0 else None
self._take_price = (self._entry_price + tp * pip if side == Sides.Buy else self._entry_price - tp * pip) if tp > 0 else None
def _apply_protection(self, candle):
if self._entry_candle_time is not None and candle.OpenTime <= self._entry_candle_time:
return False
pip = self._pip_size()
trail = int(self._trailing_stop.Value)
step = int(self._trailing_step.Value)
if self.Position > 0:
high = float(candle.HighPrice)
self._best_price = high if self._best_price is None else max(self._best_price, high)
if trail > 0 and self._best_price - self._entry_price >= (trail + step) * pip:
candidate = self._best_price - trail * pip
if self._stop_price is None or candidate >= self._stop_price + step * pip:
self._stop_price = candidate
if ((self._stop_price is not None and float(candle.LowPrice) <= self._stop_price) or
(self._take_price is not None and high >= self._take_price)):
self.SellMarket(Math.Abs(self.Position))
self._reset_protection()
return True
elif self.Position < 0:
low = float(candle.LowPrice)
self._best_price = low if self._best_price is None else min(self._best_price, low)
if trail > 0 and self._entry_price - self._best_price >= (trail + step) * pip:
candidate = self._best_price + trail * pip
if self._stop_price is None or candidate <= self._stop_price - step * pip:
self._stop_price = candidate
if ((self._stop_price is not None and float(candle.HighPrice) >= self._stop_price) or
(self._take_price is not None and low <= self._take_price)):
self.BuyMarket(Math.Abs(self.Position))
self._reset_protection()
return True
return False
def _update_stochastic(self):
period = int(self._stoch_period.Value)
if len(self._bars) < period:
return
window = self._bars[-period:]
high = max(b[0] for b in window)
low = min(b[1] for b in window)
raw = 50.0 if high == low else (self._bars[-1][2] - low) / (high - low) * 100.0
self._raw_k.append(raw)
k_smooth = int(self._stoch_k_smoothing.Value)
d_smooth = int(self._stoch_d_smoothing.Value)
if len(self._raw_k) > k_smooth + d_smooth + 2:
del self._raw_k[0]
if len(self._raw_k) >= k_smooth:
self._smooth_k.append(sum(self._raw_k[-k_smooth:]) / k_smooth)
if len(self._smooth_k) > d_smooth + 2:
del self._smooth_k[0]
def _directional_index(self):
period = int(self._adx_period.Value)
start = len(self._bars) - period
tr = plus = minus = 0.0
for i in range(start, len(self._bars)):
current = self._bars[i]
previous = self._bars[i - 1]
up = current[0] - previous[0]
down = previous[1] - current[1]
plus += up if up > down and up > 0 else 0.0
minus += down if down > up and down > 0 else 0.0
tr += max(current[0] - current[1],
abs(current[0] - previous[2]),
abs(current[1] - previous[2]))
return (0.0, 0.0) if tr <= 0 else (plus / tr * 100.0, minus / tr * 100.0)
@staticmethod
def calculate_signal_volumes(base_volume, multiplier,
long_directional, short_directional,
long_momentum, short_momentum,
long_macd, short_macd,
long_sar, short_sar):
long_volume = base_volume
short_volume = base_volume
for flag in [long_directional, long_momentum, long_macd, long_sar]:
if flag:
long_volume *= multiplier
for flag in [short_directional, short_momentum, short_macd, short_sar]:
if flag:
short_volume *= multiplier
return long_volume, short_volume
def _normalize_volume(self, volume):
if self.Security is not None:
if self.Security.MaxVolume is not None and float(self.Security.MaxVolume) > 0:
volume = min(volume, float(self.Security.MaxVolume))
if self.Security.MinVolume is not None and float(self.Security.MinVolume) > 0:
volume = max(volume, float(self.Security.MinVolume))
if self.Security.VolumeStep is not None and float(self.Security.VolumeStep) > 0:
step = float(self.Security.VolumeStep)
# Rounding first keeps float error from flooring an exact multiple one step down.
volume = math.floor(round(volume / step, 9)) * step
return volume
def _pip_size(self):
step = float(self.Security.PriceStep) if self.Security is not None and self.Security.PriceStep is not None else 0.0
if step <= 0:
return 0.0001
return step * 10.0 if abs(step - 0.00001) < 1e-12 or abs(step - 0.001) < 1e-12 else step
@staticmethod
def _ema(previous, value, period):
if previous is None:
return value
alpha = 2.0 / (period + 1.0)
return previous + alpha * (value - previous)
def _reset_protection(self):
self._entry_price = 0.0
self._stop_price = None
self._take_price = None
self._best_price = None
self._entry_candle_time = None
def CreateClone(self):
return bruno_strategy()