diff --git a/Indicators/CompositeIndicator.cs b/Indicators/CompositeIndicator.cs index 1938371898c7..1040755a4660 100644 --- a/Indicators/CompositeIndicator.cs +++ b/Indicators/CompositeIndicator.cs @@ -14,6 +14,9 @@ */ using System; +using Python.Runtime; +using QuantConnect.Data; +using QuantConnect.Data.Market; namespace QuantConnect.Indicators { @@ -64,7 +67,8 @@ public override bool IsReady /// /// Resets this indicator to its initial state /// - public override void Reset() { + public override void Reset() + { Left.Reset(); Right.Reset(); base.Reset(); @@ -84,6 +88,7 @@ public CompositeIndicator(string name, IndicatorBase left, IndicatorBase right, _composer = composer; Left = left; Right = right; + Name ??= $"COMPOSE({Left.Name},{Right.Name})"; ConfigureEventHandlers(); } @@ -98,6 +103,38 @@ public CompositeIndicator(IndicatorBase left, IndicatorBase right, IndicatorComp : this($"COMPOSE({left.Name},{right.Name})", left, right, composer) { } + /// + /// Initializes a new instance of using two indicators + /// and a custom function. + /// + /// The name of the composite indicator. + /// The first indicator in the composition. + /// The second indicator in the composition. + /// A Python function that processes the indicator values. + /// + /// Thrown if the provided left or right indicator is not a valid QuantConnect Indicator object. + /// + public CompositeIndicator(string name, PyObject left, PyObject right, PyObject handler) + : this( + name, + (IndicatorBase)left.GetIndicatorAsManagedObject(), + (IndicatorBase)right.GetIndicatorAsManagedObject(), + new IndicatorComposer(handler.ConvertToDelegate>()) + ) + { + } + + /// + /// Initializes a new instance of using two indicators + /// and a custom function. + /// + /// The first indicator in the composition. + /// The second indicator in the composition. + /// A Python function that processes the indicator values. + public CompositeIndicator(PyObject left, PyObject right, PyObject handler) + : this(null, left, right, handler) + { } + /// /// Computes the next value of this indicator from the given state /// and returns an instance of the class @@ -130,8 +167,8 @@ protected override decimal ComputeNextValue(IndicatorDataPoint _) private void ConfigureEventHandlers() { // if either of these are constants then there's no reason - bool leftIsConstant = Left.GetType().IsSubclassOfGeneric(typeof (ConstantIndicator<>)); - bool rightIsConstant = Right.GetType().IsSubclassOfGeneric(typeof (ConstantIndicator<>)); + bool leftIsConstant = Left.GetType().IsSubclassOfGeneric(typeof(ConstantIndicator<>)); + bool rightIsConstant = Right.GetType().IsSubclassOfGeneric(typeof(ConstantIndicator<>)); // wire up the Updated events such that when we get a new piece of data from both left and right // we'll call update on this indicator. It's important to note that the CompositeIndicator only uses diff --git a/Indicators/IndicatorExtensions.cs b/Indicators/IndicatorExtensions.cs index 2d21c2f35ac7..523baa4cdf68 100644 --- a/Indicators/IndicatorExtensions.cs +++ b/Indicators/IndicatorExtensions.cs @@ -19,6 +19,7 @@ using QuantConnect.Data; using Python.Runtime; using QuantConnect.Util; +using QuantConnect.Data.Market; namespace QuantConnect.Indicators { @@ -98,7 +99,8 @@ public static CompositeIndicator WeightedBy(this IndicatorBase va denominator.Update(consolidated); }; - var resetCompositeIndicator = new ResetCompositeIndicator(numerator, denominator, GetOverIndicatorComposer(), () => { + var resetCompositeIndicator = new ResetCompositeIndicator(numerator, denominator, GetOverIndicatorComposer(), () => + { x.Reset(); y.Reset(); }); @@ -132,7 +134,7 @@ public static CompositeIndicator Plus(this IndicatorBase left, decimal constant) /// The sum of the left and right indicators public static CompositeIndicator Plus(this IndicatorBase left, IndicatorBase right) { - return new (left, right, (l, r) => l.Current.Value + r.Current.Value); + return new(left, right, (l, r) => l.Current.Value + r.Current.Value); } /// @@ -147,7 +149,7 @@ public static CompositeIndicator Plus(this IndicatorBase left, IndicatorBase rig /// The sum of the left and right indicators public static CompositeIndicator Plus(this IndicatorBase left, IndicatorBase right, string name) { - return new (name, left, right, (l, r) => l.Current.Value + r.Current.Value); + return new(name, left, right, (l, r) => l.Current.Value + r.Current.Value); } /// @@ -176,7 +178,7 @@ public static CompositeIndicator Minus(this IndicatorBase left, decimal constant /// The difference of the left and right indicators public static CompositeIndicator Minus(this IndicatorBase left, IndicatorBase right) { - return new (left, right, (l, r) => l.Current.Value - r.Current.Value); + return new(left, right, (l, r) => l.Current.Value - r.Current.Value); } /// @@ -191,7 +193,7 @@ public static CompositeIndicator Minus(this IndicatorBase left, IndicatorBase ri /// The difference of the left and right indicators public static CompositeIndicator Minus(this IndicatorBase left, IndicatorBase right, string name) { - return new (name, left, right, (l, r) => l.Current.Value - r.Current.Value); + return new(name, left, right, (l, r) => l.Current.Value - r.Current.Value); } /// @@ -220,7 +222,7 @@ public static CompositeIndicator Over(this IndicatorBase left, decimal constant) /// The ratio of the left to the right indicator public static CompositeIndicator Over(this IndicatorBase left, IndicatorBase right) { - return new (left, right, GetOverIndicatorComposer()); + return new(left, right, GetOverIndicatorComposer()); } /// @@ -235,7 +237,7 @@ public static CompositeIndicator Over(this IndicatorBase left, IndicatorBase rig /// The ratio of the left to the right indicator public static CompositeIndicator Over(this IndicatorBase left, IndicatorBase right, string name) { - return new (name, left, right, GetOverIndicatorComposer()); + return new(name, left, right, GetOverIndicatorComposer()); } /// @@ -264,7 +266,7 @@ public static CompositeIndicator Times(this IndicatorBase left, decimal constant /// The product of the left to the right indicators public static CompositeIndicator Times(this IndicatorBase left, IndicatorBase right) { - return new (left, right, (l, r) => l.Current.Value * r.Current.Value); + return new(left, right, (l, r) => l.Current.Value * r.Current.Value); } /// @@ -279,7 +281,7 @@ public static CompositeIndicator Times(this IndicatorBase left, IndicatorBase ri /// The product of the left to the right indicators public static CompositeIndicator Times(this IndicatorBase left, IndicatorBase right, string name) { - return new (name, left, right, (l, r) => l.Current.Value * r.Current.Value); + return new(name, left, right, (l, r) => l.Current.Value * r.Current.Value); } /// Creates a new ExponentialMovingAverage indicator with the specified period and smoothingFactor from the left indicator @@ -418,7 +420,7 @@ public static SimpleMovingAverage SMA(PyObject left, int period, bool waitForFir return SMA(indicator, period, waitForFirstToReady); } - /// + /// /// Creates a new CompositeIndicator such that the result will be the ratio of the left to the constant /// /// @@ -562,7 +564,7 @@ public static object Plus(PyObject left, PyObject right, string name = "") return Plus(indicatorLeft, indicatorRight, name); } - private static dynamic GetIndicatorAsManagedObject(PyObject indicator) + internal static dynamic GetIndicatorAsManagedObject(this PyObject indicator) { if (indicator.TryConvert(out PythonIndicator pythonIndicator, true)) { diff --git a/Tests/Indicators/CompositeIndicatorTests.cs b/Tests/Indicators/CompositeIndicatorTests.cs index 742b755d036d..f1a1293d900b 100644 --- a/Tests/Indicators/CompositeIndicatorTests.cs +++ b/Tests/Indicators/CompositeIndicatorTests.cs @@ -15,6 +15,7 @@ using System; using NUnit.Framework; +using Python.Runtime; using QuantConnect.Indicators; namespace QuantConnect.Tests.Indicators @@ -72,13 +73,14 @@ public void CallsDelegateCorrectly() } [Test] - public virtual void ResetsProperly() { + public virtual void ResetsProperly() + { var left = new Maximum("left", 2); var right = new Minimum("right", 2); var composite = CreateCompositeIndicator(left, right, (l, r) => l.Current.Value + r.Current.Value); left.Update(DateTime.Today, 1m); - right.Update(DateTime.Today,-1m); + right.Update(DateTime.Today, -1m); left.Update(DateTime.Today.AddDays(1), -1m); right.Update(DateTime.Today.AddDays(1), 1m); @@ -94,6 +96,92 @@ public virtual void ResetsProperly() { Assert.AreEqual(right.PeriodsSinceMinimum, 0); } + [TestCase("sum", 5, 10, 15, false)] + [TestCase("min", -12, 52, -12, false)] + [TestCase("sum", 5, 10, 15, true)] + [TestCase("min", -12, 52, -12, true)] + public virtual void PythonCompositeIndicatorConstructorValidatesBehavior(string operation, decimal leftValue, decimal rightValue, decimal expectedValue, bool usePythonIndicator) + { + var left = new SimpleMovingAverage("SMA", 10); + var right = new SimpleMovingAverage("SMA", 10); + using (Py.GIL()) + { + var testModule = PyModule.FromString("testModule", + @" +from AlgorithmImports import * +from QuantConnect.Indicators import * + +def create_composite_indicator(left, right, operation): + if operation == 'sum': + def composer(l, r): + return IndicatorResult(l.current.value + r.current.value) + elif operation == 'min': + def composer(l, r): + return IndicatorResult(min(l.current.value, r.current.value)) + return CompositeIndicator(left, right, composer) + +def update_indicators(left, right, value_left, value_right): + left.update(IndicatorDataPoint(DateTime.Now, value_left)) + right.update(IndicatorDataPoint(DateTime.Now, value_right)) + "); + + using var createCompositeIndicator = testModule.GetAttr("create_composite_indicator"); + using var updateIndicators = testModule.GetAttr("update_indicators"); + + using var leftPy = usePythonIndicator ? CreatePyObjectIndicator(10) : left.ToPython(); + using var rightPy = usePythonIndicator ? CreatePyObjectIndicator(10) : right.ToPython(); + + // Create composite indicator using Python logic + using var composite = createCompositeIndicator.Invoke(leftPy, rightPy, operation.ToPython()); + + // Update the indicator with sample values (left, right) + updateIndicators.Invoke(leftPy, rightPy, leftValue.ToPython(), rightValue.ToPython()); + + // Verify composite indicator name and properties + using var name = composite.GetAttr("Name"); + Assert.AreEqual($"COMPOSE({left.Name},{right.Name})", name.ToString()); + + // Validate the composite indicator's computed value + using var value = composite.GetAttr("Current").GetAttr("Value"); + Assert.AreEqual(expectedValue, value.As()); + } + } + + private static PyObject CreatePyObjectIndicator(int period) + { + using (Py.GIL()) + { + var module = PyModule.FromString( + "custom_indicator", + @" +from AlgorithmImports import * +from collections import deque + +class CustomSimpleMovingAverage(PythonIndicator): + def __init__(self, period): + self.name = 'SMA' + self.value = 0 + self.period = period + self.warm_up_period = period + self.queue = deque(maxlen=period) + self.current = IndicatorDataPoint(DateTime.Now, self.value) + + def update(self, input): + self.queue.appendleft(input.value) + count = len(self.queue) + self.value = sum(self.queue) / count + self.current = IndicatorDataPoint(input.time, self.value) + self.on_updated(IndicatorDataPoint(DateTime.Now, input.value)) +" + ); + + var indicator = module.GetAttr("CustomSimpleMovingAverage") + .Invoke(period.ToPython()); + + return indicator; + } + } + protected virtual CompositeIndicator CreateCompositeIndicator(IndicatorBase left, IndicatorBase right, QuantConnect.Indicators.CompositeIndicator.IndicatorComposer composer) { return new CompositeIndicator(left, right, composer);