diff --git a/Algorithm/QCAlgorithm.Indicators.cs b/Algorithm/QCAlgorithm.Indicators.cs
index e51f0c0961b0..8caf49538df9 100644
--- a/Algorithm/QCAlgorithm.Indicators.cs
+++ b/Algorithm/QCAlgorithm.Indicators.cs
@@ -1365,6 +1365,26 @@ public LeastSquaresMovingAverage LSMA(Symbol symbol, int period, Resolution? res
return leastSquaresMovingAverage;
}
+ ///
+ /// Creates and registers a new Least Squares Moving Average instance with a reference symbol.
+ /// The regression is performed against the reference symbol values instead of time.
+ ///
+ /// 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
+ [DocumentationAttribute(Indicators)]
+ public LeastSquaresMovingAverage LSMA(Symbol symbol, Symbol reference, int period, Resolution? resolution = null, Func selector = null)
+ {
+ var name = CreateIndicatorName(symbol, $"LSMA({period},{reference})", 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..308423e19477 100644
--- a/Indicators/LeastSquaresMovingAverage.cs
+++ b/Indicators/LeastSquaresMovingAverage.cs
@@ -24,6 +24,8 @@ namespace QuantConnect.Indicators
/// The Least Squares Moving Average (LSMA) first calculates a least squares regression line
/// over the preceding time periods, and then projects it forward to the current period. In
/// essence, it calculates what the value would be if the regression line continued.
+ /// When a reference symbol is provided, the regression is performed against the reference
+ /// values instead of time.
/// Source: https://rtmath.net/assets/docs/finanalysis/html/b3fab79c-f4b2-40fb-8709-fdba43cdb363.htm
///
public class LeastSquaresMovingAverage : WindowIndicator, IIndicatorWarmUpPeriodProvider
@@ -33,6 +35,16 @@ public class LeastSquaresMovingAverage : WindowIndicator, II
///
private readonly double[] _t;
+ ///
+ /// The reference symbol to regress against.
+ ///
+ private readonly Symbol _referenceSymbol = Symbol.None;
+
+ ///
+ /// Rolling window of reference symbol data points.
+ ///
+ private readonly RollingWindow _referenceWindow = new(0);
+
///
/// The point where the regression line crosses the y-axis (price-axis)
///
@@ -48,6 +60,11 @@ public class LeastSquaresMovingAverage : WindowIndicator, II
///
public int WarmUpPeriod => Period;
+ ///
+ /// Gets a flag indicating when this indicator is ready and fully initialized
+ ///
+ public override bool IsReady => base.IsReady && _referenceWindow.IsReady;
+
///
/// Initializes a new instance of the class.
///
@@ -70,6 +87,47 @@ public LeastSquaresMovingAverage(int period)
{
}
+ ///
+ /// Initializes a new instance of the class
+ /// with a reference symbol for regression.
+ ///
+ /// 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
+ /// with a reference symbol for regression.
+ ///
+ /// 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 (input.Symbol == _referenceSymbol)
+ {
+ _referenceWindow.Add(input);
+ return Current.Value;
+ }
+
+ return base.ComputeNextValue(input);
+ }
+
///
/// Computes the next value of this indicator from the given state
///
@@ -88,13 +146,28 @@ 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);
+
+ var x = (decimal)Period;
+ double intercept, slope;
+ if (_referenceWindow.Size != 0 && _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 +177,8 @@ public override void Reset()
{
Intercept.Reset();
Slope.Reset();
+ _referenceWindow.Reset();
base.Reset();
}
}
-}
\ No newline at end of file
+}
diff --git a/Tests/Indicators/LeastSquaresMovingAverageTests.cs b/Tests/Indicators/LeastSquaresMovingAverageTests.cs
index da9b5be5c8a0..1e6de690a186 100644
--- a/Tests/Indicators/LeastSquaresMovingAverageTests.cs
+++ b/Tests/Indicators/LeastSquaresMovingAverageTests.cs
@@ -1,4 +1,4 @@
-/*
+/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
@@ -108,5 +108,101 @@ public override void WarmsUpProperly()
indicator.Update(time.AddMinutes(period.Value - 1), Prices[period.Value - 1]);
Assert.IsTrue(indicator.IsReady);
}
+
+ [Test]
+ public void WithReferenceIsNotReadyUntilBothWindowsFull()
+ {
+ var reference = Symbols.SPY;
+ var lsma = new LeastSquaresMovingAverage("LSMA", reference, 5);
+ var time = DateTime.Now;
+
+ for (var i = 0; i < 5; i++)
+ {
+ lsma.Update(new IndicatorDataPoint(Symbols.AAPL, time.AddMinutes(i), 100m + i));
+ }
+
+ Assert.IsFalse(lsma.IsReady, "Should not be ready without reference data");
+
+ for (var i = 0; i < 4; i++)
+ {
+ lsma.Update(new IndicatorDataPoint(reference, time.AddMinutes(i), 200m + i));
+ }
+
+ Assert.IsFalse(lsma.IsReady, "Should not be ready with insufficient reference data");
+
+ lsma.Update(new IndicatorDataPoint(reference, time.AddMinutes(4), 204m));
+ Assert.IsTrue(lsma.IsReady, "Should be ready when both windows are full");
+ }
+
+ [Test]
+ public void WithReferenceRegressesAgainstBenchmark()
+ {
+ var target = Symbols.AAPL;
+ var reference = Symbols.SPY;
+ var lsma = new LeastSquaresMovingAverage("LSMA", reference, 5);
+ var time = DateTime.Now;
+
+ // y = 2*x + 1 (target = 2*reference + 1)
+ // reference: 1, 2, 3, 4, 5
+ // target: 3, 5, 7, 9, 11
+ for (var i = 0; i < 5; i++)
+ {
+ var refValue = (decimal)(i + 1);
+ var targetValue = 2m * refValue + 1m;
+ lsma.Update(new IndicatorDataPoint(target, time.AddMinutes(i), targetValue));
+ lsma.Update(new IndicatorDataPoint(reference, time.AddMinutes(i), refValue));
+ }
+
+ Assert.IsTrue(lsma.IsReady);
+
+ // slope should be 2, intercept should be 1
+ Assert.AreEqual(2.0, (double)lsma.Slope.Current.Value, 0.0001);
+ Assert.AreEqual(1.0, (double)lsma.Intercept.Current.Value, 0.0001);
+
+ // projected value = intercept + slope * latest_reference = 1 + 2*5 = 11
+ Assert.AreEqual(11.0, (double)lsma.Current.Value, 0.0001);
+ }
+
+ [Test]
+ public void WithReferenceResetsProperly()
+ {
+ var target = Symbols.AAPL;
+ var reference = Symbols.SPY;
+ var lsma = new LeastSquaresMovingAverage("LSMA", reference, 3);
+ var time = DateTime.Now;
+
+ for (var i = 0; i < 3; i++)
+ {
+ lsma.Update(new IndicatorDataPoint(target, time.AddMinutes(i), 10m + i));
+ lsma.Update(new IndicatorDataPoint(reference, time.AddMinutes(i), 20m + i));
+ }
+
+ Assert.IsTrue(lsma.IsReady);
+
+ lsma.Reset();
+
+ Assert.IsFalse(lsma.IsReady);
+ Assert.AreEqual(0m, lsma.Current.Value);
+ Assert.AreEqual(0m, lsma.Intercept.Current.Value);
+ Assert.AreEqual(0m, lsma.Slope.Current.Value);
+ }
+
+ [Test]
+ public void WithoutReferenceBehavesIdentically()
+ {
+ var withRef = new LeastSquaresMovingAverage(20);
+ var without = new LeastSquaresMovingAverage(20);
+ var time = DateTime.Now;
+
+ for (var i = 0; i < Prices.Length; i++)
+ {
+ withRef.Update(time.AddMinutes(i), Prices[i]);
+ without.Update(time.AddMinutes(i), Prices[i]);
+
+ Assert.AreEqual(
+ Math.Round(without.Current.Value, 4),
+ Math.Round(withRef.Current.Value, 4));
+ }
+ }
}
-}
\ No newline at end of file
+}