Skip to content

Commit

Permalink
Add unit tests and update CompositeIndicator
Browse files Browse the repository at this point in the history
  • Loading branch information
JosueNina committed Feb 20, 2025
1 parent 3686c8b commit e400d0e
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 19 deletions.
34 changes: 17 additions & 17 deletions Indicators/CompositeIndicator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@

using System;
using Python.Runtime;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Python;

namespace QuantConnect.Indicators
{
Expand Down Expand Up @@ -151,14 +151,14 @@ public CompositeIndicator(PyObject left, PyObject right, PyObject handler)
/// <returns>An IndicatorComposer that applies the Python function.</returns>
private static IndicatorComposer CreateComposerFromPyObject(PyObject handler)
{
return (left, right) =>
// If the conversion fails, throw an exception
if (!handler.TryConvertToDelegate(out Func<IndicatorBase, IndicatorBase, IndicatorResult> 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);
}

/// <summary>
Expand All @@ -170,24 +170,24 @@ private static IndicatorComposer CreateComposerFromPyObject(PyObject handler)
/// <returns>True if the conversion is successful; otherwise, false.</returns>
private static bool TryConvertIndicator(PyObject pyObject, out IndicatorBase indicator)
{
if (pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> idp))
indicator = null;
if (pyObject.TryConvert(out IndicatorBase<IBaseData> ibd))
{
indicator = ibd;
}
else if (pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> idp))
{
indicator = idp;
return true;
}
if (pyObject.TryConvert(out IndicatorBase<IBaseDataBar> idb))
else if (pyObject.TryConvert(out IndicatorBase<IBaseDataBar> idb))
{
indicator = idb;
return true;
}
if (pyObject.TryConvert(out IndicatorBase<TradeBar> itb))
else if (pyObject.TryConvert(out IndicatorBase<TradeBar> itb))
{
indicator = itb;
return true;
}

indicator = null;
return false;
return indicator != null;
}

/// <summary>
Expand Down
56 changes: 54 additions & 2 deletions Tests/Indicators/CompositeIndicatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

using System;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Indicators;

namespace QuantConnect.Tests.Indicators
Expand Down Expand Up @@ -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);
Expand All @@ -94,6 +96,56 @@ 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 indicators using Python logic
var composite = createCompositeIndicator.Invoke(leftPy, rightPy, operation.ToPython());

// Update indicators 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<IndicatorBase>());
Assert.AreEqual(right, composite.GetAttr("Right").As<IndicatorBase>());

// Validate the composite indicators' computed values
Assert.AreEqual(expectedValue, composite.GetAttr("Current").GetAttr("Value").As<decimal>());
//Assert.AreEqual(5, compositeMinPy.GetAttr("Current").GetAttr("Value").As<decimal>());
}
}

protected virtual CompositeIndicator CreateCompositeIndicator(IndicatorBase left, IndicatorBase right, QuantConnect.Indicators.CompositeIndicator.IndicatorComposer composer)
{
return new CompositeIndicator(left, right, composer);
Expand Down

0 comments on commit e400d0e

Please sign in to comment.