Skip to content

Commit 2a0030a

Browse files
authored
Add a Least Squares Moving Average with a benchmark reference (#9761)
Co-authored-by: 0xpinara <191243209+0xpinara@users.noreply.github.com>
1 parent 829dfc4 commit 2a0030a

5 files changed

Lines changed: 644 additions & 51 deletions

File tree

Algorithm/QCAlgorithm.Indicators.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,27 @@ public LeastSquaresMovingAverage LSMA(Symbol symbol, int period, Resolution? res
13641364
return leastSquaresMovingAverage;
13651365
}
13661366

1367+
/// <summary>
1368+
/// Creates a Least Squares Moving Average indicator for the given target symbol in relation with
1369+
/// the reference used, that is, the regression line of the target prices on the reference prices.
1370+
/// The indicator will be automatically updated on the given resolution.
1371+
/// </summary>
1372+
/// <param name="target">The target symbol whose LSMA we want</param>
1373+
/// <param name="reference">The reference symbol to regress the target symbol on</param>
1374+
/// <param name="period">The period of the LSMA indicator</param>
1375+
/// <param name="resolution">The resolution</param>
1376+
/// <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>
1377+
/// <returns>The LeastSquaresMovingAverageWithReference indicator for the given parameters</returns>
1378+
[DocumentationAttribute(Indicators)]
1379+
public LeastSquaresMovingAverageWithReference LSMA(Symbol target, Symbol reference, int period, Resolution? resolution = null, Func<IBaseData, IBaseDataBar> selector = null)
1380+
{
1381+
var name = CreateIndicatorName(QuantConnect.Symbol.None, $"LSMA({period})", resolution);
1382+
var leastSquaresMovingAverage = new LeastSquaresMovingAverageWithReference(name, target, reference, period);
1383+
InitializeIndicator(leastSquaresMovingAverage, resolution, selector, target, reference);
1384+
1385+
return leastSquaresMovingAverage;
1386+
}
1387+
13671388
/// <summary>
13681389
/// Creates a new LinearWeightedMovingAverage indicator. This indicator will linearly distribute
13691390
/// the weights across the periods.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Linq;
18+
using MathNet.Numerics;
19+
using QuantConnect.Data.Market;
20+
21+
namespace QuantConnect.Indicators
22+
{
23+
/// <summary>
24+
/// The Least Squares Moving Average (LSMA) of a target in relation with a reference fits a least
25+
/// squares regression line of the target close prices on the reference close prices over the given
26+
/// period, instead of on the time index used by <see cref="LeastSquaresMovingAverage"/>. It then
27+
/// returns the value the regression line takes for the most recent reference price, which is the
28+
/// price the target is expected to have given where the reference is trading.
29+
///
30+
/// It is common practice to use the SPX index as the reference, so that the indicator describes
31+
/// the target price in terms of the overall market level.
32+
///
33+
/// The indicator only updates when both assets have a price for a time step. When a bar is missing
34+
/// for one of the assets, the indicator value fills forward to improve the accuracy of the indicator.
35+
/// </summary>
36+
public class LeastSquaresMovingAverageWithReference : DualSymbolIndicator<IBaseDataBar>
37+
{
38+
/// <summary>
39+
/// The point where the regression line crosses the y-axis (target price axis)
40+
/// </summary>
41+
public IndicatorBase<IndicatorDataPoint> Intercept { get; }
42+
43+
/// <summary>
44+
/// The regression line slope, the target price change per unit of reference price change
45+
/// </summary>
46+
public IndicatorBase<IndicatorDataPoint> Slope { get; }
47+
48+
/// <summary>
49+
/// Creates a new LeastSquaresMovingAverageWithReference indicator with the specified name,
50+
/// target, reference and period values
51+
/// </summary>
52+
/// <param name="name">The name of this indicator</param>
53+
/// <param name="targetSymbol">The target symbol of this indicator</param>
54+
/// <param name="referenceSymbol">The reference symbol of this indicator</param>
55+
/// <param name="period">The period of this indicator</param>
56+
public LeastSquaresMovingAverageWithReference(string name, Symbol targetSymbol, Symbol referenceSymbol, int period)
57+
: base(name, targetSymbol, referenceSymbol, period)
58+
{
59+
// Assert the period is greater than one, otherwise the regression line can not be fitted
60+
if (period < 2)
61+
{
62+
throw new ArgumentException($"Period parameter for LeastSquaresMovingAverageWithReference indicator must be greater than 1 but was {period}.");
63+
}
64+
65+
Intercept = new Identity(name + "_Intercept");
66+
Slope = new Identity(name + "_Slope");
67+
}
68+
69+
/// <summary>
70+
/// Creates a new LeastSquaresMovingAverageWithReference indicator with the specified target,
71+
/// reference and period values
72+
/// </summary>
73+
/// <param name="targetSymbol">The target symbol of this indicator</param>
74+
/// <param name="referenceSymbol">The reference symbol of this indicator</param>
75+
/// <param name="period">The period of this indicator</param>
76+
public LeastSquaresMovingAverageWithReference(Symbol targetSymbol, Symbol referenceSymbol, int period)
77+
: this($"LSMA({period})", targetSymbol, referenceSymbol, period)
78+
{
79+
}
80+
81+
/// <summary>
82+
/// Computes the value the regression line of the target on the reference takes for the
83+
/// most recent reference price
84+
/// </summary>
85+
protected override decimal ComputeIndicator()
86+
{
87+
// Until both windows are full, the indicator returns the target price, like the LSMA does
88+
if (!IsReady)
89+
{
90+
return TargetDataPoints[0].Close;
91+
}
92+
93+
// Both windows only hold the data points of the time steps both symbols have a price for,
94+
// so the target and the reference prices pair up by index
95+
var referencePrices = ReferenceDataPoints.Select(x => (double)x.Close).ToArray();
96+
var targetPrices = TargetDataPoints.Select(x => (double)x.Close).ToArray();
97+
var (intercept, slope) = Fit.Line(x: referencePrices, y: targetPrices);
98+
99+
// The regression line is undefined when the reference price does not change over the period
100+
if (intercept.IsNaNOrInfinity() || slope.IsNaNOrInfinity())
101+
{
102+
return TargetDataPoints[0].Close;
103+
}
104+
105+
var endTime = TargetDataPoints[0].EndTime;
106+
Intercept.Update(endTime, intercept.SafeDecimalCast());
107+
Slope.Update(endTime, slope.SafeDecimalCast());
108+
109+
return Intercept.Current.Value + Slope.Current.Value * ReferenceDataPoints[0].Close;
110+
}
111+
112+
/// <summary>
113+
/// Resets this indicator and all sub-indicators (Intercept, Slope)
114+
/// </summary>
115+
public override void Reset()
116+
{
117+
Intercept.Reset();
118+
Slope.Reset();
119+
base.Reset();
120+
}
121+
}
122+
}

Tests/Algorithm/AlgorithmIndicatorsTests.cs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,62 @@ public void BetaCalculation()
275275
Assert.AreEqual(new DateTime(2013, 10, 10, 16, 0, 0), lastPoint.Current.EndTime);
276276
}
277277

