Skip to content
Open
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
21 changes: 21 additions & 0 deletions Algorithm/QCAlgorithm.Indicators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1364,6 +1364,27 @@ public LeastSquaresMovingAverage LSMA(Symbol symbol, int period, Resolution? res
return leastSquaresMovingAverage;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="target">The target symbol whose LSMA we want</param>
/// <param name="reference">The reference symbol to regress the target symbol on</param>
/// <param name="period">The period of the LSMA indicator</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>The LeastSquaresMovingAverageWithReference indicator for the given parameters</returns>
[DocumentationAttribute(Indicators)]
public LeastSquaresMovingAverageWithReference LSMA(Symbol target, Symbol reference, int period, Resolution? resolution = null, Func<IBaseData, IBaseDataBar> 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;
}

/// <summary>
/// Creates a new LinearWeightedMovingAverage indicator. This indicator will linearly distribute
/// the weights across the periods.
Expand Down
122 changes: 122 additions & 0 deletions Indicators/LeastSquaresMovingAverageWithReference.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// 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 <see cref="LeastSquaresMovingAverage"/>. 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.
/// </summary>
public class LeastSquaresMovingAverageWithReference : DualSymbolIndicator<IBaseDataBar>
{
/// <summary>
/// The point where the regression line crosses the y-axis (target price axis)
/// </summary>
public IndicatorBase<IndicatorDataPoint> Intercept { get; }

/// <summary>
/// The regression line slope, the target price change per unit of reference price change
/// </summary>
public IndicatorBase<IndicatorDataPoint> Slope { get; }

/// <summary>
/// Creates a new LeastSquaresMovingAverageWithReference indicator with the specified name,
/// target, reference and period values
/// </summary>
/// <param name="name">The name of this indicator</param>
/// <param name="targetSymbol">The target symbol of this indicator</param>
/// <param name="referenceSymbol">The reference symbol of this indicator</param>
/// <param name="period">The period of this indicator</param>
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");
}

/// <summary>
/// Creates a new LeastSquaresMovingAverageWithReference indicator with the specified target,
/// reference and period values
/// </summary>
/// <param name="targetSymbol">The target symbol of this indicator</param>
/// <param name="referenceSymbol">The reference symbol of this indicator</param>
/// <param name="period">The period of this indicator</param>
public LeastSquaresMovingAverageWithReference(Symbol targetSymbol, Symbol referenceSymbol, int period)
: this($"LSMA({period})", targetSymbol, referenceSymbol, period)
{
}

/// <summary>
/// Computes the value the regression line of the target on the reference takes for the
/// most recent reference price
/// </summary>
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;
}

/// <summary>
/// Resets this indicator and all sub-indicators (Intercept, Slope)
/// </summary>
public override void Reset()
{
Intercept.Reset();
Slope.Reset();
base.Reset();
}
}
}
56 changes: 56 additions & 0 deletions Tests/Algorithm/AlgorithmIndicatorsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<double>();
var referenceCloses = new List<double>();
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)
Expand Down
Loading