From 1fb05ff1f0c10edeaef4c3b4f823447012a55c93 Mon Sep 17 00:00:00 2001 From: 0xpinara <191243209+0xpinara@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:10:47 -0700 Subject: [PATCH] Add a Least Squares Moving Average with a benchmark reference --- Algorithm/QCAlgorithm.Indicators.cs | 21 + .../LeastSquaresMovingAverageWithReference.cs | 122 ++++++ Tests/Algorithm/AlgorithmIndicatorsTests.cs | 56 +++ ...tSquaresMovingAverageWithReferenceTests.cs | 394 ++++++++++++++++++ Tests/TestData/bi_datatest.csv | 102 ++--- 5 files changed, 644 insertions(+), 51 deletions(-) create mode 100644 Indicators/LeastSquaresMovingAverageWithReference.cs create mode 100644 Tests/Indicators/LeastSquaresMovingAverageWithReferenceTests.cs diff --git a/Algorithm/QCAlgorithm.Indicators.cs b/Algorithm/QCAlgorithm.Indicators.cs index e86ffd96b864..e399deb7ed30 100644 --- a/Algorithm/QCAlgorithm.Indicators.cs +++ b/Algorithm/QCAlgorithm.Indicators.cs @@ -1364,6 +1364,27 @@ public LeastSquaresMovingAverage LSMA(Symbol symbol, int period, Resolution? res return leastSquaresMovingAverage; } + /// + /// Creates a Least Squares Moving Average indicator for the given target symbol in relation with + /// the reference used, that is, the regression line of the target prices on the reference prices. + /// The indicator will be automatically updated on the given resolution. + /// + /// The target symbol whose LSMA we want + /// The reference symbol to regress the target symbol on + /// The period of the LSMA indicator + /// The resolution + /// Selects a value from the BaseData to send into the indicator, if null defaults to casting the input value to a TradeBar + /// The LeastSquaresMovingAverageWithReference indicator for the given parameters + [DocumentationAttribute(Indicators)] + public LeastSquaresMovingAverageWithReference LSMA(Symbol target, Symbol reference, int period, Resolution? resolution = null, Func selector = null) + { + var name = CreateIndicatorName(QuantConnect.Symbol.None, $"LSMA({period})", resolution); + var leastSquaresMovingAverage = new LeastSquaresMovingAverageWithReference(name, target, reference, period); + InitializeIndicator(leastSquaresMovingAverage, resolution, selector, target, reference); + + return leastSquaresMovingAverage; + } + /// /// Creates a new LinearWeightedMovingAverage indicator. This indicator will linearly distribute /// the weights across the periods. diff --git a/Indicators/LeastSquaresMovingAverageWithReference.cs b/Indicators/LeastSquaresMovingAverageWithReference.cs new file mode 100644 index 000000000000..c8672f26a383 --- /dev/null +++ b/Indicators/LeastSquaresMovingAverageWithReference.cs @@ -0,0 +1,122 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using System; +using System.Linq; +using MathNet.Numerics; +using QuantConnect.Data.Market; + +namespace QuantConnect.Indicators +{ + /// + /// The Least Squares Moving Average (LSMA) of a target in relation with a reference fits a least + /// squares regression line of the target close prices on the reference close prices over the given + /// period, instead of on the time index used by . It then + /// returns the value the regression line takes for the most recent reference price, which is the + /// price the target is expected to have given where the reference is trading. + /// + /// It is common practice to use the SPX index as the reference, so that the indicator describes + /// the target price in terms of the overall market level. + /// + /// The indicator only updates when both assets have a price for a time step. When a bar is missing + /// for one of the assets, the indicator value fills forward to improve the accuracy of the indicator. + /// + public class LeastSquaresMovingAverageWithReference : DualSymbolIndicator + { + /// + /// The point where the regression line crosses the y-axis (target price axis) + /// + public IndicatorBase Intercept { get; } + + /// + /// The regression line slope, the target price change per unit of reference price change + /// + public IndicatorBase Slope { get; } + + /// + /// Creates a new LeastSquaresMovingAverageWithReference indicator with the specified name, + /// target, reference and period values + /// + /// The name of this indicator + /// The target symbol of this indicator + /// The reference symbol of this indicator + /// The period of this indicator + public LeastSquaresMovingAverageWithReference(string name, Symbol targetSymbol, Symbol referenceSymbol, int period) + : base(name, targetSymbol, referenceSymbol, period) + { + // Assert the period is greater than one, otherwise the regression line can not be fitted + if (period < 2) + { + throw new ArgumentException($"Period parameter for LeastSquaresMovingAverageWithReference indicator must be greater than 1 but was {period}."); + } + + Intercept = new Identity(name + "_Intercept"); + Slope = new Identity(name + "_Slope"); + } + + /// + /// Creates a new LeastSquaresMovingAverageWithReference indicator with the specified target, + /// reference and period values + /// + /// The target symbol of this indicator + /// The reference symbol of this indicator + /// The period of this indicator + public LeastSquaresMovingAverageWithReference(Symbol targetSymbol, Symbol referenceSymbol, int period) + : this($"LSMA({period})", targetSymbol, referenceSymbol, period) + { + } + + /// + /// Computes the value the regression line of the target on the reference takes for the + /// most recent reference price + /// + protected override decimal ComputeIndicator() + { + // Until both windows are full, the indicator returns the target price, like the LSMA does + if (!IsReady) + { + return TargetDataPoints[0].Close; + } + + // Both windows only hold the data points of the time steps both symbols have a price for, + // so the target and the reference prices pair up by index + var referencePrices = ReferenceDataPoints.Select(x => (double)x.Close).ToArray(); + var targetPrices = TargetDataPoints.Select(x => (double)x.Close).ToArray(); + var (intercept, slope) = Fit.Line(x: referencePrices, y: targetPrices); + + // The regression line is undefined when the reference price does not change over the period + if (intercept.IsNaNOrInfinity() || slope.IsNaNOrInfinity()) + { + return TargetDataPoints[0].Close; + } + + var endTime = TargetDataPoints[0].EndTime; + Intercept.Update(endTime, intercept.SafeDecimalCast()); + Slope.Update(endTime, slope.SafeDecimalCast()); + + return Intercept.Current.Value + Slope.Current.Value * ReferenceDataPoints[0].Close; + } + + /// + /// Resets this indicator and all sub-indicators (Intercept, Slope) + /// + public override void Reset() + { + Intercept.Reset(); + Slope.Reset(); + base.Reset(); + } + } +} diff --git a/Tests/Algorithm/AlgorithmIndicatorsTests.cs b/Tests/Algorithm/AlgorithmIndicatorsTests.cs index dd0764671c4d..2d3b43d1ab48 100644 --- a/Tests/Algorithm/AlgorithmIndicatorsTests.cs +++ b/Tests/Algorithm/AlgorithmIndicatorsTests.cs @@ -275,6 +275,62 @@ public void BetaCalculation() Assert.AreEqual(new DateTime(2013, 10, 10, 16, 0, 0), lastPoint.Current.EndTime); } + [Test] + public void LeastSquaresMovingAverageWithReferenceCalculation() + { + var period = 10; + var referenceSymbol = Symbol.Create("IBM", SecurityType.Equity, Market.USA); + var indicator = new LeastSquaresMovingAverageWithReference(_equity, referenceSymbol, period); + _algorithm.SetDateTime(new DateTime(2013, 10, 11)); + + // Fit the target closes on the reference closes of the last period time steps both symbols + // have a price for, using the ordinary least squares closed form + var targetCloses = new List(); + var referenceCloses = new List(); + foreach (var slice in _algorithm.History(new[] { _equity, referenceSymbol }, TimeSpan.FromDays(50), Resolution.Daily)) + { + if (slice.Bars.ContainsKey(_equity) && slice.Bars.ContainsKey(referenceSymbol)) + { + targetCloses.Add((double)slice.Bars[_equity].Close); + referenceCloses.Add((double)slice.Bars[referenceSymbol].Close); + } + } + var target = targetCloses.TakeLast(period).ToList(); + var reference = referenceCloses.TakeLast(period).ToList(); + var sumX = reference.Sum(); + var sumY = target.Sum(); + var expectedSlope = (period * reference.Zip(target, (x, y) => x * y).Sum() - sumX * sumY) + / (period * reference.Sum(x => x * x) - sumX * sumX); + var expectedIntercept = (sumY - expectedSlope * sumX) / period; + var expectedValue = expectedIntercept + expectedSlope * reference[^1]; + + var indicatorValues = _algorithm.IndicatorHistory(indicator, new[] { _equity, referenceSymbol }, TimeSpan.FromDays(50), Resolution.Daily); + + Assert.AreEqual(expectedSlope, (double)indicator.Slope.Current.Value, 1e-6); + Assert.AreEqual(expectedIntercept, (double)indicator.Intercept.Current.Value, 1e-6); + Assert.AreEqual(expectedValue, (double)indicator.Current.Value, 1e-6); + Assert.AreEqual(new DateTime(2013, 10, 10, 16, 0, 0), indicator.Current.EndTime); + + // The indicator history is taken on the first of the two updates each time step gets, so + // its last row holds the value the indicator had before the last pair of prices was fit + var lastPoint = indicatorValues.Last(); + Assert.AreEqual(new DateTime(2013, 10, 10, 16, 0, 0), lastPoint.Current.EndTime); + Assert.AreEqual(indicator.Previous.Value, lastPoint.Current.Value); + } + + [Test] + public void LeastSquaresMovingAverageWithReferenceIsWarmedUpByTheAlgorithm() + { + var referenceSymbol = _algorithm.AddEquity("IBM").Symbol; + + var indicator = _algorithm.LSMA(_equity, referenceSymbol, 10, Resolution.Daily); + + Assert.AreEqual("LSMA(10,day)", indicator.Name); + Assert.IsTrue(indicator.IsReady); + Assert.AreNotEqual(0m, indicator.Current.Value); + Assert.AreNotEqual(0m, indicator.Slope.Current.Value); + } + [TestCase(Language.Python)] [TestCase(Language.CSharp)] public void IndicatorsPassingHistory(Language language) diff --git a/Tests/Indicators/LeastSquaresMovingAverageWithReferenceTests.cs b/Tests/Indicators/LeastSquaresMovingAverageWithReferenceTests.cs new file mode 100644 index 000000000000..67789695ab8c --- /dev/null +++ b/Tests/Indicators/LeastSquaresMovingAverageWithReferenceTests.cs @@ -0,0 +1,394 @@ +/* + * QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. + * Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. +*/ + +using NUnit.Framework; +using QuantConnect.Data.Consolidators; +using QuantConnect.Data.Market; +using QuantConnect.Indicators; +using System; +using System.Collections.Generic; +using System.Linq; +using static QuantConnect.Tests.Indicators.TestHelper; + +namespace QuantConnect.Tests.Indicators +{ + /// + /// The expected values of the LSMAWithReference column of bi_datatest.csv were computed with + /// numpy.polyfit, fitting the AMZN closes on the SPX closes of each five point window and + /// evaluating the resulting line at the latest SPX close. + /// + [TestFixture] + public class LeastSquaresMovingAverageWithReferenceTests : CommonIndicatorTests + { + protected override string TestFileName => "bi_datatest.csv"; + + protected override string TestColumnName => "LSMAWithReference"; + + private DateTime _reference = new DateTime(2020, 1, 1); + + protected override IndicatorBase CreateIndicator() + { + Symbol targetSymbol = "AMZN 2T"; + Symbol referenceSymbol = "SPX 2T"; + if (SymbolList.Count > 1) + { + targetSymbol = SymbolList[0]; + referenceSymbol = SymbolList[1]; + } + return new LeastSquaresMovingAverageWithReference("testLSMAWithReferenceIndicator", targetSymbol, referenceSymbol, 5); + } + + protected override List GetSymbols() + { + return [Symbols.SPY, Symbols.AAPL]; + } + + [Test] + public override void TimeMovesForward() + { + var indicator = new LeastSquaresMovingAverageWithReference(Symbols.IBM, Symbols.SPY, 5); + + for (var i = 10; i > 0; i--) + { + indicator.Update(new TradeBar() { Symbol = Symbols.IBM, Low = 1, High = 2, Volume = 100, Close = 500, Time = _reference.AddDays(1 + i) }); + indicator.Update(new TradeBar() { Symbol = Symbols.SPY, Low = 1, High = 2, Volume = 100, Close = 500, Time = _reference.AddDays(1 + i) }); + } + + Assert.AreEqual(2, indicator.Samples); + } + + [Test] + public override void WarmsUpProperly() + { + var indicator = new LeastSquaresMovingAverageWithReference(Symbols.IBM, Symbols.SPY, 5); + var period = ((IIndicatorWarmUpPeriodProvider)indicator).WarmUpPeriod; + + for (var i = 0; i < period; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + indicator.Update(new TradeBar() { Symbol = Symbols.IBM, Low = 1, High = 2, Volume = 100, Close = 500 + i, Time = startTime, EndTime = endTime }); + Assert.IsFalse(indicator.IsReady, $"ready after the target bar of index {i}"); + indicator.Update(new TradeBar() { Symbol = Symbols.SPY, Low = 1, High = 2, Volume = 100, Close = 400 + 2 * i, Time = startTime, EndTime = endTime }); + } + + Assert.IsTrue(indicator.IsReady); + Assert.AreEqual(2 * period, indicator.Samples); + } + + [Test] + public override void WorksWithLowValues() + { + var indicator = new LeastSquaresMovingAverageWithReference(Symbols.IBM, Symbols.SPY, 5); + + var random = new Random(); + for (var i = 0; i < 20; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + var targetValue = (decimal)(random.NextDouble() * 0.000000000000000000000000000001); + var referenceValue = (decimal)(random.NextDouble() * 0.000000000000000000000000000001); + Assert.DoesNotThrow(() => + { + indicator.Update(new TradeBar() { Symbol = Symbols.IBM, Low = targetValue, High = targetValue, Open = targetValue, Close = targetValue, Time = startTime, EndTime = endTime }); + indicator.Update(new TradeBar() { Symbol = Symbols.SPY, Low = referenceValue, High = referenceValue, Open = referenceValue, Close = referenceValue, Time = startTime, EndTime = endTime }); + }); + } + } + + [Test] + public override void TracksPreviousState() + { + var period = 5; + var indicator = new LeastSquaresMovingAverageWithReference(Symbols.SPY, Symbols.AAPL, period); + var previousValue = indicator.Current.Value; + + for (var i = 1; i < 2 * period; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + indicator.Update(new TradeBar() { Symbol = Symbols.SPY, Low = 1, High = 2, Volume = 100, Close = 1000 + i * 10, Time = startTime, EndTime = endTime }); + indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 1000 + (i * 15), Time = startTime, EndTime = endTime }); + + Assert.AreEqual(previousValue, indicator.Previous.Value); + + previousValue = indicator.Current.Value; + } + } + + [Test] + public override void IndicatorShouldHaveSymbolAfterUpdates() + { + var period = 5; + var indicator = new LeastSquaresMovingAverageWithReference(Symbols.SPY, Symbols.AAPL, period); + + for (var i = 0; i < 2 * period; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + // The value takes the symbol of the update it was computed on + indicator.Update(new TradeBar() { Symbol = Symbols.SPY, Low = 1, High = 2, Volume = 100, Close = 1000 + i * 10, Time = startTime, EndTime = endTime }); + Assert.AreEqual(Symbols.SPY, indicator.Current.Symbol); + + indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 1000 + (i * 15), Time = startTime, EndTime = endTime }); + Assert.AreEqual(Symbols.AAPL, indicator.Current.Symbol); + } + } + + [Test] + public override void AcceptsRenkoBarsAsInput() + { + var indicator = CreateIndicator(); + var targetRenkoConsolidator = new RenkoConsolidator(10m); + var referenceRenkoConsolidator = new RenkoConsolidator(10m); + targetRenkoConsolidator.DataConsolidated += (sender, renkoBar) => + { + Assert.DoesNotThrow(() => indicator.Update(renkoBar)); + }; + + referenceRenkoConsolidator.DataConsolidated += (sender, renkoBar) => + { + Assert.DoesNotThrow(() => indicator.Update(renkoBar)); + }; + + foreach (var parts in GetCsvFileStream(TestFileName)) + { + var tradebar = parts.GetTradeBar(); + if (tradebar.Symbol.Value == "AMZN") + { + targetRenkoConsolidator.Update(tradebar); + } + else + { + referenceRenkoConsolidator.Update(tradebar); + } + } + + Assert.IsTrue(indicator.IsReady); + Assert.AreNotEqual(0, indicator.Samples); + targetRenkoConsolidator.Dispose(); + referenceRenkoConsolidator.Dispose(); + } + + [Test] + public override void AcceptsVolumeRenkoBarsAsInput() + { + var indicator = CreateIndicator(); + var targetVolumeRenkoConsolidator = new VolumeRenkoConsolidator(1000000); + var referenceVolumeRenkoConsolidator = new VolumeRenkoConsolidator(1000000000); + targetVolumeRenkoConsolidator.DataConsolidated += (sender, renkoBar) => + { + Assert.DoesNotThrow(() => indicator.Update(renkoBar)); + }; + + referenceVolumeRenkoConsolidator.DataConsolidated += (sender, renkoBar) => + { + Assert.DoesNotThrow(() => indicator.Update(renkoBar)); + }; + + foreach (var parts in GetCsvFileStream(TestFileName)) + { + var tradebar = parts.GetTradeBar(); + if (tradebar.Symbol.Value == "AMZN") + { + targetVolumeRenkoConsolidator.Update(tradebar); + } + else + { + referenceVolumeRenkoConsolidator.Update(tradebar); + } + } + + Assert.IsTrue(indicator.IsReady); + Assert.AreNotEqual(0, indicator.Samples); + targetVolumeRenkoConsolidator.Dispose(); + referenceVolumeRenkoConsolidator.Dispose(); + } + + [Test] + public void AcceptsQuoteBarsAsInput() + { + var indicator = new LeastSquaresMovingAverageWithReference(Symbols.IBM, Symbols.SPY, 5); + + // The target is worth twice the reference plus one at every time step + for (var i = 0; i < 10; i++) + { + var time = _reference.AddDays(1 + i); + var referenceValue = 100 + i; + var targetValue = 2 * referenceValue + 1; + indicator.Update(new QuoteBar { Symbol = Symbols.IBM, Ask = new Bar(1, 2, 1, targetValue), Bid = new Bar(1, 2, 1, targetValue), Time = time }); + indicator.Update(new QuoteBar { Symbol = Symbols.SPY, Ask = new Bar(1, 2, 1, referenceValue), Bid = new Bar(1, 2, 1, referenceValue), Time = time }); + } + + Assert.IsTrue(indicator.IsReady); + Assert.AreEqual(2d, (double)indicator.Slope.Current.Value, 1e-9); + Assert.AreEqual(2 * 109 + 1, (double)indicator.Current.Value, 1e-9); + } + + [Test] + public void ValidateCalculation() + { + var indicator = new LeastSquaresMovingAverageWithReference(Symbols.AAPL, Symbols.SPX, 3); + + var bars = new List() + { + new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 10, Time = _reference.AddDays(1), EndTime = _reference.AddDays(2) }, + new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 35, Time = _reference.AddDays(1), EndTime = _reference.AddDays(2) }, + new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 2, Time = _reference.AddDays(2), EndTime = _reference.AddDays(3) }, + new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 15, Time = _reference.AddDays(3), EndTime = _reference.AddDays(4) }, + new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 80, Time = _reference.AddDays(3), EndTime = _reference.AddDays(4) }, + new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 4, Time = _reference.AddDays(4), EndTime = _reference.AddDays(5) }, + new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 37, Time = _reference.AddDays(5), EndTime = _reference.AddDays(6) }, + new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 90, Time = _reference.AddDays(5), EndTime = _reference.AddDays(6) }, + new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 105, Time = _reference.AddDays(6), EndTime = _reference.AddDays(7) }, + new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 302, Time = _reference.AddDays(6), EndTime = _reference.AddDays(7) }, + }; + + foreach (var bar in bars) + { + indicator.Update(bar); + } + + // Only the time steps both symbols have a price for are paired up, and only the last + // three of them are held by the indicator windows + var closeAAPL = new List() { 15, 90, 105 }; + var closeSPX = new List() { 80, 37, 302 }; + + // Fitting closeAAPL on closeSPX with the ordinary least squares closed form + var count = closeSPX.Count; + var sumX = closeSPX.Sum(); + var sumY = closeAAPL.Sum(); + var sumXy = closeSPX.Zip(closeAAPL, (x, y) => x * y).Sum(); + var sumXx = closeSPX.Sum(x => x * x); + var expectedSlope = (count * sumXy - sumX * sumY) / (count * sumXx - sumX * sumX); + var expectedIntercept = (sumY - expectedSlope * sumX) / count; + var expectedValue = expectedIntercept + expectedSlope * closeSPX[^1]; + + Assert.AreEqual(expectedSlope, (double)indicator.Slope.Current.Value, 1e-9); + Assert.AreEqual(expectedIntercept, (double)indicator.Intercept.Current.Value, 1e-9); + Assert.AreEqual(expectedValue, (double)indicator.Current.Value, 1e-9); + } + + [Test] + public void ProjectsTheReferenceWithALinearRelationship() + { + var indicator = new LeastSquaresMovingAverageWithReference(Symbols.AAPL, Symbols.SPX, 5); + + // The target is worth twice the reference plus one at every time step + for (var i = 0; i < 10; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + var referenceValue = 100 + i; + indicator.Update(new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = referenceValue, Time = startTime, EndTime = endTime }); + indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 2 * referenceValue + 1, Time = startTime, EndTime = endTime }); + } + + Assert.AreEqual(2d, (double)indicator.Slope.Current.Value, 1e-9); + Assert.AreEqual(1d, (double)indicator.Intercept.Current.Value, 1e-9); + Assert.AreEqual(2 * 109 + 1, (double)indicator.Current.Value, 1e-9); + } + + [Test] + public void ReturnsTheTargetPriceWhenTheReferenceDoesNotChange() + { + var indicator = new LeastSquaresMovingAverageWithReference(Symbols.AAPL, Symbols.SPX, 5); + + for (var i = 0; i < 10; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 200 + i, Time = startTime, EndTime = endTime }); + indicator.Update(new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 100, Time = startTime, EndTime = endTime }); + } + + // The regression line can not be fitted, so the indicator falls back to the target price + Assert.AreEqual(209m, indicator.Current.Value); + Assert.AreEqual(0m, indicator.Slope.Current.Value); + Assert.AreEqual(0m, indicator.Intercept.Current.Value); + } + + [Test] + public void WorksWithDifferentTimeZones() + { + var indicator = new LeastSquaresMovingAverageWithReference(Symbols.SPY, Symbols.BTCUSD, 5); + + for (var i = 0; i < 10; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + indicator.Update(new TradeBar() { Symbol = Symbols.SPY, Low = 1, High = 2, Volume = 100, Close = 2 * (100 + i) + 1, Time = startTime, EndTime = endTime }); + indicator.Update(new TradeBar() { Symbol = Symbols.BTCUSD, Low = 1, High = 2, Volume = 100, Close = 100 + i, Time = startTime, EndTime = endTime }); + } + + Assert.IsTrue(indicator.IsReady); + Assert.AreEqual(2d, (double)indicator.Slope.Current.Value, 1e-9); + Assert.AreEqual(2 * 109 + 1, (double)indicator.Current.Value, 1e-9); + } + + [Test] + public void PairsPricesByTimeRegardlessOfArrivalOrder() + { + var targetFirst = new LeastSquaresMovingAverageWithReference(Symbols.AAPL, Symbols.SPX, 5); + var referenceFirst = new LeastSquaresMovingAverageWithReference(Symbols.AAPL, Symbols.SPX, 5); + + for (var i = 0; i < 10; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + var targetBar = new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 200 + i * 3, Time = startTime, EndTime = endTime }; + var referenceBar = new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 100 + i, Time = startTime, EndTime = endTime }; + + targetFirst.Update(targetBar); + targetFirst.Update(referenceBar); + + referenceFirst.Update(referenceBar); + referenceFirst.Update(targetBar); + } + + Assert.IsTrue(targetFirst.IsReady); + Assert.AreEqual(targetFirst.Current.Value, referenceFirst.Current.Value); + } + + [Test] + public void DoesNotPairPricesFromDifferentTimes() + { + var indicator = new LeastSquaresMovingAverageWithReference(Symbols.AAPL, Symbols.SPX, 5); + + for (var i = 0; i < 5; i++) + { + var startTime = _reference.AddDays(1 + i); + var endTime = startTime.AddDays(1); + indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 200 + i * 3, Time = startTime, EndTime = endTime }); + indicator.Update(new TradeBar() { Symbol = Symbols.SPX, Low = 1, High = 2, Volume = 100, Close = 100 + i, Time = startTime, EndTime = endTime }); + } + + var lastValue = indicator.Current.Value; + + // The target bar of the next time step leaves the reference behind, so no price is paired up + var lastStartTime = _reference.AddDays(6); + indicator.Update(new TradeBar() { Symbol = Symbols.AAPL, Low = 1, High = 2, Volume = 100, Close = 500, Time = lastStartTime, EndTime = lastStartTime.AddDays(1) }); + + Assert.AreEqual(lastValue, indicator.Current.Value); + } + + [Test] + public void ThrowsOnPeriodBelowTwo() + { + Assert.Throws(() => + new LeastSquaresMovingAverageWithReference(Symbols.AAPL, Symbols.SPX, 1)); + } + } +} diff --git a/Tests/TestData/bi_datatest.csv b/Tests/TestData/bi_datatest.csv index f314105b4b9f..62b0cd0600a6 100644 --- a/Tests/TestData/bi_datatest.csv +++ b/Tests/TestData/bi_datatest.csv @@ -1,51 +1,51 @@ -Symbol,Date,High,Low,Open,Close,Volume,Adj Close,Beta -AMZN,20211004,3279.989990234375,3176.25,3279.389892578125,3189.780029296875,4523100,3189.780029296875,0 -SPX,20211004,4355.509765625,4278.93994140625,4348.83984375,4300.4599609375,3110560000,4300.4599609375,0 -AMZN,20211005,3260.72998046875,3202.4599609375,3204.5,3221.0,3269200,3221.0,0 -SPX,20211005,4369.22998046875,4309.8701171875,4309.8701171875,4345.72021484375,2967400000,4345.72021484375,0 -AMZN,20211006,3264.340087890625,3198.6201171875,3213.530029296875,3262.010009765625,2533000,3262.010009765625,0 -SPX,20211006,4365.56982421875,4290.490234375,4319.56982421875,4363.5498046875,3219590000,4363.5498046875,-0.458533447 -AMZN,20211007,3325.75,3283.06005859375,3291.5400390625,3302.429931640625,2409100,3302.429931640625,-0.458533447 -SPX,20211007,4429.97021484375,4383.72998046875,4383.72998046875,4399.759765625,3096080000,4399.759765625,-0.409675968 -AMZN,20211008,3321.429931640625,3288.199951171875,3317.0,3288.6201171875,1995500,3288.6201171875,-0.409675968 -SPX,20211008,4412.02001953125,4386.22021484375,4406.509765625,4391.33984375,2401890000,4391.33984375,1.166105454 -AMZN,20211011,3292.590087890625,3238.10009765625,3275.0,3246.300048828125,2034200,3246.300048828125,1.166105454 -SPX,20211011,4415.8798828125,4360.58984375,4385.43994140625,4361.18994140625,2580000000,4361.18994140625,1.46640552 -AMZN,20211012,3267.530029296875,3236.280029296875,3257.0,3247.330078125,1819600,3247.330078125,1.46640552 -SPX,20211012,4374.89013671875,4342.08984375,4368.31005859375,4350.64990234375,2608150000,4350.64990234375,1.750343086 -AMZN,20211013,3288.3798828125,3261.090087890625,3269.7099609375,3284.280029296875,2420100,3284.280029296875,1.750343086 -SPX,20211013,4372.8701171875,4329.919921875,4358.009765625,4363.7998046875,2926460000,4363.7998046875,1.727848937 -AMZN,20211014,3312.60009765625,3290.780029296875,3302.449951171875,3299.860107421875,2109500,3299.860107421875,1.727848937 -SPX,20211014,4439.72998046875,4386.75,4386.75,4438.259765625,2642920000,4438.259765625,0.621509074 -AMZN,20211015,3410.419921875,3304.0,3311.419921875,3409.02001953125,5175100,3409.02001953125,0.621509074 -SPX,20211015,4475.81982421875,4447.68994140625,4447.68994140625,4471.3701171875,3000560000,4471.3701171875,0.922922917 -AMZN,20211018,3449.169921875,3385.10009765625,3388.360107421875,3446.739990234375,3174100,3446.739990234375,0.922922917 -SPX,20211018,4488.75,4447.47021484375,4463.72021484375,4486.4599609375,2683540000,4486.4599609375,0.25307576 -AMZN,20211019,3454.68994140625,3422.0,3434.2900390625,3444.14990234375,2386100,3444.14990234375,0.25307576 -SPX,20211019,4520.39990234375,4496.41015625,4497.33984375,4519.6298828125,2531210000,4519.6298828125,-0.484044822 -AMZN,20211020,3462.860107421875,3400.3701171875,3452.659912109375,3415.06005859375,2139800,3415.06005859375,-0.484044822 -SPX,20211020,4540.8701171875,4524.39990234375,4524.419921875,4536.18994140625,2671560000,4536.18994140625,0.157447516 -AMZN,20211021,3440.280029296875,3403.0,3414.25,3435.010009765625,1881400,3435.010009765625,0.157447516 -SPX,20211021,4551.43994140625,4526.89013671875,4532.240234375,4549.77978515625,3016950000,4549.77978515625,3.070627551 -AMZN,20211022,3429.840087890625,3331.300048828125,3421.0,3335.550048828125,3139100,3335.550048828125,3.070627551 -SPX,20211022,4559.669921875,4524.0,4546.1201171875,4544.89990234375,3062810000,4544.89990234375,3.293313624 -AMZN,20211025,3347.800048828125,3297.699951171875,3335.0,3320.3701171875,2226000,3320.3701171875,3.293313624 -SPX,20211025,4572.6201171875,4537.35986328125,4553.68994140625,4566.47998046875,3250210000,4566.47998046875,3.196949447 -AMZN,20211026,3416.1201171875,3343.97998046875,3349.510009765625,3376.070068359375,2698300,3376.070068359375,3.196949447 -SPX,20211026,4598.52978515625,4569.169921875,4578.68994140625,4574.7900390625,2866500000,4574.7900390625,3.703064397 -AMZN,20211027,3437.0,3371.449951171875,3388.0,3392.489990234375,2702200,3392.489990234375,3.703064397 -SPX,20211027,4584.56982421875,4551.66015625,4580.22021484375,4551.68017578125,3259510000,4551.68017578125,0.626560824 -AMZN,20211028,3479.0,3386.0,3402.10009765625,3446.570068359375,5708700,3446.570068359375,0.626560824 -SPX,20211028,4597.5498046875,4562.83984375,4562.83984375,4596.419921875,3197560000,4596.419921875,1.278764178 -AMZN,20211029,3374.820068359375,3273.320068359375,3300.02001953125,3372.429931640625,6469500,3372.429931640625,1.278764178 -SPX,20211029,4608.080078125,4567.58984375,4572.8701171875,4605.3798828125,3632260000,4605.3798828125,0.588131043 -AMZN,20211101,3375.860107421875,3292.02001953125,3361.800048828125,3318.110107421875,3608900,3318.110107421875,0.588131043 -SPX,20211101,4620.33984375,4595.06005859375,4610.6201171875,4613.669921875,2924000000,4613.669921875,0.826491586 -AMZN,20211102,3331.1201171875,3283.550048828125,3315.010009765625,3312.75,2627600,3312.75,0.826491586 -SPX,20211102,4635.14990234375,4613.33984375,4613.33984375,4630.64990234375,3309690000,4630.64990234375,0.887785578 -AMZN,20211103,3394.919921875,3297.52001953125,3309.0,3384.0,3397200,3384.0,0.887785578 -SPX,20211103,4663.4599609375,4621.18994140625,4630.64990234375,4660.56982421875,3339440000,4660.56982421875,4.895868782 -AMZN,20211104,3498.6298828125,3365.0,3370.0,3477.0,5346300,3477.0,4.895868782 -SPX,20211104,4683.0,4662.58984375,4662.93017578125,4680.06005859375,3332940000,4680.06005859375,9.779665289 -AMZN,20211105,3566.25,3476.97998046875,3477.0,3549.01953125,2937769,3549.01953125,9.779665289 -SPX,20211105,4718.5,4699.259765625,4699.259765625,4701.85986328125,1462781000,4701.85986328125,8.761816471 \ No newline at end of file +Symbol,Date,High,Low,Open,Close,Volume,Adj Close,Beta,LSMAWithReference +AMZN,20211004,3279.989990234375,3176.25,3279.389892578125,3189.780029296875,4523100,3189.780029296875,0,0.000000000 +SPX,20211004,4355.509765625,4278.93994140625,4348.83984375,4300.4599609375,3110560000,4300.4599609375,0,3189.780029297 +AMZN,20211005,3260.72998046875,3202.4599609375,3204.5,3221.0,3269200,3221.0,0,3189.780029297 +SPX,20211005,4369.22998046875,4309.8701171875,4309.8701171875,4345.72021484375,2967400000,4345.72021484375,0,3221.000000000 +AMZN,20211006,3264.340087890625,3198.6201171875,3213.530029296875,3262.010009765625,2533000,3262.010009765625,0,3221.000000000 +SPX,20211006,4365.56982421875,4290.490234375,4319.56982421875,4363.5498046875,3219590000,4363.5498046875,-0.458533447,3262.010009766 +AMZN,20211007,3325.75,3283.06005859375,3291.5400390625,3302.429931640625,2409100,3302.429931640625,-0.458533447,3262.010009766 +SPX,20211007,4429.97021484375,4383.72998046875,4383.72998046875,4399.759765625,3096080000,4399.759765625,-0.409675968,3302.429931641 +AMZN,20211008,3321.429931640625,3288.199951171875,3317.0,3288.6201171875,1995500,3288.6201171875,-0.409675968,3302.429931641 +SPX,20211008,4412.02001953125,4386.22021484375,4406.509765625,4391.33984375,2401890000,4391.33984375,1.166105454,3288.922756555 +AMZN,20211011,3292.590087890625,3238.10009765625,3275.0,3246.300048828125,2034200,3246.300048828125,1.166105454,3288.922756555 +SPX,20211011,4415.8798828125,4360.58984375,4385.43994140625,4361.18994140625,2580000000,4361.18994140625,1.46640552,3248.233118518 +AMZN,20211012,3267.530029296875,3236.280029296875,3257.0,3247.330078125,1819600,3247.330078125,1.46640552,3248.233118518 +SPX,20211012,4374.89013671875,4342.08984375,4368.31005859375,4350.64990234375,2608150000,4350.64990234375,1.750343086,3242.974531805 +AMZN,20211013,3288.3798828125,3261.090087890625,3269.7099609375,3284.280029296875,2420100,3284.280029296875,1.750343086,3242.974531805 +SPX,20211013,4372.8701171875,4329.919921875,4358.009765625,4363.7998046875,2926460000,4363.7998046875,1.727848937,3263.800718699 +AMZN,20211014,3312.60009765625,3290.780029296875,3302.449951171875,3299.860107421875,2109500,3299.860107421875,1.727848937,3263.800718699 +SPX,20211014,4439.72998046875,4386.75,4386.75,4438.259765625,2642920000,4438.259765625,0.621509074,3305.502789252 +AMZN,20211015,3410.419921875,3304.0,3311.419921875,3409.02001953125,5175100,3409.02001953125,0.621509074,3305.502789252 +SPX,20211015,4475.81982421875,4447.68994140625,4447.68994140625,4471.3701171875,3000560000,4471.3701171875,0.922922917,3379.397047644 +AMZN,20211018,3449.169921875,3385.10009765625,3388.360107421875,3446.739990234375,3174100,3446.739990234375,0.922922917,3379.397047644 +SPX,20211018,4488.75,4447.47021484375,4463.72021484375,4486.4599609375,2683540000,4486.4599609375,0.25307576,3419.241436736 +AMZN,20211019,3454.68994140625,3422.0,3434.2900390625,3444.14990234375,2386100,3444.14990234375,0.25307576,3419.241436736 +SPX,20211019,4520.39990234375,4496.41015625,4497.33984375,4519.6298828125,2531210000,4519.6298828125,-0.484044822,3453.155478136 +AMZN,20211020,3462.860107421875,3400.3701171875,3452.659912109375,3415.06005859375,2139800,3415.06005859375,-0.484044822,3453.155478136 +SPX,20211020,4540.8701171875,4524.39990234375,4524.419921875,4536.18994140625,2671560000,4536.18994140625,0.157447516,3454.822602296 +AMZN,20211021,3440.280029296875,3403.0,3414.25,3435.010009765625,1881400,3435.010009765625,0.157447516,3454.822602296 +SPX,20211021,4551.43994140625,4526.89013671875,4532.240234375,4549.77978515625,3016950000,4549.77978515625,3.070627551,3433.056998135 +AMZN,20211022,3429.840087890625,3331.300048828125,3421.0,3335.550048828125,3139100,3335.550048828125,3.070627551,3433.056998135 +SPX,20211022,4559.669921875,4524.0,4546.1201171875,4544.89990234375,3062810000,4544.89990234375,3.293313624,3398.815314759 +AMZN,20211025,3347.800048828125,3297.699951171875,3335.0,3320.3701171875,2226000,3320.3701171875,3.293313624,3398.815314759 +SPX,20211025,4572.6201171875,4537.35986328125,4553.68994140625,4566.47998046875,3250210000,4566.47998046875,3.196949447,3334.489412197 +AMZN,20211026,3416.1201171875,3343.97998046875,3349.510009765625,3376.070068359375,2698300,3376.070068359375,3.196949447,3334.489412197 +SPX,20211026,4598.52978515625,4569.169921875,4578.68994140625,4574.7900390625,2866500000,4574.7900390625,3.703064397,3350.672032735 +AMZN,20211027,3437.0,3371.449951171875,3388.0,3392.489990234375,2702200,3392.489990234375,3.703064397,3350.672032735 +SPX,20211027,4584.56982421875,4551.66015625,4580.22021484375,4551.68017578125,3259510000,4551.68017578125,0.626560824,3376.891332611 +AMZN,20211028,3479.0,3386.0,3402.10009765625,3446.570068359375,5708700,3446.570068359375,0.626560824,3376.891332611 +SPX,20211028,4597.5498046875,4562.83984375,4562.83984375,4596.419921875,3197560000,4596.419921875,1.278764178,3423.423944006 +AMZN,20211029,3374.820068359375,3273.320068359375,3300.02001953125,3372.429931640625,6469500,3372.429931640625,1.278764178,3423.423944006 +SPX,20211029,4608.080078125,4567.58984375,4572.8701171875,4605.3798828125,3632260000,4605.3798828125,0.588131043,3400.611389422 +AMZN,20211101,3375.860107421875,3292.02001953125,3361.800048828125,3318.110107421875,3608900,3318.110107421875,0.588131043,3400.611389422 +SPX,20211101,4620.33984375,4595.06005859375,4610.6201171875,4613.669921875,2924000000,4613.669921875,0.826491586,3365.476087161 +AMZN,20211102,3331.1201171875,3283.550048828125,3315.010009765625,3312.75,2627600,3312.75,0.826491586,3365.476087161 +SPX,20211102,4635.14990234375,4613.33984375,4613.33984375,4630.64990234375,3309690000,4630.64990234375,0.887785578,3334.598669514 +AMZN,20211103,3394.919921875,3297.52001953125,3309.0,3384.0,3397200,3384.0,0.887785578,3334.598669514 +SPX,20211103,4663.4599609375,4621.18994140625,4630.64990234375,4660.56982421875,3339440000,4660.56982421875,4.895868782,3343.285091967 +AMZN,20211104,3498.6298828125,3365.0,3370.0,3477.0,5346300,3477.0,4.895868782,3343.285091967 +SPX,20211104,4683.0,4662.58984375,4662.93017578125,4680.06005859375,3332940000,4680.06005859375,9.779665289,3440.447882539 +AMZN,20211105,3566.25,3476.97998046875,3477.0,3549.01953125,2937769,3549.01953125,9.779665289,3440.447882539 +SPX,20211105,4718.5,4699.259765625,4699.259765625,4701.85986328125,1462781000,4701.85986328125,8.761816471,3531.706432825 \ No newline at end of file