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);