diff --git a/Indicators/SwissArmyKnife.cs b/Indicators/SwissArmyKnife.cs index d76c45768bc9..edd5543e946f 100644 --- a/Indicators/SwissArmyKnife.cs +++ b/Indicators/SwissArmyKnife.cs @@ -1,4 +1,4 @@ -/* +/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * @@ -49,16 +49,33 @@ public enum SwissArmyKnifeTool /// public class SwissArmyKnife : Indicator, IIndicatorWarmUpPeriodProvider { - private readonly RollingWindow _price; - private readonly RollingWindow _filt; private readonly int _period; - private readonly double _a0 = 1; - private readonly double _a1 = 0; - private readonly double _a2 = 0; - private readonly double _b0 = 1; - private readonly double _b1 = 0; - private readonly double _b2 = 0; - private readonly double _c0 = 1; + private readonly SwissArmyKnifeTool _tool; + + /// + /// Gets the Gaussian Filter sub-indicator + /// + public IndicatorBase Gauss { get; } + + /// + /// Gets the Butterworth Filter sub-indicator + /// + public IndicatorBase Butter { get; } + + /// + /// Gets the High Pass Filter sub-indicator + /// + public IndicatorBase HighPass { get; } + + /// + /// Gets the Two Pole High Pass Filter sub-indicator + /// + public IndicatorBase TwoPoleHighPass { get; } + + /// + /// Gets the BandPass Filter sub-indicator + /// + public IndicatorBase BandPass { get; } /// /// Swiss Army Knife indicator by John Ehlers @@ -81,52 +98,14 @@ public SwissArmyKnife(int period, double delta, SwissArmyKnifeTool tool) public SwissArmyKnife(string name, int period, double delta, SwissArmyKnifeTool tool) : base(name) { - _filt = new RollingWindow(2) {0, 0}; - _price = new RollingWindow(3); _period = period; - var beta = 2.415 * (1 - Math.Cos(2 * Math.PI / period)); - var alpha = -beta + Math.Sqrt(Math.Pow(beta, 2) + 2d * beta); + _tool = tool; - switch (tool) - { - case SwissArmyKnifeTool.Gauss: - _c0 = alpha * alpha; - _a1 = 2d * (1 - alpha); - _a2 = -(1 - alpha) * (1 - alpha); - break; - case SwissArmyKnifeTool.Butter: - _c0 = alpha * alpha / 4d; - _b1 = 2; - _b2 = 1; - _a1 = 2d * (1 - alpha); - _a2 = -(1 - alpha) * (1 - alpha); - break; - case SwissArmyKnifeTool.HighPass: - alpha = (Math.Cos(2 * Math.PI / period) + Math.Sin(2 * Math.PI / period) - 1) / Math.Cos(2 * Math.PI / period); - _c0 = (1 + alpha) / 2; - _b1 = -1; - _a1 = 1 - alpha; - break; - case SwissArmyKnifeTool.TwoPoleHighPass: - _c0 = (1 + alpha) * (1 + alpha) / 4; - _b1 = -2; - _b2 = 1; - _a1 = 2d * (1 - alpha); - _a2 = -(1 - alpha) * (1 - alpha); - break; - case SwissArmyKnifeTool.BandPass: - beta = Math.Cos(2 * Math.PI / period); - var gamma = (1 / Math.Cos(4 * Math.PI * delta / period)); - alpha = gamma - Math.Sqrt(Math.Pow(gamma, 2) - 1); - _c0 = (1 - alpha) / 2d; - _b0 = 1; - _b2 = -1; - _a1 = -beta * (1 - alpha); - _a2 = alpha; - break; - default: - throw new ArgumentOutOfRangeException(nameof(tool), tool, "Invalid SwissArmyKnifeTool"); - } + Gauss = new SingleToolFilter($"{name}_Gauss", period, delta, SwissArmyKnifeTool.Gauss); + Butter = new SingleToolFilter($"{name}_Butter", period, delta, SwissArmyKnifeTool.Butter); + HighPass = new SingleToolFilter($"{name}_HP", period, delta, SwissArmyKnifeTool.HighPass); + TwoPoleHighPass = new SingleToolFilter($"{name}_2PHP", period, delta, SwissArmyKnifeTool.TwoPoleHighPass); + BandPass = new SingleToolFilter($"{name}_BP", period, delta, SwissArmyKnifeTool.BandPass); } /// @@ -146,19 +125,21 @@ public SwissArmyKnife(string name, int period, double delta, SwissArmyKnifeTool /// A new value for this indicator protected override decimal ComputeNextValue(IndicatorDataPoint input) { - _price.Add((double)input.Price); + Gauss.Update(input); + Butter.Update(input); + HighPass.Update(input); + TwoPoleHighPass.Update(input); + BandPass.Update(input); - if (_price.Samples == 1) + switch (_tool) { - _price.Add(_price[0]); - _price.Add(_price[0]); + case SwissArmyKnifeTool.Gauss: return Gauss.Current.Value; + case SwissArmyKnifeTool.Butter: return Butter.Current.Value; + case SwissArmyKnifeTool.HighPass: return HighPass.Current.Value; + case SwissArmyKnifeTool.TwoPoleHighPass: return TwoPoleHighPass.Current.Value; + case SwissArmyKnifeTool.BandPass: return BandPass.Current.Value; + default: throw new ArgumentOutOfRangeException(nameof(_tool), _tool, "Invalid SwissArmyKnifeTool"); } - - var signal = _a0 * _c0 * (_b0 * _price[0] + _b1 * _price[1] + _b2 * _price[2]) + _a0 * (_a1 * _filt[0] + _a2 * _filt[1]); - - _filt.Add(signal); - - return (decimal)signal; } /// @@ -166,11 +147,108 @@ protected override decimal ComputeNextValue(IndicatorDataPoint input) /// public override void Reset() { - _price.Reset(); - _filt.Reset(); - _filt.Add(0); - _filt.Add(0); + Gauss.Reset(); + Butter.Reset(); + HighPass.Reset(); + TwoPoleHighPass.Reset(); + BandPass.Reset(); base.Reset(); } + + /// + /// Single-tool digital filter used internally by SwissArmyKnife + /// + private class SingleToolFilter : Indicator + { + private readonly RollingWindow _price; + private readonly RollingWindow _filt; + private readonly int _period; + private readonly double _a0 = 1; + private readonly double _a1 = 0; + private readonly double _a2 = 0; + private readonly double _b0 = 1; + private readonly double _b1 = 0; + private readonly double _b2 = 0; + private readonly double _c0 = 1; + + public SingleToolFilter(string name, int period, double delta, SwissArmyKnifeTool tool) + : base(name) + { + _filt = new RollingWindow(2) {0, 0}; + _price = new RollingWindow(3); + _period = period; + var beta = 2.415 * (1 - Math.Cos(2 * Math.PI / period)); + var alpha = -beta + Math.Sqrt(Math.Pow(beta, 2) + 2d * beta); + + switch (tool) + { + case SwissArmyKnifeTool.Gauss: + _c0 = alpha * alpha; + _a1 = 2d * (1 - alpha); + _a2 = -(1 - alpha) * (1 - alpha); + break; + case SwissArmyKnifeTool.Butter: + _c0 = alpha * alpha / 4d; + _b1 = 2; + _b2 = 1; + _a1 = 2d * (1 - alpha); + _a2 = -(1 - alpha) * (1 - alpha); + break; + case SwissArmyKnifeTool.HighPass: + alpha = (Math.Cos(2 * Math.PI / period) + Math.Sin(2 * Math.PI / period) - 1) / Math.Cos(2 * Math.PI / period); + _c0 = (1 + alpha) / 2; + _b1 = -1; + _a1 = 1 - alpha; + break; + case SwissArmyKnifeTool.TwoPoleHighPass: + _c0 = (1 + alpha) * (1 + alpha) / 4; + _b1 = -2; + _b2 = 1; + _a1 = 2d * (1 - alpha); + _a2 = -(1 - alpha) * (1 - alpha); + break; + case SwissArmyKnifeTool.BandPass: + beta = Math.Cos(2 * Math.PI / period); + var gamma = (1 / Math.Cos(4 * Math.PI * delta / period)); + alpha = gamma - Math.Sqrt(Math.Pow(gamma, 2) - 1); + _c0 = (1 - alpha) / 2d; + _b0 = 1; + _b2 = -1; + _a1 = -beta * (1 - alpha); + _a2 = alpha; + break; + default: + throw new ArgumentOutOfRangeException(nameof(tool), tool, "Invalid SwissArmyKnifeTool"); + } + } + + public override bool IsReady => Samples >= _period; + + protected override decimal ComputeNextValue(IndicatorDataPoint input) + { + _price.Add((double)input.Price); + + if (_price.Samples == 1) + { + _price.Add(_price[0]); + _price.Add(_price[0]); + } + + var signal = _a0 * _c0 * (_b0 * _price[0] + _b1 * _price[1] + _b2 * _price[2]) + _a0 * (_a1 * _filt[0] + _a2 * _filt[1]); + + _filt.Add(signal); + + return (decimal)signal; + } + + public override void Reset() + { + _price.Reset(); + _filt.Reset(); + _filt.Add(0); + _filt.Add(0); + base.Reset(); + } + } } -} \ No newline at end of file +} diff --git a/Tests/Indicators/SwissArmyKnifeTests.cs b/Tests/Indicators/SwissArmyKnifeTests.cs index b932b3a755fe..706a378a6323 100644 --- a/Tests/Indicators/SwissArmyKnifeTests.cs +++ b/Tests/Indicators/SwissArmyKnifeTests.cs @@ -1,4 +1,4 @@ -/* +/* * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. * @@ -32,7 +32,7 @@ protected override IndicatorBase CreateIndicator() protected override string TestColumnName => "Gauss"; protected override Action, double> Assertion => - (indicator, expected) => + (indicator, expected) => Assert.AreEqual(expected, (double) indicator.Current.Value, 0.01); [Test] @@ -81,6 +81,92 @@ public void ComparesButterAgainstExternalData() RunTestIndicator(indicator, "Butter", 0.01m); } + [Test] + public void SubIndicatorsComputeAllToolsSimultaneously() + { + var sak = new SwissArmyKnife(20, 0.1, SwissArmyKnifeTool.Gauss); + + var gaussStandalone = new SwissArmyKnife(20, 0.1, SwissArmyKnifeTool.Gauss); + var butterStandalone = new SwissArmyKnife(20, 0.1, SwissArmyKnifeTool.Butter); + var hpStandalone = new SwissArmyKnife(20, 0.1, SwissArmyKnifeTool.HighPass); + var tphpStandalone = new SwissArmyKnife(20, 0.1, SwissArmyKnifeTool.TwoPoleHighPass); + var bpStandalone = new SwissArmyKnife(20, 0.1, SwissArmyKnifeTool.BandPass); + + foreach (var data in TestHelper.GetDataStream(25)) + { + sak.Update(data); + gaussStandalone.Update(data); + butterStandalone.Update(data); + hpStandalone.Update(data); + tphpStandalone.Update(data); + bpStandalone.Update(data); + + Assert.AreEqual(gaussStandalone.Current.Value, sak.Gauss.Current.Value); + Assert.AreEqual(butterStandalone.Current.Value, sak.Butter.Current.Value); + Assert.AreEqual(hpStandalone.Current.Value, sak.HighPass.Current.Value); + Assert.AreEqual(tphpStandalone.Current.Value, sak.TwoPoleHighPass.Current.Value); + Assert.AreEqual(bpStandalone.Current.Value, sak.BandPass.Current.Value); + } + } + + [Test] + public void SubIndicatorsResetProperly() + { + var sak = new SwissArmyKnife(4, 0.1, SwissArmyKnifeTool.Gauss); + + foreach (var data in TestHelper.GetDataStream(5)) + { + sak.Update(data); + } + + Assert.IsTrue(sak.Gauss.IsReady); + Assert.IsTrue(sak.Butter.IsReady); + Assert.IsTrue(sak.HighPass.IsReady); + Assert.IsTrue(sak.TwoPoleHighPass.IsReady); + Assert.IsTrue(sak.BandPass.IsReady); + + sak.Reset(); + + Assert.IsFalse(sak.Gauss.IsReady); + Assert.IsFalse(sak.Butter.IsReady); + Assert.IsFalse(sak.HighPass.IsReady); + Assert.IsFalse(sak.TwoPoleHighPass.IsReady); + Assert.IsFalse(sak.BandPass.IsReady); + } + + [Test] + public void PrimaryToolMatchesCurrentValue() + { + foreach (SwissArmyKnifeTool tool in Enum.GetValues(typeof(SwissArmyKnifeTool))) + { + var sak = new SwissArmyKnife(20, 0.1, tool); + + foreach (var data in TestHelper.GetDataStream(25)) + { + sak.Update(data); + + switch (tool) + { + case SwissArmyKnifeTool.Gauss: + Assert.AreEqual(sak.Gauss.Current.Value, sak.Current.Value); + break; + case SwissArmyKnifeTool.Butter: + Assert.AreEqual(sak.Butter.Current.Value, sak.Current.Value); + break; + case SwissArmyKnifeTool.HighPass: + Assert.AreEqual(sak.HighPass.Current.Value, sak.Current.Value); + break; + case SwissArmyKnifeTool.TwoPoleHighPass: + Assert.AreEqual(sak.TwoPoleHighPass.Current.Value, sak.Current.Value); + break; + case SwissArmyKnifeTool.BandPass: + Assert.AreEqual(sak.BandPass.Current.Value, sak.Current.Value); + break; + } + } + } + } + private void RunTestIndicator(IndicatorBase indicator, string field, decimal variance) { TestHelper.TestIndicator(indicator, TestFileName, field, (actual, expected) => { AssertResult(expected, actual.Current.Value, variance); }); @@ -92,4 +178,4 @@ private static void AssertResult(double expected, decimal actual, decimal varian Assert.IsTrue(Math.Abs((decimal)expected - actual) < variance); } } -} \ No newline at end of file +}