Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Algorithm/QCAlgorithm.Indicators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1365,6 +1365,26 @@ public LeastSquaresMovingAverage LSMA(Symbol symbol, int period, Resolution? res
return leastSquaresMovingAverage;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="symbol">The symbol whose LSMA we seek.</param>
/// <param name="reference">The reference symbol to regress against.</param>
/// <param name="period">The LSMA period. Normally 14.</param>
/// <param name="resolution">The resolution.</param>
/// <param name="selector">Selects a value from the BaseData to send into the indicator, if null defaults to casting the input value to a TradeBar.</param>
/// <returns>A LeastSquaredMovingAverage configured with the specified period and reference</returns>
[DocumentationAttribute(Indicators)]
public LeastSquaresMovingAverage LSMA(Symbol symbol, Symbol reference, int period, Resolution? resolution = null, Func<IBaseData, decimal> 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;
}

/// <summary>
/// Creates a new LinearWeightedMovingAverage indicator. This indicator will linearly distribute
/// the weights across the periods.
Expand Down
86 changes: 80 additions & 6 deletions Indicators/LeastSquaresMovingAverage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
/// </summary>
public class LeastSquaresMovingAverage : WindowIndicator<IndicatorDataPoint>, IIndicatorWarmUpPeriodProvider
Expand All @@ -33,6 +35,16 @@ public class LeastSquaresMovingAverage : WindowIndicator<IndicatorDataPoint>, II
/// </summary>
private readonly double[] _t;

/// <summary>
/// The reference symbol to regress against.
/// </summary>
private readonly Symbol _referenceSymbol = Symbol.None;

/// <summary>
/// Rolling window of reference symbol data points.
/// </summary>
private readonly RollingWindow<IndicatorDataPoint> _referenceWindow = new(0);

/// <summary>
/// The point where the regression line crosses the y-axis (price-axis)
/// </summary>
Expand All @@ -48,6 +60,11 @@ public class LeastSquaresMovingAverage : WindowIndicator<IndicatorDataPoint>, II
/// </summary>
public int WarmUpPeriod => Period;

/// <summary>
/// Gets a flag indicating when this indicator is ready and fully initialized
/// </summary>
public override bool IsReady => base.IsReady && _referenceWindow.IsReady;

/// <summary>
/// Initializes a new instance of the <see cref="LeastSquaresMovingAverage"/> class.
/// </summary>
Expand All @@ -70,6 +87,47 @@ public LeastSquaresMovingAverage(int period)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="LeastSquaresMovingAverage"/> class
/// with a reference symbol for regression.
/// </summary>
/// <param name="name">The name of this indicator</param>
/// <param name="referenceSymbol">The reference symbol to regress against</param>
/// <param name="period">The number of data points to hold in the window</param>
public LeastSquaresMovingAverage(string name, Symbol referenceSymbol, int period)
: this(name, period)
{
_referenceSymbol = referenceSymbol;
_referenceWindow = new RollingWindow<IndicatorDataPoint>(period);
}

/// <summary>
/// Initializes a new instance of the <see cref="LeastSquaresMovingAverage"/> class
/// with a reference symbol for regression.
/// </summary>
/// <param name="referenceSymbol">The reference symbol to regress against</param>
/// <param name="period">The number of data points to hold in the window</param>
public LeastSquaresMovingAverage(Symbol referenceSymbol, int period)
: this($"LSMA({period},{referenceSymbol})", referenceSymbol, period)
{
}

/// <summary>
/// Computes the next value of this indicator from the given state
/// </summary>
/// <param name="input">The input given to the indicator</param>
/// <returns>A new value for this indicator</returns>
protected override decimal ComputeNextValue(IndicatorDataPoint input)
{
if (input.Symbol == _referenceSymbol)
{
_referenceWindow.Add(input);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think this will work correctly, if main symbol comes first and later reference ( should rely on data coming in order) it wont be used for the same time, this requires some time awareness, can see similar multi symbol indicators and how they work.
Also generally I think ideally we should compare the indicator values in a test against some external source? like we usually do, at least trying to find one
Minor also but shouldn't create an unrequired rolling window _referenceWindow = new(0);

return Current.Value;
}

return base.ComputeNextValue(input);
}

/// <summary>
/// Computes the next value of this indicator from the given state
/// </summary>
Expand All @@ -88,13 +146,28 @@ protected override decimal ComputeNextValue(IReadOnlyWindow<IndicatorDataPoint>
.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;
}

/// <summary>
Expand All @@ -104,7 +177,8 @@ public override void Reset()
{
Intercept.Reset();
Slope.Reset();
_referenceWindow.Reset();
base.Reset();
}
}
}
}
100 changes: 98 additions & 2 deletions Tests/Indicators/LeastSquaresMovingAverageTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/*
/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
Expand Down Expand Up @@ -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));
}
}
}
}
}