278+
[Test]
279+
public void LeastSquaresMovingAverageWithReferenceCalculation()
280+
{
281+
var period = 10;
282+
var referenceSymbol = Symbol.Create("IBM", SecurityType.Equity, Market.USA);
283+
var indicator = new LeastSquaresMovingAverageWithReference(_equity, referenceSymbol, period);
284+
_algorithm.SetDateTime(new DateTime(2013, 10, 11));
285+
286+
// Fit the target closes on the reference closes of the last period time steps both symbols
287+
// have a price for, using the ordinary least squares closed form
288+
var targetCloses = new List<double>();
289+
var referenceCloses = new List<double>();
290+
foreach (var slice in _algorithm.History(new[] { _equity, referenceSymbol }, TimeSpan.FromDays(50), Resolution.Daily))
291+
{
292+
if (slice.Bars.ContainsKey(_equity) && slice.Bars.ContainsKey(referenceSymbol))
293+
{
294+
targetCloses.Add((double)slice.Bars[_equity].Close);
295+
referenceCloses.Add((double)slice.Bars[referenceSymbol].Close);
296+
}
297+
}
298+
var target = targetCloses.TakeLast(period).ToList();
299+
var reference = referenceCloses.TakeLast(period).ToList();
300+
var sumX = reference.Sum();
301+
var sumY = target.Sum();
302+
var expectedSlope = (period * reference.Zip(target, (x, y) => x * y).Sum() - sumX * sumY)
303+
/ (period * reference.Sum(x => x * x) - sumX * sumX);
304+
var expectedIntercept = (sumY - expectedSlope * sumX) / period;
305+
var expectedValue = expectedIntercept + expectedSlope * reference[^1];
306+
307+
var indicatorValues = _algorithm.IndicatorHistory(indicator, new[] { _equity, referenceSymbol }, TimeSpan.FromDays(50), Resolution.Daily);
308+
309+
Assert.AreEqual(expectedSlope, (double)indicator.Slope.Current.Value, 1e-6);
310+
Assert.AreEqual(expectedIntercept, (double)indicator.Intercept.Current.Value, 1e-6);
311+
Assert.AreEqual(expectedValue, (double)indicator.Current.Value, 1e-6);
312+
Assert.AreEqual(new DateTime(2013, 10, 10, 16, 0, 0), indicator.Current.EndTime);
313+
314+
// The indicator history is taken on the first of the two updates each time step gets, so
315+
// its last row holds the value the indicator had before the last pair of prices was fit
316+
var lastPoint = indicatorValues.Last();
317+
Assert.AreEqual(new DateTime(2013, 10, 10, 16, 0, 0), lastPoint.Current.EndTime);
318+
Assert.AreEqual(indicator.Previous.Value, lastPoint.Current.Value);
319+
}
320+
321+
[Test]
322+
public void LeastSquaresMovingAverageWithReferenceIsWarmedUpByTheAlgorithm()
323+
{
324+
var referenceSymbol = _algorithm.AddEquity("IBM").Symbol;
325+
326+
var indicator = _algorithm.LSMA(_equity, referenceSymbol, 10, Resolution.Daily);
327+
328+
Assert.AreEqual("LSMA(10,day)", indicator.Name);
329+
Assert.IsTrue(indicator.IsReady);
330+
Assert.AreNotEqual(0m, indicator.Current.Value);
331+
Assert.AreNotEqual(0m, indicator.Slope.Current.Value);
332+
}
333+
278334
[TestCase(Language.Python)]
279335
[TestCase(Language.CSharp)]
280336
public void IndicatorsPassingHistory(Language language)

0 commit comments

Comments
 (0)