Skip to content

Commit cf26cab

Browse files
committed
Add unit tests and update CompositeIndicator
1 parent 3686c8b commit cf26cab

File tree

2 files changed

+70
-19
lines changed

2 files changed

+70
-19
lines changed

Indicators/CompositeIndicator.cs

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515

1616
using System;
1717
using Python.Runtime;
18+
using QuantConnect.Data;
1819
using QuantConnect.Data.Market;
19-
using QuantConnect.Python;
2020

2121
namespace QuantConnect.Indicators
2222
{
@@ -151,14 +151,14 @@ public CompositeIndicator(PyObject left, PyObject right, PyObject handler)
151151
/// <returns>An IndicatorComposer that applies the Python function.</returns>
152152
private static IndicatorComposer CreateComposerFromPyObject(PyObject handler)
153153
{
154-
return (left, right) =>
154+
// If the conversion fails, throw an exception
155+
if (!handler.TryConvertToDelegate(out Func<IndicatorBase, IndicatorBase, IndicatorResult> composer))
155156
{
156-
using (Py.GIL())
157-
{
158-
dynamic result = handler.Invoke(left.Current.Value, right.Current.Value);
159-
return new IndicatorResult(result);
160-
}
161-
};
157+
throw new InvalidOperationException("Failed to convert the handler into a valid delegate.");
158+
}
159+
160+
// Return the converted delegate, since it matches the signature of IndicatorComposer
161+
return new IndicatorComposer(composer);
162162
}
163163

164164
/// <summary>
@@ -170,24 +170,24 @@ private static IndicatorComposer CreateComposerFromPyObject(PyObject handler)
170170
/// <returns>True if the conversion is successful; otherwise, false.</returns>
171171
private static bool TryConvertIndicator(PyObject pyObject, out IndicatorBase indicator)
172172
{
173-
if (pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> idp))
173+
indicator = null;
174+
if (pyObject.TryConvert(out IndicatorBase<IBaseData> ibd))
175+
{
176+
indicator = ibd;
177+
}
178+
else if (pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> idp))
174179
{
175180
indicator = idp;
176-
return true;
177181
}
178-
if (pyObject.TryConvert(out IndicatorBase<IBaseDataBar> idb))
182+
else if (pyObject.TryConvert(out IndicatorBase<IBaseDataBar> idb))
179183
{
180184
indicator = idb;
181-
return true;
182185
}
183-
if (pyObject.TryConvert(out IndicatorBase<TradeBar> itb))
186+
else if (pyObject.TryConvert(out IndicatorBase<TradeBar> itb))
184187
{
185188
indicator = itb;
186-
return true;
187189
}
188-
189-
indicator = null;
190-
return false;
190+
return indicator != null;
191191
}
192192

193193
/// <summary>

Tests/Indicators/CompositeIndicatorTests.cs

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
using System;
1717
using NUnit.Framework;
18+
using Python.Runtime;
1819
using QuantConnect.Indicators;
1920

2021
namespace QuantConnect.Tests.Indicators
@@ -72,13 +73,14 @@ public void CallsDelegateCorrectly()
7273
}
7374

7475
[Test]
75-
public virtual void ResetsProperly() {
76+
public virtual void ResetsProperly()
77+
{
7678
var left = new Maximum("left", 2);
7779
var right = new Minimum("right", 2);
7880
var composite = CreateCompositeIndicator(left, right, (l, r) => l.Current.Value + r.Current.Value);
7981

8082
left.Update(DateTime.Today, 1m);
81-
right.Update(DateTime.Today,-1m);
83+
right.Update(DateTime.Today, -1m);
8284

8385
left.Update(DateTime.Today.AddDays(1), -1m);
8486
right.Update(DateTime.Today.AddDays(1), 1m);
@@ -94,6 +96,55 @@ public virtual void ResetsProperly() {
9496
Assert.AreEqual(right.PeriodsSinceMinimum, 0);
9597
}
9698

99+
[TestCase("sum", 5, 10, 15)]
100+
[TestCase("min", -12, 52, -12)]
101+
public virtual void PythonCompositeIndicatorConstructorValidatesBehavior(string operation, decimal leftValue, decimal rightValue, decimal expectedValue)
102+
{
103+
var left = new SimpleMovingAverage("SMA", 10);
104+
var right = new SimpleMovingAverage("SMA", 10);
105+
using (Py.GIL())
106+
{
107+
var testModule = PyModule.FromString("testModule",
108+
@"
109+
from AlgorithmImports import *
110+
from QuantConnect.Indicators import *
111+
112+
def create_composite_indicator(left, right, operation):
113+
if operation == 'sum':
114+
def composer(l, r):
115+
return IndicatorResult(l.Current.Value + r.Current.Value)
116+
elif operation == 'min':
117+
def composer(l, r):
118+
return IndicatorResult(min(l.Current.Value, r.Current.Value))
119+
return CompositeIndicator(left, right, composer)
120+
121+
def update_indicators(left, right, value_left, value_right):
122+
left.Update(IndicatorDataPoint(DateTime.Now, value_left))
123+
right.Update(IndicatorDataPoint(DateTime.Now, value_right))
124+
");
125+
126+
var createCompositeIndicator = testModule.GetAttr("create_composite_indicator");
127+
var updateIndicators = testModule.GetAttr("update_indicators");
128+
129+
var leftPy = left.ToPython();
130+
var rightPy = right.ToPython();
131+
132+
// Create composite indicator using Python logic
133+
var composite = createCompositeIndicator.Invoke(leftPy, rightPy, operation.ToPython());
134+
135+
// Update the indicator with sample values (left, right)
136+
updateIndicators.Invoke(leftPy, rightPy, leftValue.ToPython(), rightValue.ToPython());
137+
138+
// Verify composite indicator name and properties
139+
Assert.AreEqual($"COMPOSE({left.Name},{right.Name})", composite.GetAttr("Name").ToString());
140+
Assert.AreEqual(left, composite.GetAttr("Left").As<IndicatorBase>());
141+
Assert.AreEqual(right, composite.GetAttr("Right").As<IndicatorBase>());
142+
143+
// Validate the composite indicator computed value
144+
Assert.AreEqual(expectedValue, composite.GetAttr("Current").GetAttr("Value").As<decimal>());
145+
}
146+
}
147+
97148
protected virtual CompositeIndicator CreateCompositeIndicator(IndicatorBase left, IndicatorBase right, QuantConnect.Indicators.CompositeIndicator.IndicatorComposer composer)
98149
{
99150
return new CompositeIndicator(left, right, composer);

0 commit comments

Comments
 (0)