Skip to content

Commit

Permalink
Add python support for CompositeIndicator
Browse files Browse the repository at this point in the history
  • Loading branch information
JosueNina committed Feb 20, 2025
1 parent 4dfb6eb commit 3686c8b
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 64 deletions.
63 changes: 2 additions & 61 deletions Algorithm/QCAlgorithm.Python.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -569,65 +569,6 @@ public void AddUniverseOptions(PyObject universe, PyObject optionFilter)
}
}

/// <summary>
/// Creates a new <see cref="CompositeIndicator"/> using two indicators and a custom Python function as a handler.
/// </summary>
/// <param name="name">The name of the composite indicator.</param>
/// <param name="left">The first indicator used in the composition.</param>
/// <param name="right">The second indicator used in the composition.</param>
/// <param name="handler">A Python function that takes two indicator values and returns the computed result.</param>
/// <returns>A new instance of <see cref="CompositeIndicator"/>.</returns>
/// <exception cref="ArgumentException">
/// Thrown when the provided left or right indicator is not a valid QuantConnect Indicator object.
/// </exception>
[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);
}

/// <summary>
/// Attempts to convert a Python object into a valid QuantConnect indicator.
/// </summary>
/// <param name="pyObject">The Python object to convert.</param>
/// <returns>
/// A valid <see cref="IndicatorBase"/> instance if conversion is successful; otherwise, <c>null</c>.
/// </returns>
public IndicatorBase GetIndicator(PyObject pyObject)
{
if (pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> indicatorDataPoint))
{
return indicatorDataPoint;
}
if (pyObject.TryConvert(out IndicatorBase<IBaseDataBar> indicatorDataBar))
{
return indicatorDataBar;
}
if (pyObject.TryConvert(out IndicatorBase<TradeBar> indicatorTradeBar))
{
return indicatorTradeBar;
}
return null;
}

/// <summary>
/// Registers the consolidator to receive automatic updates as well as configures the indicator to receive updates
/// from the consolidator.
Expand Down Expand Up @@ -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 =>
{
Expand Down
98 changes: 95 additions & 3 deletions Indicators/CompositeIndicator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
*/

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

namespace QuantConnect.Indicators
{
Expand Down Expand Up @@ -64,7 +67,8 @@ public override bool IsReady
/// <summary>
/// Resets this indicator to its initial state
/// </summary>
public override void Reset() {
public override void Reset()
{
Left.Reset();
Right.Reset();
base.Reset();
Expand Down Expand Up @@ -98,6 +102,94 @@ public CompositeIndicator(IndicatorBase left, IndicatorBase right, IndicatorComp
: this($"COMPOSE({left.Name},{right.Name})", left, right, composer)
{ }

/// <summary>
/// Initializes a new instance of <see cref="CompositeIndicator"/> using two indicators
/// and a custom function.
/// </summary>
/// <param name="name">The name of the composite indicator.</param>
/// <param name="left">The first indicator in the composition.</param>
/// <param name="right">The second indicator in the composition.</param>
/// <param name="handler">A Python function that processes the indicator values.</param>
/// <exception cref="ArgumentException">
/// Thrown if the provided left or right indicator is not a valid QuantConnect Indicator object.
/// </exception>
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();
}

/// <summary>
/// Initializes a new instance of <see cref="CompositeIndicator"/> using two indicators
/// and a custom function.
/// </summary>
/// <param name="left">The first indicator in the composition.</param>
/// <param name="right">The second indicator in the composition.</param>
/// <param name="handler">A Python function that processes the indicator values.</param>
public CompositeIndicator(PyObject left, PyObject right, PyObject handler)
: this(null, left, right, handler)
{ }

/// <summary>
/// Creates an IndicatorComposer from a Python function.
/// </summary>
/// <param name="handler">A PyObject representing the Python function.</param>
/// <returns>An IndicatorComposer that applies the Python function.</returns>
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);
}
};
}

/// <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)
{
if (pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> idp))
{
indicator = idp;
return true;
}
if (pyObject.TryConvert(out IndicatorBase<IBaseDataBar> idb))
{
indicator = idb;
return true;
}
if (pyObject.TryConvert(out IndicatorBase<TradeBar> itb))
{
indicator = itb;
return true;
}

indicator = null;
return false;
}

/// <summary>
/// Computes the next value of this indicator from the given state
/// and returns an instance of the <see cref="IndicatorResult"/> class
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 3686c8b

Please sign in to comment.