From cf26cab7eeb90fd262007488d3d918bd18fcd64f Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Thu, 20 Feb 2025 15:23:49 -0500 Subject: [PATCH] Add unit tests and update CompositeIndicator --- Indicators/CompositeIndicator.cs | 34 ++++++------- Tests/Indicators/CompositeIndicatorTests.cs | 55 ++++++++++++++++++++- 2 files changed, 70 insertions(+), 19 deletions(-) diff --git a/Indicators/CompositeIndicator.cs b/Indicators/CompositeIndicator.cs index d1644c7705fd..eca64cccc001 100644 --- a/Indicators/CompositeIndicator.cs +++ b/Indicators/CompositeIndicator.cs @@ -15,8 +15,8 @@ using System; using Python.Runtime; +using QuantConnect.Data; using QuantConnect.Data.Market; -using QuantConnect.Python; namespace QuantConnect.Indicators { @@ -151,14 +151,14 @@ public CompositeIndicator(PyObject left, PyObject right, PyObject handler) /// An IndicatorComposer that applies the Python function. private static IndicatorComposer CreateComposerFromPyObject(PyObject handler) { - return (left, right) => + // If the conversion fails, throw an exception + if (!handler.TryConvertToDelegate(out Func composer)) { - using (Py.GIL()) - { - dynamic result = handler.Invoke(left.Current.Value, right.Current.Value); - return new IndicatorResult(result); - } - }; + throw new InvalidOperationException("Failed to convert the handler into a valid delegate."); + } + + // Return the converted delegate, since it matches the signature of IndicatorComposer + return new IndicatorComposer(composer); } /// @@ -170,24 +170,24 @@ private static IndicatorComposer CreateComposerFromPyObject(PyObject handler) /// True if the conversion is successful; otherwise, false. private static bool TryConvertIndicator(PyObject pyObject, out IndicatorBase indicator) { - if (pyObject.TryConvert(out IndicatorBase idp)) + indicator = null; + if (pyObject.TryConvert(out IndicatorBase ibd)) + { + indicator = ibd; + } + else if (pyObject.TryConvert(out IndicatorBase idp)) { indicator = idp; - return true; } - if (pyObject.TryConvert(out IndicatorBase idb)) + else if (pyObject.TryConvert(out IndicatorBase idb)) { indicator = idb; - return true; } - if (pyObject.TryConvert(out IndicatorBase itb)) + else if (pyObject.TryConvert(out IndicatorBase itb)) { indicator = itb; - return true; } - - indicator = null; - return false; + return indicator != null; } /// diff --git a/Tests/Indicators/CompositeIndicatorTests.cs b/Tests/Indicators/CompositeIndicatorTests.cs index 742b755d036d..c3913fb6a55f 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,55 @@ public virtual void ResetsProperly() { Assert.AreEqual(right.PeriodsSinceMinimum, 0); } + [TestCase("sum", 5, 10, 15)] + [TestCase("min", -12, 52, -12)] + public virtual void PythonCompositeIndicatorConstructorValidatesBehavior(string operation, decimal leftValue, decimal rightValue, decimal expectedValue) + { + 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)) + "); + + var createCompositeIndicator = testModule.GetAttr("create_composite_indicator"); + var updateIndicators = testModule.GetAttr("update_indicators"); + + var leftPy = left.ToPython(); + var rightPy = right.ToPython(); + + // Create composite indicator using Python logic + 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 + Assert.AreEqual($"COMPOSE({left.Name},{right.Name})", composite.GetAttr("Name").ToString()); + Assert.AreEqual(left, composite.GetAttr("Left").As()); + Assert.AreEqual(right, composite.GetAttr("Right").As()); + + // Validate the composite indicator computed value + Assert.AreEqual(expectedValue, composite.GetAttr("Current").GetAttr("Value").As()); + } + } + protected virtual CompositeIndicator CreateCompositeIndicator(IndicatorBase left, IndicatorBase right, QuantConnect.Indicators.CompositeIndicator.IndicatorComposer composer) { return new CompositeIndicator(left, right, composer);