diff --git a/Algorithm/QCAlgorithm.Indicators.cs b/Algorithm/QCAlgorithm.Indicators.cs index e51f0c0961b0..4a27134a1312 100644 --- a/Algorithm/QCAlgorithm.Indicators.cs +++ b/Algorithm/QCAlgorithm.Indicators.cs @@ -1365,6 +1365,25 @@ public LeastSquaresMovingAverage LSMA(Symbol symbol, int period, Resolution? res return leastSquaresMovingAverage; } + /// + /// Creates and registers a new Least Squares Moving Average instance. + /// + /// The symbol whose LSMA we seek. + /// The reference symbol to regress against. + /// The LSMA period. Normally 14. + /// The resolution. + /// Selects a value from the BaseData to send into the indicator, if null defaults to casting the input value to a TradeBar. + /// A LeastSquaredMovingAverage configured with the specified period and reference symbol + [DocumentationAttribute(Indicators)] + public LeastSquaresMovingAverage LSMA(Symbol symbol, Symbol reference, int period, Resolution? resolution = null, Func selector = null) + { + var name = CreateIndicatorName(symbol, $"LSMA({period})", resolution); + var leastSquaresMovingAverage = new LeastSquaresMovingAverage(name, reference, period); + InitializeIndicator(leastSquaresMovingAverage, resolution, selector, symbol, reference); + + return leastSquaresMovingAverage; + } + /// /// Creates a new LinearWeightedMovingAverage indicator. This indicator will linearly distribute /// the weights across the periods. diff --git a/Indicators/LeastSquaresMovingAverage.cs b/Indicators/LeastSquaresMovingAverage.cs index c946d6bf1fa9..628b403a4df2 100644 --- a/Indicators/LeastSquaresMovingAverage.cs +++ b/Indicators/LeastSquaresMovingAverage.cs @@ -33,6 +33,36 @@ public class LeastSquaresMovingAverage : WindowIndicator, II /// private readonly double[] _t; + /// + /// Reference symbol to use as the regression x-axis, when provided. + /// + private readonly Symbol _referenceSymbol = Symbol.Empty; + + /// + /// Window of matched reference data points. + /// + private readonly RollingWindow _referenceWindow; + + /// + /// Target symbol inferred from the first non-reference input. + /// + private Symbol _targetSymbol = Symbol.Empty; + + /// + /// Pending target input waiting for a matching reference input time. + /// + private IndicatorDataPoint _targetInput; + + /// + /// Pending reference input waiting for a matching target input time. + /// + private IndicatorDataPoint _referenceInput; + + /// + /// Last computed value, returned while waiting for matched target/reference inputs. + /// + private decimal _lastComputedValue; + /// /// The point where the regression line crosses the y-axis (price-axis) /// @@ -43,10 +73,15 @@ public class LeastSquaresMovingAverage : WindowIndicator, II /// public IndicatorBase Slope { get; } + /// + /// Gets a flag indicating when this indicator is ready and fully initialized + /// + public override bool IsReady => base.IsReady && (_referenceWindow == null || _referenceWindow.IsReady); + /// /// Required period, in data points, for the indicator to be ready and fully initialized. /// - public int WarmUpPeriod => Period; + public override int WarmUpPeriod => Period; /// /// Initializes a new instance of the class. @@ -61,6 +96,19 @@ public LeastSquaresMovingAverage(string name, int period) Slope = new Identity(name + "_Slope"); } + /// + /// Initializes a new instance of the class. + /// + /// The name of this indicator + /// The reference symbol to regress against + /// The number of data points to hold in the window + public LeastSquaresMovingAverage(string name, Symbol referenceSymbol, int period) + : this(name, period) + { + _referenceSymbol = referenceSymbol; + _referenceWindow = new RollingWindow(period); + } + /// /// Initializes a new instance of the class. /// @@ -70,6 +118,59 @@ public LeastSquaresMovingAverage(int period) { } + /// + /// Initializes a new instance of the class. + /// + /// The reference symbol to regress against + /// The number of data points to hold in the window + public LeastSquaresMovingAverage(Symbol referenceSymbol, int period) + : this($"LSMA({period},{referenceSymbol})", referenceSymbol, period) + { + } + + /// + /// Computes the next value of this indicator from the given state + /// + /// The input given to the indicator + /// + /// A new value for this indicator + /// + protected override decimal ComputeNextValue(IndicatorDataPoint input) + { + if (_referenceWindow == null) + { + return base.ComputeNextValue(input); + } + + if (input.Symbol == _referenceSymbol) + { + _referenceInput = input; + } + else + { + if (_targetSymbol == Symbol.Empty) + { + _targetSymbol = input.Symbol; + } + else if (input.Symbol != _targetSymbol) + { + throw new ArgumentException($"The input symbol {input.Symbol} is not the target or reference symbol."); + } + + _targetInput = input; + } + + if (_targetInput != null && _referenceInput != null && _targetInput.EndTime == _referenceInput.EndTime) + { + _referenceWindow.Add(_referenceInput); + _lastComputedValue = base.ComputeNextValue(_targetInput); + _targetInput = null; + _referenceInput = null; + } + + return _lastComputedValue; + } + /// /// Computes the next value of this indicator from the given state /// @@ -88,13 +189,30 @@ protected override decimal ComputeNextValue(IReadOnlyWindow .OrderBy(i => i.EndTime) .Select(i => Convert.ToDouble(i.Value)) .ToArray(); - // Fit OLS - var ols = Fit.Line(x: _t, y: series); - Intercept.Update(input.EndTime, (decimal)ols.Item1); - Slope.Update(input.EndTime, (decimal)ols.Item2); + + decimal x = Period; + double intercept; + double slope; + + if (_referenceWindow != null && _referenceWindow.IsReady) + { + var xValues = _referenceWindow + .OrderBy(i => i.EndTime) + .Select(i => Convert.ToDouble(i.Value)) + .ToArray(); + x = _referenceWindow[0].Value; + (intercept, slope) = Fit.Line(x: xValues, y: series); + } + else + { + (intercept, slope) = Fit.Line(x: _t, y: series); + } + + Intercept.Update(input.EndTime, intercept.SafeDecimalCast()); + Slope.Update(input.EndTime, slope.SafeDecimalCast()); // Calculate the fitted value corresponding to the input - return Intercept.Current.Value + Slope.Current.Value * Period; + return Intercept.Current.Value + Slope.Current.Value * x; } /// @@ -104,7 +222,12 @@ public override void Reset() { Intercept.Reset(); Slope.Reset(); + _referenceWindow?.Reset(); + _targetInput = null; + _referenceInput = null; + _targetSymbol = Symbol.Empty; + _lastComputedValue = 0m; base.Reset(); } } -} \ No newline at end of file +} diff --git a/Tests/Indicators/LeastSquaresMovingAverageTests.cs b/Tests/Indicators/LeastSquaresMovingAverageTests.cs index da9b5be5c8a0..7183412fdb86 100644 --- a/Tests/Indicators/LeastSquaresMovingAverageTests.cs +++ b/Tests/Indicators/LeastSquaresMovingAverageTests.cs @@ -71,6 +71,92 @@ protected override void RunTestIndicator(IndicatorBase indic } } + [Test] + public void WithReferenceRegressesAgainstBenchmarkWhenTargetArrivesFirst() + { + var indicator = new LeastSquaresMovingAverage("LSMA", Symbols.SPY, 3); + + UpdatePair(indicator, 1, 3, targetFirst: true); + Assert.IsFalse(indicator.IsReady); + + UpdatePair(indicator, 2, 5, targetFirst: true); + Assert.IsFalse(indicator.IsReady); + + UpdatePair(indicator, 3, 7, targetFirst: true); + + Assert.IsTrue(indicator.IsReady); + Assert.AreEqual(1m, Math.Round(indicator.Intercept.Current.Value, 8)); + Assert.AreEqual(2m, Math.Round(indicator.Slope.Current.Value, 8)); + Assert.AreEqual(7m, Math.Round(indicator.Current.Value, 8)); + } + + [Test] + public void WithReferenceRegressesAgainstBenchmarkWhenReferenceArrivesFirst() + { + var indicator = new LeastSquaresMovingAverage("LSMA", Symbols.SPY, 3); + + UpdatePair(indicator, 5, 11, targetFirst: false); + Assert.IsFalse(indicator.IsReady); + + UpdatePair(indicator, 6, 13, targetFirst: false); + Assert.IsFalse(indicator.IsReady); + + UpdatePair(indicator, 7, 15, targetFirst: false); + + Assert.IsTrue(indicator.IsReady); + Assert.AreEqual(1m, Math.Round(indicator.Intercept.Current.Value, 8)); + Assert.AreEqual(2m, Math.Round(indicator.Slope.Current.Value, 8)); + Assert.AreEqual(15m, Math.Round(indicator.Current.Value, 8)); + } + + [Test] + public void WithReferenceWaitsForMatchingTimes() + { + var indicator = new LeastSquaresMovingAverage("LSMA", Symbols.SPY, 2); + var time = DateTime.UtcNow; + + indicator.Update(new IndicatorDataPoint(Symbols.AAPL, time, 3)); + indicator.Update(new IndicatorDataPoint(Symbols.SPY, time.AddMinutes(1), 2)); + + Assert.IsFalse(indicator.IsReady); + Assert.AreEqual(0m, indicator.Current.Value); + + indicator.Update(new IndicatorDataPoint(Symbols.AAPL, time.AddMinutes(1), 5)); + + Assert.IsFalse(indicator.IsReady); + Assert.AreEqual(5m, indicator.Current.Value); + + UpdatePair(indicator, 3, 7, time.AddMinutes(2), targetFirst: true); + + Assert.IsTrue(indicator.IsReady); + Assert.AreEqual(1m, Math.Round(indicator.Intercept.Current.Value, 8)); + Assert.AreEqual(2m, Math.Round(indicator.Slope.Current.Value, 8)); + Assert.AreEqual(7m, Math.Round(indicator.Current.Value, 8)); + } + + [Test] + public void WithReferenceResetsProperly() + { + var indicator = new LeastSquaresMovingAverage("LSMA", Symbols.SPY, 3); + + UpdatePair(indicator, 1, 3, targetFirst: true); + UpdatePair(indicator, 2, 5, targetFirst: true); + UpdatePair(indicator, 3, 7, targetFirst: true); + + Assert.IsTrue(indicator.IsReady); + + indicator.Reset(); + + TestHelper.AssertIndicatorIsInDefaultState(indicator); + + UpdatePair(indicator, 4, 9, targetFirst: false); + UpdatePair(indicator, 5, 11, targetFirst: false); + UpdatePair(indicator, 6, 13, targetFirst: false); + + Assert.IsTrue(indicator.IsReady); + Assert.AreEqual(13m, Math.Round(indicator.Current.Value, 8)); + } + [Test] public override void ResetsProperly() { @@ -108,5 +194,27 @@ public override void WarmsUpProperly() indicator.Update(time.AddMinutes(period.Value - 1), Prices[period.Value - 1]); Assert.IsTrue(indicator.IsReady); } + + private static void UpdatePair(LeastSquaresMovingAverage indicator, decimal referenceValue, decimal targetValue, bool targetFirst) + { + UpdatePair(indicator, referenceValue, targetValue, DateTime.UtcNow.AddMinutes((double)referenceValue), targetFirst); + } + + private static void UpdatePair(LeastSquaresMovingAverage indicator, decimal referenceValue, decimal targetValue, DateTime time, bool targetFirst) + { + var target = new IndicatorDataPoint(Symbols.AAPL, time, targetValue); + var reference = new IndicatorDataPoint(Symbols.SPY, time, referenceValue); + + if (targetFirst) + { + indicator.Update(target); + indicator.Update(reference); + } + else + { + indicator.Update(reference); + indicator.Update(target); + } + } } -} \ No newline at end of file +}