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
+}