Skip to content

Commit

Permalink
Add new test case
Browse files Browse the repository at this point in the history
  • Loading branch information
JosueNina committed Feb 21, 2025
1 parent cf26cab commit 4376f4d
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 55 deletions.
33 changes: 2 additions & 31 deletions Indicators/CompositeIndicator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
Expand Down Expand Up @@ -161,35 +161,6 @@ private static IndicatorComposer CreateComposerFromPyObject(PyObject handler)
return new IndicatorComposer(composer);
}

/// <summary>
/// Attempts to convert a <see cref="PyObject"/> into an <see cref="IndicatorBase"/>.
/// Supports indicators based on <see cref="IndicatorDataPoint"/>, <see cref="IBaseDataBar"/>, and <see cref="TradeBar"/>.
/// </summary>
/// <param name="pyObject">The Python object to convert.</param>
/// <param name="indicator">The converted indicator if successful; otherwise, null.</param>
/// <returns>True if the conversion is successful; otherwise, false.</returns>
private static bool TryConvertIndicator(PyObject pyObject, out IndicatorBase indicator)
{
indicator = null;
if (pyObject.TryConvert(out IndicatorBase<IBaseData> ibd))
{
indicator = ibd;
}
else if (pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> idp))
{
indicator = idp;
}
else if (pyObject.TryConvert(out IndicatorBase<IBaseDataBar> idb))
{
indicator = idb;
}
else if (pyObject.TryConvert(out IndicatorBase<TradeBar> itb))
{
indicator = itb;
}
return indicator != null;
}

/// <summary>
/// Computes the next value of this indicator from the given state
/// and returns an instance of the <see cref="IndicatorResult"/> class
Expand Down
38 changes: 28 additions & 10 deletions Indicators/IndicatorExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
using QuantConnect.Data;
using Python.Runtime;
using QuantConnect.Util;
using QuantConnect.Data.Market;

namespace QuantConnect.Indicators
{
Expand Down Expand Up @@ -98,7 +99,8 @@ public static CompositeIndicator WeightedBy<T, TWeight>(this IndicatorBase<T> va
denominator.Update(consolidated);
};

var resetCompositeIndicator = new ResetCompositeIndicator(numerator, denominator, GetOverIndicatorComposer(), () => {
var resetCompositeIndicator = new ResetCompositeIndicator(numerator, denominator, GetOverIndicatorComposer(), () =>
{
x.Reset();
y.Reset();
});
Expand Down Expand Up @@ -132,7 +134,7 @@ public static CompositeIndicator Plus(this IndicatorBase left, decimal constant)
/// <returns>The sum of the left and right indicators</returns>
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);
}

/// <summary>
Expand All @@ -147,7 +149,7 @@ public static CompositeIndicator Plus(this IndicatorBase left, IndicatorBase rig
/// <returns>The sum of the left and right indicators</returns>
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);
}

/// <summary>
Expand Down Expand Up @@ -176,7 +178,7 @@ public static CompositeIndicator Minus(this IndicatorBase left, decimal constant
/// <returns>The difference of the left and right indicators</returns>
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);
}

/// <summary>
Expand All @@ -191,7 +193,7 @@ public static CompositeIndicator Minus(this IndicatorBase left, IndicatorBase ri
/// <returns>The difference of the left and right indicators</returns>
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);
}

/// <summary>
Expand Down Expand Up @@ -220,7 +222,7 @@ public static CompositeIndicator Over(this IndicatorBase left, decimal constant)
/// <returns>The ratio of the left to the right indicator</returns>
public static CompositeIndicator Over(this IndicatorBase left, IndicatorBase right)
{
return new (left, right, GetOverIndicatorComposer());
return new(left, right, GetOverIndicatorComposer());
}

/// <summary>
Expand All @@ -235,7 +237,7 @@ public static CompositeIndicator Over(this IndicatorBase left, IndicatorBase rig
/// <returns>The ratio of the left to the right indicator</returns>
public static CompositeIndicator Over(this IndicatorBase left, IndicatorBase right, string name)
{
return new (name, left, right, GetOverIndicatorComposer());
return new(name, left, right, GetOverIndicatorComposer());
}

/// <summary>
Expand Down Expand Up @@ -264,7 +266,7 @@ public static CompositeIndicator Times(this IndicatorBase left, decimal constant
/// <returns>The product of the left to the right indicators</returns>
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);
}

/// <summary>
Expand All @@ -279,7 +281,23 @@ public static CompositeIndicator Times(this IndicatorBase left, IndicatorBase ri
/// <returns>The product of the left to the right indicators</returns>
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);
}

/// <summary>
/// Attempts to convert a <see cref="PyObject"/> into an <see cref="IndicatorBase"/>.
/// </summary>
/// <param name="pyObject">The Python object to convert.</param>
/// <param name="indicator">The resulting indicator if successful; otherwise, null.</param>
/// <returns><c>true</c> if the conversion succeeds; otherwise, <c>false</c>.</returns>
public static bool TryConvertToIndicator(this PyObject pyObject, out IndicatorBase indicator)
{
indicator = null;

return pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> idp) && (indicator = idp) != null ||
pyObject.TryConvert(out IndicatorBase<IBaseDataBar> idb) && (indicator = idb) != null ||
pyObject.TryConvert(out IndicatorBase<TradeBar> itb) && (indicator = itb) != null ||
pyObject.TryConvert(out IndicatorBase<IBaseData> ibd) && (indicator = ibd) != null;
}

/// <summary>Creates a new ExponentialMovingAverage indicator with the specified period and smoothingFactor from the left indicator
Expand Down Expand Up @@ -418,7 +436,7 @@ public static SimpleMovingAverage SMA(PyObject left, int period, bool waitForFir
return SMA(indicator, period, waitForFirstToReady);
}

/// <summary>
/// <summary>
/// Creates a new CompositeIndicator such that the result will be the ratio of the left to the constant
/// </summary>
/// <remarks>
Expand Down
78 changes: 64 additions & 14 deletions Tests/Indicators/CompositeIndicatorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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<IndicatorBase>());
Assert.AreEqual(right, composite.GetAttr("Right").As<IndicatorBase>());
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<IndicatorBase>());
Assert.AreEqual(right, typeRight.As<IndicatorBase>());

// Validate the composite indicator computed value
Assert.AreEqual(expectedValue, composite.GetAttr("Current").GetAttr("Value").As<decimal>());
using var value = composite.GetAttr("Current").GetAttr("Value");
Assert.AreEqual(expectedValue, value.As<decimal>());
}
}


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

Expand Down

0 comments on commit 4376f4d

Please sign in to comment.