From 4dfb6ebb2c2be5fd415906abcbe3db8206872f0e Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Wed, 19 Feb 2025 16:21:52 -0500 Subject: [PATCH 1/9] Initial solution --- Algorithm/QCAlgorithm.Python.cs | 63 +++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index 19e453f729fb..e1e72d668bac 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -299,7 +299,7 @@ public Universe AddUniverse(PyObject pyObject) return AddUniverse(pyObject, null, null); } // TODO: to be removed when https://github.com/QuantConnect/pythonnet/issues/62 is solved - else if(pyObject.TryConvert(out universe)) + else if (pyObject.TryConvert(out universe)) { return AddUniverse(universe); } @@ -569,6 +569,65 @@ public void AddUniverseOptions(PyObject universe, PyObject optionFilter) } } + /// + /// Creates a new using two indicators and a custom Python function as a handler. + /// + /// The name of the composite indicator. + /// The first indicator used in the composition. + /// The second indicator used in the composition. + /// A Python function that takes two indicator values and returns the computed result. + /// A new instance of . + /// + /// Thrown when the provided left or right indicator is not a valid QuantConnect Indicator object. + /// + [DocumentationAttribute(Universes)] + public CompositeIndicator CompositeIndicator(string name, PyObject left, PyObject right, PyObject handler) + { + var leftIndicator = GetIndicator(left); + var rightIndicator = GetIndicator(right); + if (leftIndicator == null) + { + throw new ArgumentException($"The left argument should be a QuantConnect Indicator object, {left} was provided."); + } + if (rightIndicator == null) + { + throw new ArgumentException($"The right argument should be a QuantConnect Indicator object, {right} was provided."); + } + CompositeIndicator.IndicatorComposer composer = (left, right) => + { + using (Py.GIL()) + { + dynamic result = handler.Invoke(left.Current.Value, right.Current.Value); + return new IndicatorResult(result); + } + }; + return new CompositeIndicator(name, leftIndicator, rightIndicator, composer); + } + + /// + /// Attempts to convert a Python object into a valid QuantConnect indicator. + /// + /// The Python object to convert. + /// + /// A valid instance if conversion is successful; otherwise, null. + /// + public IndicatorBase GetIndicator(PyObject pyObject) + { + if (pyObject.TryConvert(out IndicatorBase indicatorDataPoint)) + { + return indicatorDataPoint; + } + if (pyObject.TryConvert(out IndicatorBase indicatorDataBar)) + { + return indicatorDataBar; + } + if (pyObject.TryConvert(out IndicatorBase indicatorTradeBar)) + { + return indicatorTradeBar; + } + return null; + } + /// /// Registers the consolidator to receive automatic updates as well as configures the indicator to receive updates /// from the consolidator. @@ -1768,7 +1827,7 @@ private dynamic[] GetIndicatorArray(PyObject first, PyObject second = null, PyOb { using (Py.GIL()) { - var array = new[] {first, second, third, fourth} + var array = new[] { first, second, third, fourth } .Select( x => { From 3686c8b7fad86a3ad86a686f2354e65e1b1aa1f1 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Thu, 20 Feb 2025 02:32:59 -0500 Subject: [PATCH 2/9] Add python support for CompositeIndicator --- Algorithm/QCAlgorithm.Python.cs | 63 +------------------- Indicators/CompositeIndicator.cs | 98 +++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 64 deletions(-) diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index e1e72d668bac..19e453f729fb 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -299,7 +299,7 @@ public Universe AddUniverse(PyObject pyObject) return AddUniverse(pyObject, null, null); } // TODO: to be removed when https://github.com/QuantConnect/pythonnet/issues/62 is solved - else if (pyObject.TryConvert(out universe)) + else if(pyObject.TryConvert(out universe)) { return AddUniverse(universe); } @@ -569,65 +569,6 @@ public void AddUniverseOptions(PyObject universe, PyObject optionFilter) } } - /// - /// Creates a new using two indicators and a custom Python function as a handler. - /// - /// The name of the composite indicator. - /// The first indicator used in the composition. - /// The second indicator used in the composition. - /// A Python function that takes two indicator values and returns the computed result. - /// A new instance of . - /// - /// Thrown when the provided left or right indicator is not a valid QuantConnect Indicator object. - /// - [DocumentationAttribute(Universes)] - public CompositeIndicator CompositeIndicator(string name, PyObject left, PyObject right, PyObject handler) - { - var leftIndicator = GetIndicator(left); - var rightIndicator = GetIndicator(right); - if (leftIndicator == null) - { - throw new ArgumentException($"The left argument should be a QuantConnect Indicator object, {left} was provided."); - } - if (rightIndicator == null) - { - throw new ArgumentException($"The right argument should be a QuantConnect Indicator object, {right} was provided."); - } - CompositeIndicator.IndicatorComposer composer = (left, right) => - { - using (Py.GIL()) - { - dynamic result = handler.Invoke(left.Current.Value, right.Current.Value); - return new IndicatorResult(result); - } - }; - return new CompositeIndicator(name, leftIndicator, rightIndicator, composer); - } - - /// - /// Attempts to convert a Python object into a valid QuantConnect indicator. - /// - /// The Python object to convert. - /// - /// A valid instance if conversion is successful; otherwise, null. - /// - public IndicatorBase GetIndicator(PyObject pyObject) - { - if (pyObject.TryConvert(out IndicatorBase indicatorDataPoint)) - { - return indicatorDataPoint; - } - if (pyObject.TryConvert(out IndicatorBase indicatorDataBar)) - { - return indicatorDataBar; - } - if (pyObject.TryConvert(out IndicatorBase indicatorTradeBar)) - { - return indicatorTradeBar; - } - return null; - } - /// /// Registers the consolidator to receive automatic updates as well as configures the indicator to receive updates /// from the consolidator. @@ -1827,7 +1768,7 @@ private dynamic[] GetIndicatorArray(PyObject first, PyObject second = null, PyOb { using (Py.GIL()) { - var array = new[] { first, second, third, fourth } + var array = new[] {first, second, third, fourth} .Select( x => { diff --git a/Indicators/CompositeIndicator.cs b/Indicators/CompositeIndicator.cs index 1938371898c7..d1644c7705fd 100644 --- a/Indicators/CompositeIndicator.cs +++ b/Indicators/CompositeIndicator.cs @@ -14,6 +14,9 @@ */ using System; +using Python.Runtime; +using QuantConnect.Data.Market; +using QuantConnect.Python; 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(); @@ -98,6 +102,94 @@ 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) + : base(name) + { + if (!TryConvertIndicator(left, out var leftIndicator)) + { + throw new ArgumentException($"The left argument should be a QuantConnect Indicator object, {left} was provided."); + } + if (!TryConvertIndicator(right, out var rightIndicator)) + { + throw new ArgumentException($"The right argument should be a QuantConnect Indicator object, {right} was provided."); + } + + // if no name was provided, auto-generate one + Name ??= $"COMPOSE({leftIndicator.Name},{rightIndicator.Name})"; + Left = leftIndicator; + Right = rightIndicator; + _composer = CreateComposerFromPyObject(handler); + ConfigureEventHandlers(); + } + + /// + /// 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) + { } + + /// + /// Creates an IndicatorComposer from a Python function. + /// + /// A PyObject representing the Python function. + /// An IndicatorComposer that applies the Python function. + private static IndicatorComposer CreateComposerFromPyObject(PyObject handler) + { + return (left, right) => + { + using (Py.GIL()) + { + dynamic result = handler.Invoke(left.Current.Value, right.Current.Value); + return new IndicatorResult(result); + } + }; + } + + /// + /// Attempts to convert a into an . + /// Supports indicators based on , , and . + /// + /// The Python object to convert. + /// The converted indicator if successful; otherwise, null. + /// True if the conversion is successful; otherwise, false. + private static bool TryConvertIndicator(PyObject pyObject, out IndicatorBase indicator) + { + if (pyObject.TryConvert(out IndicatorBase idp)) + { + indicator = idp; + return true; + } + if (pyObject.TryConvert(out IndicatorBase idb)) + { + indicator = idb; + return true; + } + if (pyObject.TryConvert(out IndicatorBase itb)) + { + indicator = itb; + return true; + } + + indicator = null; + return false; + } + /// /// Computes the next value of this indicator from the given state /// and returns an instance of the class @@ -130,8 +222,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 From cf26cab7eeb90fd262007488d3d918bd18fcd64f Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Thu, 20 Feb 2025 15:23:49 -0500 Subject: [PATCH 3/9] 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); From 4376f4defaa0db7a7df0d01c3fc929def7b22938 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 21 Feb 2025 03:24:23 -0500 Subject: [PATCH 4/9] Add new test case --- Indicators/CompositeIndicator.cs | 33 +-------- Indicators/IndicatorExtensions.cs | 38 +++++++--- Tests/Indicators/CompositeIndicatorTests.cs | 78 +++++++++++++++++---- 3 files changed, 94 insertions(+), 55 deletions(-) diff --git a/Indicators/CompositeIndicator.cs b/Indicators/CompositeIndicator.cs index eca64cccc001..20420797b1ff 100644 --- a/Indicators/CompositeIndicator.cs +++ b/Indicators/CompositeIndicator.cs @@ -116,11 +116,11 @@ public CompositeIndicator(IndicatorBase left, IndicatorBase right, IndicatorComp public CompositeIndicator(string name, PyObject left, PyObject right, PyObject handler) : base(name) { - if (!TryConvertIndicator(left, out var leftIndicator)) + if (!left.TryConvertToIndicator(out var leftIndicator)) { throw new ArgumentException($"The left argument should be a QuantConnect Indicator object, {left} was provided."); } - if (!TryConvertIndicator(right, out var rightIndicator)) + if (!right.TryConvertToIndicator(out var rightIndicator)) { throw new ArgumentException($"The right argument should be a QuantConnect Indicator object, {right} was provided."); } @@ -161,35 +161,6 @@ private static IndicatorComposer CreateComposerFromPyObject(PyObject handler) return new IndicatorComposer(composer); } - /// - /// Attempts to convert a into an . - /// Supports indicators based on , , and . - /// - /// The Python object to convert. - /// The converted indicator if successful; otherwise, null. - /// True if the conversion is successful; otherwise, false. - private static bool TryConvertIndicator(PyObject pyObject, out IndicatorBase indicator) - { - indicator = null; - if (pyObject.TryConvert(out IndicatorBase ibd)) - { - indicator = ibd; - } - else if (pyObject.TryConvert(out IndicatorBase idp)) - { - indicator = idp; - } - else if (pyObject.TryConvert(out IndicatorBase idb)) - { - indicator = idb; - } - else if (pyObject.TryConvert(out IndicatorBase itb)) - { - indicator = itb; - } - return indicator != null; - } - /// /// Computes the next value of this indicator from the given state /// and returns an instance of the class diff --git a/Indicators/IndicatorExtensions.cs b/Indicators/IndicatorExtensions.cs index 2d21c2f35ac7..d99572571938 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,23 @@ 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); + } + + /// + /// Attempts to convert a into an . + /// + /// The Python object to convert. + /// The resulting indicator if successful; otherwise, null. + /// true if the conversion succeeds; otherwise, false. + public static bool TryConvertToIndicator(this PyObject pyObject, out IndicatorBase indicator) + { + indicator = null; + + return pyObject.TryConvert(out IndicatorBase idp) && (indicator = idp) != null || + pyObject.TryConvert(out IndicatorBase idb) && (indicator = idb) != null || + pyObject.TryConvert(out IndicatorBase itb) && (indicator = itb) != null || + pyObject.TryConvert(out IndicatorBase ibd) && (indicator = ibd) != null; } /// Creates a new ExponentialMovingAverage indicator with the specified period and smoothingFactor from the left indicator @@ -418,7 +436,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 /// /// diff --git a/Tests/Indicators/CompositeIndicatorTests.cs b/Tests/Indicators/CompositeIndicatorTests.cs index c3913fb6a55f..f774ff9c15d0 100644 --- a/Tests/Indicators/CompositeIndicatorTests.cs +++ b/Tests/Indicators/CompositeIndicatorTests.cs @@ -96,12 +96,24 @@ 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) + [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); + IndicatorBase left; + IndicatorBase right; + if (usePythonIndicator) + { + left = new PythonIndicator(CreatePyObjectIndicator(10)); + right = new PythonIndicator(CreatePyObjectIndicator(10)); + } + else + { + left = new SimpleMovingAverage("SMA", 10); + right = new SimpleMovingAverage("SMA", 10); + } using (Py.GIL()) { var testModule = PyModule.FromString("testModule", @@ -123,25 +135,63 @@ def update_indicators(left, right, value_left, value_right): right.Update(IndicatorDataPoint(DateTime.Now, value_right)) "); - var createCompositeIndicator = testModule.GetAttr("create_composite_indicator"); - var updateIndicators = testModule.GetAttr("update_indicators"); + using var createCompositeIndicator = testModule.GetAttr("create_composite_indicator"); + using var updateIndicators = testModule.GetAttr("update_indicators"); - var leftPy = left.ToPython(); - var rightPy = right.ToPython(); + using var leftPy = left.ToPython(); + using var rightPy = right.ToPython(); // Create composite indicator using Python logic - var composite = createCompositeIndicator.Invoke(leftPy, rightPy, operation.ToPython()); + 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 - 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()); + using var name = composite.GetAttr("Name"); + using var typeLeft = composite.GetAttr("Left"); + using var typeRight = composite.GetAttr("Right"); + Assert.AreEqual($"COMPOSE({left.Name},{right.Name})", name.ToString()); + Assert.AreEqual(left, typeLeft.As()); + Assert.AreEqual(right, typeRight.As()); // Validate the composite indicator computed value - Assert.AreEqual(expectedValue, composite.GetAttr("Current").GetAttr("Value").As()); + 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 = 'CustomSMA' + self.Value = 0 + self.Period = period + self.WarmUpPeriod = period + self.queue = deque(maxlen=period) + + def Update(self, input): + self.queue.appendleft(input.Value) + count = len(self.queue) + self.Value = sum(self.queue) / count + return count == self.queue.maxlen +" + ); + + var indicator = module.GetAttr("CustomSimpleMovingAverage") + .Invoke(period.ToPython()); + + return indicator; } } From 7d6195b937c4d3c671b841a043af4086ce19358a Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 21 Feb 2025 03:39:34 -0500 Subject: [PATCH 5/9] Update TryConvertToIndicator logic --- Indicators/IndicatorExtensions.cs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/Indicators/IndicatorExtensions.cs b/Indicators/IndicatorExtensions.cs index d99572571938..a84f94df11b1 100644 --- a/Indicators/IndicatorExtensions.cs +++ b/Indicators/IndicatorExtensions.cs @@ -294,10 +294,24 @@ public static bool TryConvertToIndicator(this PyObject pyObject, out IndicatorBa { indicator = null; - return pyObject.TryConvert(out IndicatorBase idp) && (indicator = idp) != null || - pyObject.TryConvert(out IndicatorBase idb) && (indicator = idb) != null || - pyObject.TryConvert(out IndicatorBase itb) && (indicator = itb) != null || - pyObject.TryConvert(out IndicatorBase ibd) && (indicator = ibd) != null; + if (pyObject.TryConvert(out IndicatorBase idp)) + { + indicator = idp; + } + else if (pyObject.TryConvert(out IndicatorBase idb)) + { + indicator = idb; + } + else if (pyObject.TryConvert(out IndicatorBase itb)) + { + indicator = itb; + } + else if (pyObject.TryConvert(out IndicatorBase ibd)) + { + indicator = ibd; + } + + return indicator != null; } /// Creates a new ExponentialMovingAverage indicator with the specified period and smoothingFactor from the left indicator From 91c6f27abbb344ef6cd83057c96d283bf9dad60f Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 21 Feb 2025 16:36:11 -0500 Subject: [PATCH 6/9] Solve review comments --- Algorithm/QCAlgorithm.Python.cs | 19 ++++++----- Indicators/CompositeIndicator.cs | 35 ++------------------- Indicators/IndicatorExtensions.cs | 29 ++++++++++------- Tests/Indicators/CompositeIndicatorTests.cs | 33 ++++++------------- 4 files changed, 39 insertions(+), 77 deletions(-) diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index 19e453f729fb..683c03077d3d 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -299,7 +299,7 @@ public Universe AddUniverse(PyObject pyObject) return AddUniverse(pyObject, null, null); } // TODO: to be removed when https://github.com/QuantConnect/pythonnet/issues/62 is solved - else if(pyObject.TryConvert(out universe)) + else if (pyObject.TryConvert(out universe)) { return AddUniverse(universe); } @@ -662,31 +662,30 @@ public void RegisterIndicator(Symbol symbol, PyObject indicator, PyObject pyObje public void RegisterIndicator(Symbol symbol, PyObject indicator, IDataConsolidator consolidator, PyObject selector = null) { // TODO: to be removed when https://github.com/QuantConnect/pythonnet/issues/62 is solved - IndicatorBase indicatorDataPoint; - IndicatorBase indicatorDataBar; - IndicatorBase indicatorTradeBar; - if (indicator.TryConvert(out var pythonIndicator)) + var convertedIndicator = indicator.ConvertToIndicator(); + + if (convertedIndicator is PythonIndicator pythonIndicator) { RegisterIndicator(symbol, WrapPythonIndicator(indicator, pythonIndicator), consolidator, selector?.ConvertToDelegate>()); } - else if (indicator.TryConvert(out indicatorDataPoint)) + else if (convertedIndicator is IndicatorBase indicatorDataPoint) { RegisterIndicator(symbol, indicatorDataPoint, consolidator, selector?.ConvertToDelegate>()); } - else if (indicator.TryConvert(out indicatorDataBar)) + else if (convertedIndicator is IndicatorBase indicatorDataBar) { RegisterIndicator(symbol, indicatorDataBar, consolidator, selector?.ConvertToDelegate>()); } - else if (indicator.TryConvert(out indicatorTradeBar)) + else if (convertedIndicator is IndicatorBase indicatorTradeBar) { RegisterIndicator(symbol, indicatorTradeBar, consolidator, selector?.ConvertToDelegate>()); } - else if (indicator.TryConvert(out IndicatorBase indicatorBaseData)) + else if (convertedIndicator is IndicatorBase indicatorBaseData) { RegisterIndicator(symbol, indicatorBaseData, consolidator, selector?.ConvertToDelegate>()); @@ -1768,7 +1767,7 @@ private dynamic[] GetIndicatorArray(PyObject first, PyObject second = null, PyOb { using (Py.GIL()) { - var array = new[] {first, second, third, fourth} + var array = new[] { first, second, third, fourth } .Select( x => { diff --git a/Indicators/CompositeIndicator.cs b/Indicators/CompositeIndicator.cs index 20420797b1ff..c04c9cb9c9aa 100644 --- a/Indicators/CompositeIndicator.cs +++ b/Indicators/CompositeIndicator.cs @@ -88,6 +88,7 @@ public CompositeIndicator(string name, IndicatorBase left, IndicatorBase right, _composer = composer; Left = left; Right = right; + Name ??= $"COMPOSE({Left.Name},{Right.Name})"; ConfigureEventHandlers(); } @@ -114,23 +115,8 @@ public CompositeIndicator(IndicatorBase left, IndicatorBase right, IndicatorComp /// 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) - : base(name) + : this(name, left.ConvertToIndicator(), right.ConvertToIndicator(), new IndicatorComposer(handler.ConvertToDelegate>())) { - if (!left.TryConvertToIndicator(out var leftIndicator)) - { - throw new ArgumentException($"The left argument should be a QuantConnect Indicator object, {left} was provided."); - } - if (!right.TryConvertToIndicator(out var rightIndicator)) - { - throw new ArgumentException($"The right argument should be a QuantConnect Indicator object, {right} was provided."); - } - - // if no name was provided, auto-generate one - Name ??= $"COMPOSE({leftIndicator.Name},{rightIndicator.Name})"; - Left = leftIndicator; - Right = rightIndicator; - _composer = CreateComposerFromPyObject(handler); - ConfigureEventHandlers(); } /// @@ -144,23 +130,6 @@ public CompositeIndicator(PyObject left, PyObject right, PyObject handler) : this(null, left, right, handler) { } - /// - /// Creates an IndicatorComposer from a Python function. - /// - /// A PyObject representing the Python function. - /// An IndicatorComposer that applies the Python function. - private static IndicatorComposer CreateComposerFromPyObject(PyObject handler) - { - // If the conversion fails, throw an exception - if (!handler.TryConvertToDelegate(out Func composer)) - { - 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); - } - /// /// Computes the next value of this indicator from the given state /// and returns an instance of the class diff --git a/Indicators/IndicatorExtensions.cs b/Indicators/IndicatorExtensions.cs index a84f94df11b1..c14fc516e054 100644 --- a/Indicators/IndicatorExtensions.cs +++ b/Indicators/IndicatorExtensions.cs @@ -285,33 +285,40 @@ public static CompositeIndicator Times(this IndicatorBase left, IndicatorBase ri } /// - /// Attempts to convert a into an . + /// Converts a into an . /// /// The Python object to convert. - /// The resulting indicator if successful; otherwise, null. - /// true if the conversion succeeds; otherwise, false. - public static bool TryConvertToIndicator(this PyObject pyObject, out IndicatorBase indicator) + /// The corresponding if the conversion is successful. + public static IndicatorBase ConvertToIndicator(this PyObject pyObject) { - indicator = null; - - if (pyObject.TryConvert(out IndicatorBase idp)) + IndicatorBase indicator; + if (pyObject.TryConvert(out var pi)) + { + pi.SetIndicator(pyObject); + indicator = pi; + } + else if (pyObject.TryConvert>(out var idp)) { indicator = idp; } - else if (pyObject.TryConvert(out IndicatorBase idb)) + else if (pyObject.TryConvert>(out var idb)) { indicator = idb; } - else if (pyObject.TryConvert(out IndicatorBase itb)) + else if (pyObject.TryConvert>(out var itb)) { indicator = itb; } - else if (pyObject.TryConvert(out IndicatorBase ibd)) + else if (pyObject.TryConvert>(out var ibd)) { indicator = ibd; } + else + { + indicator = new PythonIndicator(pyObject); + } - return indicator != null; + return indicator; } /// Creates a new ExponentialMovingAverage indicator with the specified period and smoothingFactor from the left indicator diff --git a/Tests/Indicators/CompositeIndicatorTests.cs b/Tests/Indicators/CompositeIndicatorTests.cs index f774ff9c15d0..219c88b2932c 100644 --- a/Tests/Indicators/CompositeIndicatorTests.cs +++ b/Tests/Indicators/CompositeIndicatorTests.cs @@ -102,18 +102,8 @@ public virtual void ResetsProperly() [TestCase("min", -12, 52, -12, true)] public virtual void PythonCompositeIndicatorConstructorValidatesBehavior(string operation, decimal leftValue, decimal rightValue, decimal expectedValue, bool usePythonIndicator) { - IndicatorBase left; - IndicatorBase right; - if (usePythonIndicator) - { - left = new PythonIndicator(CreatePyObjectIndicator(10)); - right = new PythonIndicator(CreatePyObjectIndicator(10)); - } - else - { - left = new SimpleMovingAverage("SMA", 10); - right = new SimpleMovingAverage("SMA", 10); - } + var left = new SimpleMovingAverage("SMA", 10); + var right = new SimpleMovingAverage("SMA", 10); using (Py.GIL()) { var testModule = PyModule.FromString("testModule", @@ -133,13 +123,13 @@ def composer(l, r): 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 = left.ToPython(); - using var rightPy = right.ToPython(); + 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()); @@ -149,19 +139,14 @@ def update_indicators(left, right, value_left, value_right): // Verify composite indicator name and properties using var name = composite.GetAttr("Name"); - using var typeLeft = composite.GetAttr("Left"); - using var typeRight = composite.GetAttr("Right"); Assert.AreEqual($"COMPOSE({left.Name},{right.Name})", name.ToString()); - Assert.AreEqual(left, typeLeft.As()); - Assert.AreEqual(right, typeRight.As()); - // Validate the composite indicator computed value + // 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()) @@ -174,17 +159,19 @@ from collections import deque class CustomSimpleMovingAverage(PythonIndicator): def __init__(self, period): - self.Name = 'CustomSMA' + self.Name = 'SMA' self.Value = 0 self.Period = period self.WarmUpPeriod = 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 - return count == self.queue.maxlen + self.Current = IndicatorDataPoint(input.Time, self.Value) + self.on_updated(IndicatorDataPoint(DateTime.Now, input.Value)) " ); From 7d38bb6d62ecdc72e2cab0b52d5cade0049fe026 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Fri, 21 Feb 2025 17:22:51 -0500 Subject: [PATCH 7/9] Resolve review comments --- Algorithm/QCAlgorithm.Python.cs | 2 +- Indicators/IndicatorExtensions.cs | 23 +++++++++-------- Tests/Indicators/CompositeIndicatorTests.cs | 28 ++++++++++----------- 3 files changed, 27 insertions(+), 26 deletions(-) diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index 683c03077d3d..b385866a995a 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -692,7 +692,7 @@ public void RegisterIndicator(Symbol symbol, PyObject indicator, IDataConsolidat } else { - RegisterIndicator(symbol, WrapPythonIndicator(indicator), consolidator, + RegisterIndicator(symbol, WrapPythonIndicator(indicator, (PythonIndicator)convertedIndicator), consolidator, selector?.ConvertToDelegate>()); } } diff --git a/Indicators/IndicatorExtensions.cs b/Indicators/IndicatorExtensions.cs index c14fc516e054..9159500520c2 100644 --- a/Indicators/IndicatorExtensions.cs +++ b/Indicators/IndicatorExtensions.cs @@ -292,26 +292,27 @@ public static CompositeIndicator Times(this IndicatorBase left, IndicatorBase ri public static IndicatorBase ConvertToIndicator(this PyObject pyObject) { IndicatorBase indicator; - if (pyObject.TryConvert(out var pi)) + + if (pyObject.TryConvert(out var pythonIndicator)) { - pi.SetIndicator(pyObject); - indicator = pi; + pythonIndicator.SetIndicator(pyObject); + indicator = pythonIndicator; } - else if (pyObject.TryConvert>(out var idp)) + else if (pyObject.TryConvert>(out var dataPointIndicator)) { - indicator = idp; + indicator = dataPointIndicator; } - else if (pyObject.TryConvert>(out var idb)) + else if (pyObject.TryConvert>(out var baseDataBarIndicator)) { - indicator = idb; + indicator = baseDataBarIndicator; } - else if (pyObject.TryConvert>(out var itb)) + else if (pyObject.TryConvert>(out var tradeBarIndicator)) { - indicator = itb; + indicator = tradeBarIndicator; } - else if (pyObject.TryConvert>(out var ibd)) + else if (pyObject.TryConvert>(out var baseDataIndicator)) { - indicator = ibd; + indicator = baseDataIndicator; } else { diff --git a/Tests/Indicators/CompositeIndicatorTests.cs b/Tests/Indicators/CompositeIndicatorTests.cs index 219c88b2932c..f1a1293d900b 100644 --- a/Tests/Indicators/CompositeIndicatorTests.cs +++ b/Tests/Indicators/CompositeIndicatorTests.cs @@ -114,15 +114,15 @@ 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) + 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 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)) + left.update(IndicatorDataPoint(DateTime.Now, value_left)) + right.update(IndicatorDataPoint(DateTime.Now, value_right)) "); using var createCompositeIndicator = testModule.GetAttr("create_composite_indicator"); @@ -159,19 +159,19 @@ from collections import deque class CustomSimpleMovingAverage(PythonIndicator): def __init__(self, period): - self.Name = 'SMA' - self.Value = 0 - self.Period = period - self.WarmUpPeriod = 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) + self.current = IndicatorDataPoint(DateTime.Now, self.value) - def Update(self, input): - self.queue.appendleft(input.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)) + self.value = sum(self.queue) / count + self.current = IndicatorDataPoint(input.time, self.value) + self.on_updated(IndicatorDataPoint(DateTime.Now, input.value)) " ); From f995487a6b43d75641f13f6d4989d19dbb3e645c Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Mon, 24 Feb 2025 09:13:51 -0500 Subject: [PATCH 8/9] Remove unnessary case --- Algorithm/QCAlgorithm.Python.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index b385866a995a..111614b828d6 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -690,11 +690,6 @@ public void RegisterIndicator(Symbol symbol, PyObject indicator, IDataConsolidat RegisterIndicator(symbol, indicatorBaseData, consolidator, selector?.ConvertToDelegate>()); } - else - { - RegisterIndicator(symbol, WrapPythonIndicator(indicator, (PythonIndicator)convertedIndicator), consolidator, - selector?.ConvertToDelegate>()); - } } /// From 4af8cfaf2abd878061e06bb7a40c90d137efd1a2 Mon Sep 17 00:00:00 2001 From: Josue Nina Date: Mon, 24 Feb 2025 14:52:28 -0500 Subject: [PATCH 9/9] Use GetIndicatorAsManagedObject instead of ConvertToIndicator --- Algorithm/QCAlgorithm.Python.cs | 24 ++++++++++++------- Indicators/CompositeIndicator.cs | 7 +++++- Indicators/IndicatorExtensions.cs | 40 +------------------------------ 3 files changed, 22 insertions(+), 49 deletions(-) diff --git a/Algorithm/QCAlgorithm.Python.cs b/Algorithm/QCAlgorithm.Python.cs index 111614b828d6..19e453f729fb 100644 --- a/Algorithm/QCAlgorithm.Python.cs +++ b/Algorithm/QCAlgorithm.Python.cs @@ -299,7 +299,7 @@ public Universe AddUniverse(PyObject pyObject) return AddUniverse(pyObject, null, null); } // TODO: to be removed when https://github.com/QuantConnect/pythonnet/issues/62 is solved - else if (pyObject.TryConvert(out universe)) + else if(pyObject.TryConvert(out universe)) { return AddUniverse(universe); } @@ -662,34 +662,40 @@ public void RegisterIndicator(Symbol symbol, PyObject indicator, PyObject pyObje public void RegisterIndicator(Symbol symbol, PyObject indicator, IDataConsolidator consolidator, PyObject selector = null) { // TODO: to be removed when https://github.com/QuantConnect/pythonnet/issues/62 is solved + IndicatorBase indicatorDataPoint; + IndicatorBase indicatorDataBar; + IndicatorBase indicatorTradeBar; - var convertedIndicator = indicator.ConvertToIndicator(); - - if (convertedIndicator is PythonIndicator pythonIndicator) + if (indicator.TryConvert(out var pythonIndicator)) { RegisterIndicator(symbol, WrapPythonIndicator(indicator, pythonIndicator), consolidator, selector?.ConvertToDelegate>()); } - else if (convertedIndicator is IndicatorBase indicatorDataPoint) + else if (indicator.TryConvert(out indicatorDataPoint)) { RegisterIndicator(symbol, indicatorDataPoint, consolidator, selector?.ConvertToDelegate>()); } - else if (convertedIndicator is IndicatorBase indicatorDataBar) + else if (indicator.TryConvert(out indicatorDataBar)) { RegisterIndicator(symbol, indicatorDataBar, consolidator, selector?.ConvertToDelegate>()); } - else if (convertedIndicator is IndicatorBase indicatorTradeBar) + else if (indicator.TryConvert(out indicatorTradeBar)) { RegisterIndicator(symbol, indicatorTradeBar, consolidator, selector?.ConvertToDelegate>()); } - else if (convertedIndicator is IndicatorBase indicatorBaseData) + else if (indicator.TryConvert(out IndicatorBase indicatorBaseData)) { RegisterIndicator(symbol, indicatorBaseData, consolidator, selector?.ConvertToDelegate>()); } + else + { + RegisterIndicator(symbol, WrapPythonIndicator(indicator), consolidator, + selector?.ConvertToDelegate>()); + } } /// @@ -1762,7 +1768,7 @@ private dynamic[] GetIndicatorArray(PyObject first, PyObject second = null, PyOb { using (Py.GIL()) { - var array = new[] { first, second, third, fourth } + var array = new[] {first, second, third, fourth} .Select( x => { diff --git a/Indicators/CompositeIndicator.cs b/Indicators/CompositeIndicator.cs index c04c9cb9c9aa..1040755a4660 100644 --- a/Indicators/CompositeIndicator.cs +++ b/Indicators/CompositeIndicator.cs @@ -115,7 +115,12 @@ public CompositeIndicator(IndicatorBase left, IndicatorBase right, IndicatorComp /// 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, left.ConvertToIndicator(), right.ConvertToIndicator(), new IndicatorComposer(handler.ConvertToDelegate>())) + : this( + name, + (IndicatorBase)left.GetIndicatorAsManagedObject(), + (IndicatorBase)right.GetIndicatorAsManagedObject(), + new IndicatorComposer(handler.ConvertToDelegate>()) + ) { } diff --git a/Indicators/IndicatorExtensions.cs b/Indicators/IndicatorExtensions.cs index 9159500520c2..523baa4cdf68 100644 --- a/Indicators/IndicatorExtensions.cs +++ b/Indicators/IndicatorExtensions.cs @@ -284,44 +284,6 @@ public static CompositeIndicator Times(this IndicatorBase left, IndicatorBase ri return new(name, left, right, (l, r) => l.Current.Value * r.Current.Value); } - /// - /// Converts a into an . - /// - /// The Python object to convert. - /// The corresponding if the conversion is successful. - public static IndicatorBase ConvertToIndicator(this PyObject pyObject) - { - IndicatorBase indicator; - - if (pyObject.TryConvert(out var pythonIndicator)) - { - pythonIndicator.SetIndicator(pyObject); - indicator = pythonIndicator; - } - else if (pyObject.TryConvert>(out var dataPointIndicator)) - { - indicator = dataPointIndicator; - } - else if (pyObject.TryConvert>(out var baseDataBarIndicator)) - { - indicator = baseDataBarIndicator; - } - else if (pyObject.TryConvert>(out var tradeBarIndicator)) - { - indicator = tradeBarIndicator; - } - else if (pyObject.TryConvert>(out var baseDataIndicator)) - { - indicator = baseDataIndicator; - } - else - { - indicator = new PythonIndicator(pyObject); - } - - return indicator; - } - /// Creates a new ExponentialMovingAverage indicator with the specified period and smoothingFactor from the left indicator /// /// The ExponentialMovingAverage indicator will be created using the data from left @@ -602,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)) {