Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python Support for CompositeIndicator #8598

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 10 additions & 11 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 @@ -662,38 +662,37 @@ public void RegisterIndicator(Symbol symbol, PyObject indicator, PyObject pyObje
public void RegisterIndicator(Symbol symbol, PyObject indicator, IDataConsolidator consolidator, PyObject selector = null)
{
// TODO: to be removed when https://github.com/QuantConnect/pythonnet/issues/62 is solved
IndicatorBase<IndicatorDataPoint> indicatorDataPoint;
IndicatorBase<IBaseDataBar> indicatorDataBar;
IndicatorBase<TradeBar> indicatorTradeBar;

if (indicator.TryConvert<PythonIndicator>(out var pythonIndicator))
var convertedIndicator = indicator.ConvertToIndicator();

if (convertedIndicator is PythonIndicator pythonIndicator)
{
RegisterIndicator(symbol, WrapPythonIndicator(indicator, pythonIndicator), consolidator,
selector?.ConvertToDelegate<Func<IBaseData, IBaseData>>());
}
else if (indicator.TryConvert(out indicatorDataPoint))
else if (convertedIndicator is IndicatorBase<IndicatorDataPoint> indicatorDataPoint)
{
RegisterIndicator(symbol, indicatorDataPoint, consolidator,
selector?.ConvertToDelegate<Func<IBaseData, decimal>>());
}
else if (indicator.TryConvert(out indicatorDataBar))
else if (convertedIndicator is IndicatorBase<IBaseDataBar> indicatorDataBar)
{
RegisterIndicator(symbol, indicatorDataBar, consolidator,
selector?.ConvertToDelegate<Func<IBaseData, IBaseDataBar>>());
}
else if (indicator.TryConvert(out indicatorTradeBar))
else if (convertedIndicator is IndicatorBase<TradeBar> indicatorTradeBar)
{
RegisterIndicator(symbol, indicatorTradeBar, consolidator,
selector?.ConvertToDelegate<Func<IBaseData, TradeBar>>());
}
else if (indicator.TryConvert(out IndicatorBase<IBaseData> indicatorBaseData))
else if (convertedIndicator is IndicatorBase<IBaseData> indicatorBaseData)
{
RegisterIndicator(symbol, indicatorBaseData, consolidator,
selector?.ConvertToDelegate<Func<IBaseData, IBaseData>>());
}
else
{
RegisterIndicator(symbol, WrapPythonIndicator(indicator), consolidator,
RegisterIndicator(symbol, WrapPythonIndicator(indicator, (PythonIndicator)convertedIndicator), consolidator,
selector?.ConvertToDelegate<Func<IBaseData, IBaseData>>());
}
}
Expand Down Expand Up @@ -1768,7 +1767,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
38 changes: 35 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;
using QuantConnect.Data.Market;

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 All @@ -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();
}

Expand All @@ -98,6 +103,33 @@ 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)
: this(name, left.ConvertToIndicator(), right.ConvertToIndicator(), new IndicatorComposer(handler.ConvertToDelegate<Func<IndicatorBase, IndicatorBase, IndicatorResult>>()))
{
}

/// <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>
/// 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 +162,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
60 changes: 50 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,45 @@ 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>
/// Converts a <see cref="PyObject"/> into an <see cref="IndicatorBase"/>.
/// </summary>
/// <param name="pyObject">The Python object to convert.</param>
/// <returns>The corresponding <see cref="IndicatorBase"/> if the conversion is successful.</returns>
public static IndicatorBase ConvertToIndicator(this PyObject pyObject)
{
IndicatorBase indicator;

if (pyObject.TryConvert<PythonIndicator>(out var pythonIndicator))
{
pythonIndicator.SetIndicator(pyObject);
indicator = pythonIndicator;
}
else if (pyObject.TryConvert<IndicatorBase<IndicatorDataPoint>>(out var dataPointIndicator))
{
indicator = dataPointIndicator;
}
else if (pyObject.TryConvert<IndicatorBase<IBaseDataBar>>(out var baseDataBarIndicator))
{
indicator = baseDataBarIndicator;
}
else if (pyObject.TryConvert<IndicatorBase<TradeBar>>(out var tradeBarIndicator))
{
indicator = tradeBarIndicator;
}
else if (pyObject.TryConvert<IndicatorBase<IBaseData>>(out var baseDataIndicator))
{
indicator = baseDataIndicator;
}
else
{
indicator = new PythonIndicator(pyObject);
}

return indicator;
}

/// <summary>Creates a new ExponentialMovingAverage indicator with the specified period and smoothingFactor from the left indicator
Expand Down Expand Up @@ -418,7 +458,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
92 changes: 90 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,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<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 = '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);
Expand Down
Loading