Files
quantconnect--lean/Tests/Indicators/IndicatorExtensionsTests.cs
T
2026-07-13 13:02:50 +08:00

749 lines
26 KiB
C#

/*
* 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.Collections.Generic;
using NUnit.Framework;
using QuantConnect.Indicators;
using System.Linq;
using Python.Runtime;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Statistics;
namespace QuantConnect.Tests.Indicators
{
[TestFixture]
public class IndicatorExtensionsTests
{
[Test, Parallelizable(ParallelScope.Self)]
public void PipesDataUsingOfFromFirstToSecond()
{
var first = new SimpleMovingAverage(2);
var second = new Delay(1);
// this is a configuration step, but returns the reference to the second for method chaining
second.Of(first);
var data1 = new IndicatorDataPoint(DateTime.UtcNow, 1m);
var data2 = new IndicatorDataPoint(DateTime.UtcNow, 2m);
var data3 = new IndicatorDataPoint(DateTime.UtcNow, 3m);
var data4 = new IndicatorDataPoint(DateTime.UtcNow, 4m);
// sma has one item
first.Update(data1);
Assert.IsFalse(first.IsReady);
Assert.AreEqual(0m, second.Current.Value);
// sma is ready, delay will repeat this value
first.Update(data2);
Assert.IsTrue(first.IsReady);
Assert.IsFalse(second.IsReady);
Assert.AreEqual(1.5m, second.Current.Value);
// delay is ready, and repeats its first input
first.Update(data3);
Assert.IsTrue(second.IsReady);
Assert.AreEqual(1.5m, second.Current.Value);
// now getting the delayed data
first.Update(data4);
Assert.AreEqual(2.5m, second.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void PipesDataFirstWeightedBySecond()
{
const int period = 4;
var value = new Identity("Value");
var weight = new Identity("Weight");
var third = value.WeightedBy(weight, period);
var data = Enumerable.Range(1, 10).ToList();
var window = Enumerable.Reverse(data).Take(period);
var current = window.Sum(x => 2 * x * x) / (decimal)window.Sum(x => x);
foreach (var item in data)
{
value.Update(new IndicatorDataPoint(DateTime.UtcNow, Convert.ToDecimal(2 * item)));
weight.Update(new IndicatorDataPoint(DateTime.UtcNow, Convert.ToDecimal(item)));
}
Assert.AreEqual(current, third.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void NewDataPushesToDerivedIndicators()
{
var identity = new Identity("identity");
var sma = new SimpleMovingAverage(3);
identity.Updated += (sender, consolidated) =>
{
sma.Update(consolidated);
};
identity.Update(DateTime.UtcNow, 1m);
identity.Update(DateTime.UtcNow, 2m);
Assert.IsFalse(sma.IsReady);
identity.Update(DateTime.UtcNow, 3m);
Assert.IsTrue(sma.IsReady);
Assert.AreEqual(2m, sma.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void MultiChainSMA()
{
var identity = new Identity("identity");
var delay = new Delay(2);
// create the SMA of the delay of the identity
var sma = delay.Of(identity).SMA(2);
identity.Update(DateTime.UtcNow, 1m);
Assert.IsTrue(identity.IsReady);
Assert.IsFalse(delay.IsReady);
Assert.IsFalse(sma.IsReady);
identity.Update(DateTime.UtcNow, 2m);
Assert.IsTrue(identity.IsReady);
Assert.IsFalse(delay.IsReady);
Assert.IsFalse(sma.IsReady);
identity.Update(DateTime.UtcNow, 3m);
Assert.IsTrue(identity.IsReady);
Assert.IsTrue(delay.IsReady);
Assert.IsFalse(sma.IsReady);
identity.Update(DateTime.UtcNow, 4m);
Assert.IsTrue(identity.IsReady);
Assert.IsTrue(delay.IsReady);
Assert.IsTrue(sma.IsReady);
Assert.AreEqual(1.5m, sma.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void MultiChainEMA()
{
var identity = new Identity("identity");
var delay = new Delay(2);
// create the EMA of chained methods
var ema = delay.Of(identity).EMA(2, 1);
// Assert.IsTrue(ema. == 1);
identity.Update(DateTime.UtcNow, 1m);
Assert.IsTrue(identity.IsReady);
Assert.IsFalse(delay.IsReady);
Assert.IsFalse(ema.IsReady);
identity.Update(DateTime.UtcNow, 2m);
Assert.IsTrue(identity.IsReady);
Assert.IsFalse(delay.IsReady);
Assert.IsFalse(ema.IsReady);
identity.Update(DateTime.UtcNow, 3m);
Assert.IsTrue(identity.IsReady);
Assert.IsTrue(delay.IsReady);
Assert.IsFalse(ema.IsReady);
identity.Update(DateTime.UtcNow, 4m);
Assert.IsTrue(identity.IsReady);
Assert.IsTrue(delay.IsReady);
Assert.IsTrue(ema.IsReady);
}
[Test, Parallelizable(ParallelScope.Self)]
public void MultiChainMAX()
{
var identity = new Identity("identity");
var delay = new Delay(2);
// create the MAX of the delay of the identity
var max = delay.Of(identity).MAX(2);
identity.Update(DateTime.UtcNow, 1m);
Assert.IsTrue(identity.IsReady);
Assert.IsFalse(delay.IsReady);
Assert.IsFalse(max.IsReady);
identity.Update(DateTime.UtcNow, 2m);
Assert.IsTrue(identity.IsReady);
Assert.IsFalse(delay.IsReady);
Assert.IsFalse(max.IsReady);
identity.Update(DateTime.UtcNow, 3m);
Assert.IsTrue(identity.IsReady);
Assert.IsTrue(delay.IsReady);
Assert.IsFalse(max.IsReady);
identity.Update(DateTime.UtcNow, 4m);
Assert.IsTrue(identity.IsReady);
Assert.IsTrue(delay.IsReady);
Assert.IsTrue(max.IsReady);
}
[Test, Parallelizable(ParallelScope.Self)]
public void MultiChainMIN()
{
var identity = new Identity("identity");
var delay = new Delay(2);
// create the MIN of the delay of the identity
var min = delay.Of(identity).MIN(2);
identity.Update(DateTime.UtcNow, 1m);
Assert.IsTrue(identity.IsReady);
Assert.IsFalse(delay.IsReady);
Assert.IsFalse(min.IsReady);
identity.Update(DateTime.UtcNow, 2m);
Assert.IsTrue(identity.IsReady);
Assert.IsFalse(delay.IsReady);
Assert.IsFalse(min.IsReady);
identity.Update(DateTime.UtcNow, 3m);
Assert.IsTrue(identity.IsReady);
Assert.IsTrue(delay.IsReady);
Assert.IsFalse(min.IsReady);
identity.Update(DateTime.UtcNow, 4m);
Assert.IsTrue(identity.IsReady);
Assert.IsTrue(delay.IsReady);
Assert.IsTrue(min.IsReady);
}
[Test, Parallelizable(ParallelScope.Self)]
public void PlusAddsLeftAndRightAfterBothUpdated()
{
var left = new Identity("left");
var right = new Identity("right");
var composite = left.Plus(right);
left.Update(DateTime.Today, 1m);
right.Update(DateTime.Today, 1m);
Assert.AreEqual(2m, composite.Current.Value);
left.Update(DateTime.Today, 2m);
Assert.AreEqual(2m, composite.Current.Value);
left.Update(DateTime.Today, 3m);
Assert.AreEqual(2m, composite.Current.Value);
right.Update(DateTime.Today, 4m);
Assert.AreEqual(7m, composite.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void PlusAddsLeftAndConstant()
{
var left = new Identity("left");
var composite = left.Plus(5);
left.Update(DateTime.Today, 1m);
Assert.AreEqual(6m, composite.Current.Value);
left.Update(DateTime.Today, 2m);
Assert.AreEqual(7m, composite.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void MinusSubtractsLeftAndRightAfterBothUpdated()
{
var left = new Identity("left");
var right = new Identity("right");
var composite = left.Minus(right);
left.Update(DateTime.Today, 1m);
right.Update(DateTime.Today, 1m);
Assert.AreEqual(0m, composite.Current.Value);
left.Update(DateTime.Today, 2m);
Assert.AreEqual(0m, composite.Current.Value);
left.Update(DateTime.Today, 3m);
Assert.AreEqual(0m, composite.Current.Value);
right.Update(DateTime.Today, 4m);
Assert.AreEqual(-1m, composite.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void MinusSubtractsLeftAndConstant()
{
var left = new Identity("left");
var composite = left.Minus(1);
left.Update(DateTime.Today, 1m);
Assert.AreEqual(0m, composite.Current.Value);
left.Update(DateTime.Today, 2m);
Assert.AreEqual(1m, composite.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void OverDividesLeftAndRightAfterBothUpdated()
{
var left = new Identity("left");
var right = new Identity("right");
var composite = left.Over(right);
left.Update(DateTime.Today, 1m);
right.Update(DateTime.Today, 1m);
Assert.AreEqual(1m, composite.Current.Value);
left.Update(DateTime.Today, 2m);
Assert.AreEqual(1m, composite.Current.Value);
left.Update(DateTime.Today, 3m);
Assert.AreEqual(1m, composite.Current.Value);
right.Update(DateTime.Today, 4m);
Assert.AreEqual(3m / 4m, composite.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void OverDividesLeftAndConstant()
{
var left = new Identity("left");
var composite = left.Over(2);
left.Update(DateTime.Today, 2m);
Assert.AreEqual(1m, composite.Current.Value);
left.Update(DateTime.Today, 4m);
Assert.AreEqual(2m, composite.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void OverHandlesDivideByZero()
{
var left = new Identity("left");
var right = new Identity("right");
var composite = left.Over(right);
var updatedEventFired = false;
composite.Updated += delegate { updatedEventFired = true; };
left.Update(DateTime.Today, 1m);
Assert.IsFalse(updatedEventFired);
right.Update(DateTime.Today, 0m);
Assert.IsFalse(updatedEventFired);
// submitting another update to right won't cause an update without corresponding update to left
right.Update(DateTime.Today, 1m);
Assert.IsFalse(updatedEventFired);
left.Update(DateTime.Today, 1m);
Assert.IsTrue(updatedEventFired);
}
[Test, Parallelizable(ParallelScope.Self)]
public void TimesMultipliesLeftAndRightAfterBothUpdated()
{
var left = new Identity("left");
var right = new Identity("right");
var composite = left.Times(right);
left.Update(DateTime.Today, 1m);
right.Update(DateTime.Today, 1m);
Assert.AreEqual(1m, composite.Current.Value);
left.Update(DateTime.Today, 2m);
Assert.AreEqual(1m, composite.Current.Value);
left.Update(DateTime.Today, 3m);
Assert.AreEqual(1m, composite.Current.Value);
right.Update(DateTime.Today, 4m);
Assert.AreEqual(12m, composite.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void TimesMultipliesLeftAndConstant()
{
var left = new Identity("left");
var composite = left.Times(10);
left.Update(DateTime.Today, 1m);
Assert.AreEqual(10m, composite.Current.Value);
left.Update(DateTime.Today, 2m);
Assert.AreEqual(20m, composite.Current.Value);
}
[Test, Parallelizable(ParallelScope.Self)]
public void WorksForIndicatorsOfDifferentTypes()
{
var indicatorA1 = new TestIndicatorA("1");
var indicatorA2 = new TestIndicatorA("2");
indicatorA1.Over(indicatorA2);
indicatorA1.Minus(indicatorA2);
indicatorA1.Times(indicatorA2);
indicatorA1.Plus(indicatorA2);
indicatorA1.Of(indicatorA2);
var indicatorB1 = new TestIndicatorB("1");
var indicatorB2 = new TestIndicatorB("2");
indicatorB1.Over(indicatorB2);
indicatorB1.Minus(indicatorB2);
indicatorB1.Times(indicatorB2);
indicatorB1.Plus(indicatorB2);
indicatorB1.Of(indicatorB2);
}
protected static TestCaseData[] IndicatorOfDifferentBaseCases()
{
// Helper for getting all permutations of the indicators listed below
static IEnumerable<IEnumerable<T>>
GetPermutations<T>(IEnumerable<T> list, int length)
{
if (length == 1) return list.Select(t => new T[] { t });
return GetPermutations(list, length - 1)
.SelectMany(t => list.Where(o => !t.Contains(o)),
(t1, t2) => t1.Concat(new T[] { t2 }));
}
// Define our indicators to test on
var testIndicators = new IIndicator[]
{
new TestIndicator<BaseData>("BD"),
new TestIndicator<QuoteBar>("QB"),
new TestIndicator<TradeBar>("TB"),
new TestIndicator<IndicatorDataPoint>("IDP")
};
// Methods defined in CompositeTestRunner
var methods = new string[]
{
"minus", "plus", "over", "times"
};
// Create every combination of indicators
var combinations = GetPermutations(testIndicators, 2);
// Create a case for each method with each combination of indicators
var cases = new List<TestCaseData>();
foreach (var combo in combinations)
{
foreach (var method in methods)
{
var newCase = new TestCaseData(combo, method);
cases.Add(newCase);
}
}
return cases.ToArray();
}
[TestCaseSource(nameof(IndicatorOfDifferentBaseCases))]
public void DifferentBaseIndicators(IEnumerable<IIndicator> indicators, string method)
{
CompositeTestRunner(indicators.ElementAt(0), indicators.ElementAt(1), method);
}
[TestCaseSource(nameof(IndicatorOfDifferentBaseCases))]
public void DifferentBaseIndicatorsPy(IEnumerable<IIndicator> indicators, string method)
{
using (Py.GIL())
{
CompositeTestRunner(indicators.ElementAt(0).ToPython(), indicators.ElementAt(1).ToPython(), method);
}
}
public static void CompositeTestRunner(dynamic left, dynamic right, string method)
{
// Reset before every test; the permutation setup in test cases uses the same instance for each permutation
left.Reset();
right.Reset();
double expected;
CompositeIndicator compositeIndicator;
switch (method)
{
case "minus":
expected = -5; // 5 - 10
compositeIndicator = IndicatorExtensions.Minus(left, right);
break;
case "plus":
expected = 15; // 5 + 10
compositeIndicator = IndicatorExtensions.Plus(left, right);
break;
case "over":
expected = .5; // 5 / 10
compositeIndicator = IndicatorExtensions.Over(left, right);
break;
case "times":
expected = 50; // 5 * 10
compositeIndicator = IndicatorExtensions.Times(left, right);
break;
default:
Assert.Fail($"Method '{method}' not handled by this test, please implement");
throw new ArgumentException($"Cannot proceed with test using method {method}");
}
// Check our values are all zero
Assert.AreEqual(0, (int)right.Current.Value);
Assert.AreEqual(0, (int)left.Current.Value);
Assert.AreEqual(0, compositeIndicator.Current.Value);
// Use our test indicator method to update left and right
left.UpdateValue(5);
right.UpdateValue(10);
// Check final expected values, this ensures that composites are updating correctly
Assert.AreEqual(5, (int)left.Current.Value);
Assert.AreEqual(10, (int)right.Current.Value);
Assert.AreEqual(expected, compositeIndicator.Current.Value);
}
[Test]
public void MinusSubtractsLeftAndConstant_Py()
{
using (Py.GIL())
{
var left = new Identity("left");
var composite = (IIndicator) IndicatorExtensions.Minus(left.ToPython(), 10);
left.Update(DateTime.Today, 1);
Assert.AreEqual(-9, composite.Current.Value);
left.Update(DateTime.Today, 2);
Assert.AreEqual(-8, composite.Current.Value);
}
}
[Test]
public void PlusAddsLeftAndConstant_Py()
{
using (Py.GIL())
{
var left = new Identity("left");
var composite = (IIndicator)IndicatorExtensions.Plus(left.ToPython(), 10);
left.Update(DateTime.Today, 1);
Assert.AreEqual(11, composite.Current.Value);
left.Update(DateTime.Today, 2);
Assert.AreEqual(12, composite.Current.Value);
}
}
[Test]
public void OverDivdesLeftAndConstant_Py()
{
using (Py.GIL())
{
var left = new Identity("left");
var composite = (IIndicator)IndicatorExtensions.Over(left.ToPython(), 5);
left.Update(DateTime.Today, 10);
Assert.AreEqual(2, composite.Current.Value);
left.Update(DateTime.Today, 20);
Assert.AreEqual(4, composite.Current.Value);
}
}
[Test]
public void TimesMultipliesLeftAndConstant_Py()
{
using (Py.GIL())
{
var left = new Identity("left");
var composite = (IIndicator)IndicatorExtensions.Times(left.ToPython(), 5);
left.Update(DateTime.Today, 10);
Assert.AreEqual(50, composite.Current.Value);
left.Update(DateTime.Today, 20);
Assert.AreEqual(100, composite.Current.Value);
}
}
[Test]
public void TimesMultipliesLeftAndRight_Py()
{
using (Py.GIL())
{
var left = new Identity("left");
var right = new Identity("right");
var composite = (IIndicator)IndicatorExtensions.Times(left.ToPython(), right.ToPython());
left.Update(DateTime.Today, 10);
right.Update(DateTime.Today, 10);
Assert.AreEqual(100, composite.Current.Value);
left.Update(DateTime.Today, 20);
Assert.AreEqual(100, composite.Current.Value);
}
}
[Test]
public void OverDividesLeftAndRight_Py()
{
using (Py.GIL())
{
var left = new Identity("left");
var right = new Identity("right");
var composite = (IIndicator)IndicatorExtensions.Over(left.ToPython(), right.ToPython());
left.Update(DateTime.Today, 10);
right.Update(DateTime.Today, 10);
Assert.AreEqual(1, composite.Current.Value);
left.Update(DateTime.Today, 20);
Assert.AreEqual(1, composite.Current.Value);
}
}
[Test]
public void PlusAddsLeftAndRight_Py()
{
using (Py.GIL())
{
var left = new Identity("left");
var right = new Identity("right");
var composite = (IIndicator)IndicatorExtensions.Plus(left.ToPython(), right.ToPython());
left.Update(DateTime.Today, 10);
right.Update(DateTime.Today, 10);
Assert.AreEqual(20, composite.Current.Value);
left.Update(DateTime.Today, 20);
Assert.AreEqual(20, composite.Current.Value);
}
}
[Test]
public void MinusSubstractsLeftAndRight_Py()
{
using (Py.GIL())
{
var left = new Identity("left");
var right = new Identity("right");
var composite = (IIndicator)IndicatorExtensions.Minus(left.ToPython(), right.ToPython());
left.Update(DateTime.Today, 10);
right.Update(DateTime.Today, 10);
Assert.AreEqual(0, composite.Current.Value);
left.Update(DateTime.Today, 20);
Assert.AreEqual(0, composite.Current.Value);
}
}
[Test]
public void RunPythonRegressionAlgorithmWithIndicatorExtensions()
{
var parameter = new RegressionTests.AlgorithmStatisticsTestParameters("IndicatorExtensionsSMAWithCustomIndicatorsRegressionAlgorithm",
new Dictionary<string, string> {
{PerformanceMetrics.TotalOrders, "0"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "0%"},
{"Drawdown", "0%"},
{"Expectancy", "0"},
{"Net Profit", "0%"},
{"Sharpe Ratio", "0"},
{"Probabilistic Sharpe Ratio", "0%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0"},
{"Beta", "0"},
{"Annual Standard Deviation", "0"},
{"Annual Variance", "0"},
{"Information Ratio", "0.717"},
{"Tracking Error", "0.593"},
{"Treynor Ratio", "0"},
{"Total Fees", "$0.00"},
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
},
Language.Python,
AlgorithmStatus.Completed);
AlgorithmRunner.RunLocalBacktest(parameter.Algorithm,
parameter.Statistics,
parameter.Language,
parameter.ExpectedFinalStatus,
initialCash: 100000);
}
private class TestIndicatorA : IndicatorBase<IBaseData>
{
public TestIndicatorA(string name) : base(name)
{
}
public override bool IsReady { get; }
protected override decimal ComputeNextValue(IBaseData input)
{
throw new NotImplementedException();
}
}
private class TestIndicatorB : IndicatorBase<IndicatorDataPoint>
{
public TestIndicatorB(string name) : base(name)
{
}
public override bool IsReady
{
get
{
throw new NotImplementedException();
}
}
protected override decimal ComputeNextValue(IndicatorDataPoint input)
{
throw new NotImplementedException();
}
}
private class TestIndicator<T> : IndicatorBase<T>
where T : IBaseData
{
public TestIndicator(string name)
: base(name)
{
}
public override bool IsReady
{
get
{
return true;
}
}
public void UpdateValue(int value)
{
Current = new IndicatorDataPoint(DateTime.MinValue, value);
OnUpdated(Current);
}
protected override decimal ComputeNextValue(T input)
{
return input.Value;
}
}
}
}