Bruno 策略
Bruno 策略来源于 MetaTrader 5 平台,是一个典型的趋势追踪系统。本移植版本沿用了全部过滤条件:带有方向性指标线的 ADX、两条指数移动平均线(EMA 8 与 EMA 21)、MACD(13、34、8)、随机指标(21、3、3)以及参数为 0.055/0.21 的抛物线 SAR 斜率。每当某个过滤器支持当前方向时,就会把基础手数乘以预设倍数。如果同一根 K 线上多空信号同时被放大,则放弃交易以避免冲突。
交易逻辑
- 趋势方向
- 当
+DI > -DI且+DI > 20时加强做多信号。 - 当
+DI < -DI且+DI < 40时加强做空信号。
- 当
- 动量确认
- 做多需要 EMA(8) 高于 EMA(21),随机指标 %K 高于 %D 且 %K 低于超买线(默认 80)。
- 做空需要 EMA(8) 低于 EMA(21),随机指标 %K 低于 %D 且 %K 高于超卖线(默认 20)。
- MACD 滤波
- 多头:MACD 主线在 0 轴之上并且高于信号线。
- 空头:MACD 主线在 0 轴之下并且低于信号线。
- Parabolic SAR 斜率
- 当前两个 SAR 值上升且 EMA(8) > EMA(21) 时进一步确认多头。
- 当前两个 SAR 值下降且 EMA(8) < EMA(21) 时进一步确认空头。
每满足一个条件,BaseVolume 会乘以 SignalMultiplier(默认 1.6)。任意时刻只允许一个方向成立;最终信号出现后,策略会先平掉反向仓位,再按新的手数开仓,并把当前收盘价记录为入场价。
仓位管理
- 止损/止盈:以“调整后的点值”表示的固定距离,与原始 EA 保持一致。当价格在 K 线内触及任一水平时立即平仓。
- 移动止损:当浮动盈利超过
TrailingStop + TrailingStep点后启用,把止损线拉到距离价格TrailingStop点的位置,只有在盈利继续增加至少TrailingStep点时才会再次上移。 - 信号冲突:若多空过滤条件同时满足,则本根 K 线不入场。
参数说明
| 分类 | 参数 | 说明 |
|---|---|---|
| 交易 | BaseVolume |
乘法前的基础手数。 |
| 交易 | SignalMultiplier |
每个确认过滤器对手数的乘数。 |
| 风险控制 | StopLossPips / TakeProfitPips |
止损与止盈距离(调整点)。设为 0 代表禁用。 |
| 风险控制 | TrailingStopPips / TrailingStepPips |
移动止损的距离与最小步长。 |
| 指标 | AdxPeriod, AdxPositiveThreshold, AdxNegativeThreshold |
ADX 周期与方向性阈值。 |
| 指标 | FastEmaPeriod, SlowEmaPeriod |
趋势确认所用 EMA 的周期。 |
| 指标 | MacdFastPeriod, MacdSlowPeriod, MacdSignalPeriod |
MACD 参数。 |
| 指标 | StochasticPeriod, StochasticKsmoothing, StochasticDsmoothing, StochasticOverbought, StochasticOversold |
随机指标设置。 |
| 通用 | CandleType |
全部计算使用的时间框架(默认 1 小时)。 |
其他说明
- 点值换算遵循 MetaTrader 规则:报价保留 3 或 5 位小数的品种将点值乘以 10。
- 抛物线 SAR 的加速步长为
0.055,最大加速为0.21,与原始 EA 一致。 - 手数放大逻辑被保留,但在 StockSharp 中以单一净头寸的方式进行管理。
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()