Skip to content

Commit 3686c8b

Browse files
committed
Add python support for CompositeIndicator
1 parent 4dfb6eb commit 3686c8b

File tree

2 files changed

+97
-64
lines changed

2 files changed

+97
-64
lines changed

Algorithm/QCAlgorithm.Python.cs

Lines changed: 2 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ public Universe AddUniverse(PyObject pyObject)
299299
return AddUniverse(pyObject, null, null);
300300
}
301301
// TODO: to be removed when https://github.com/QuantConnect/pythonnet/issues/62 is solved
302-
else if (pyObject.TryConvert(out universe))
302+
else if(pyObject.TryConvert(out universe))
303303
{
304304
return AddUniverse(universe);
305305
}
@@ -569,65 +569,6 @@ public void AddUniverseOptions(PyObject universe, PyObject optionFilter)
569569
}
570570
}
571571

572-
/// <summary>
573-
/// Creates a new <see cref="CompositeIndicator"/> using two indicators and a custom Python function as a handler.
574-
/// </summary>
575-
/// <param name="name">The name of the composite indicator.</param>
576-
/// <param name="left">The first indicator used in the composition.</param>
577-
/// <param name="right">The second indicator used in the composition.</param>
578-
/// <param name="handler">A Python function that takes two indicator values and returns the computed result.</param>
579-
/// <returns>A new instance of <see cref="CompositeIndicator"/>.</returns>
580-
/// <exception cref="ArgumentException">
581-
/// Thrown when the provided left or right indicator is not a valid QuantConnect Indicator object.
582-
/// </exception>
583-
[DocumentationAttribute(Universes)]
584-
public CompositeIndicator CompositeIndicator(string name, PyObject left, PyObject right, PyObject handler)
585-
{
586-
var leftIndicator = GetIndicator(left);
587-
var rightIndicator = GetIndicator(right);
588-
if (leftIndicator == null)
589-
{
590-
throw new ArgumentException($"The left argument should be a QuantConnect Indicator object, {left} was provided.");
591-
}
592-
if (rightIndicator == null)
593-
{
594-
throw new ArgumentException($"The right argument should be a QuantConnect Indicator object, {right} was provided.");
595-
}
596-
CompositeIndicator.IndicatorComposer composer = (left, right) =>
597-
{
598-
using (Py.GIL())
599-
{
600-
dynamic result = handler.Invoke(left.Current.Value, right.Current.Value);
601-
return new IndicatorResult(result);
602-
}
603-
};
604-
return new CompositeIndicator(name, leftIndicator, rightIndicator, composer);
605-
}
606-
607-
/// <summary>
608-
/// Attempts to convert a Python object into a valid QuantConnect indicator.
609-
/// </summary>
610-
/// <param name="pyObject">The Python object to convert.</param>
611-
/// <returns>
612-
/// A valid <see cref="IndicatorBase"/> instance if conversion is successful; otherwise, <c>null</c>.
613-
/// </returns>
614-
public IndicatorBase GetIndicator(PyObject pyObject)
615-
{
616-
if (pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> indicatorDataPoint))
617-
{
618-
return indicatorDataPoint;
619-
}
620-
if (pyObject.TryConvert(out IndicatorBase<IBaseDataBar> indicatorDataBar))
621-
{
622-
return indicatorDataBar;
623-
}
624-
if (pyObject.TryConvert(out IndicatorBase<TradeBar> indicatorTradeBar))
625-
{
626-
return indicatorTradeBar;
627-
}
628-
return null;
629-
}
630-
631572
/// <summary>
632573
/// Registers the consolidator to receive automatic updates as well as configures the indicator to receive updates
633574
/// from the consolidator.
@@ -1827,7 +1768,7 @@ private dynamic[] GetIndicatorArray(PyObject first, PyObject second = null, PyOb
18271768
{
18281769
using (Py.GIL())
18291770
{
1830-
var array = new[] { first, second, third, fourth }
1771+
var array = new[] {first, second, third, fourth}
18311772
.Select(
18321773
x =>
18331774
{

Indicators/CompositeIndicator.cs

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
*/
1515

1616
using System;
17+
using Python.Runtime;
18+
using QuantConnect.Data.Market;
19+
using QuantConnect.Python;
1720

1821
namespace QuantConnect.Indicators
1922
{
@@ -64,7 +67,8 @@ public override bool IsReady
6467
/// <summary>
6568
/// Resets this indicator to its initial state
6669
/// </summary>
67-
public override void Reset() {
70+
public override void Reset()
71+
{
6872
Left.Reset();
6973
Right.Reset();
7074
base.Reset();
@@ -98,6 +102,94 @@ public CompositeIndicator(IndicatorBase left, IndicatorBase right, IndicatorComp
98102
: this($"COMPOSE({left.Name},{right.Name})", left, right, composer)
99103
{ }
100104

105+
/// <summary>
106+
/// Initializes a new instance of <see cref="CompositeIndicator"/> using two indicators
107+
/// and a custom function.
108+
/// </summary>
109+
/// <param name="name">The name of the composite indicator.</param>
110+
/// <param name="left">The first indicator in the composition.</param>
111+
/// <param name="right">The second indicator in the composition.</param>
112+
/// <param name="handler">A Python function that processes the indicator values.</param>
113+
/// <exception cref="ArgumentException">
114+
/// Thrown if the provided left or right indicator is not a valid QuantConnect Indicator object.
115+
/// </exception>
116+
public CompositeIndicator(string name, PyObject left, PyObject right, PyObject handler)
117+
: base(name)
118+
{
119+
if (!TryConvertIndicator(left, out var leftIndicator))
120+
{
121+
throw new ArgumentException($"The left argument should be a QuantConnect Indicator object, {left} was provided.");
122+
}
123+
if (!TryConvertIndicator(right, out var rightIndicator))
124+
{
125+
throw new ArgumentException($"The right argument should be a QuantConnect Indicator object, {right} was provided.");
126+
}
127+
128+
// if no name was provided, auto-generate one
129+
Name ??= $"COMPOSE({leftIndicator.Name},{rightIndicator.Name})";
130+
Left = leftIndicator;
131+
Right = rightIndicator;
132+
_composer = CreateComposerFromPyObject(handler);
133+
ConfigureEventHandlers();
134+
}
135+
136+
/// <summary>
137+
/// Initializes a new instance of <see cref="CompositeIndicator"/> using two indicators
138+
/// and a custom function.
139+
/// </summary>
140+
/// <param name="left">The first indicator in the composition.</param>
141+
/// <param name="right">The second indicator in the composition.</param>
142+
/// <param name="handler">A Python function that processes the indicator values.</param>
143+
public CompositeIndicator(PyObject left, PyObject right, PyObject handler)
144+
: this(null, left, right, handler)
145+
{ }
146+
147+
/// <summary>
148+
/// Creates an IndicatorComposer from a Python function.
149+
/// </summary>
150+
/// <param name="handler">A PyObject representing the Python function.</param>
151+
/// <returns>An IndicatorComposer that applies the Python function.</returns>
152+
private static IndicatorComposer CreateComposerFromPyObject(PyObject handler)
153+
{
154+
return (left, right) =>
155+
{
156+
using (Py.GIL())
157+
{
158+
dynamic result = handler.Invoke(left.Current.Value, right.Current.Value);
159+
return new IndicatorResult(result);
160+
}
161+
};
162+
}
163+
164+
/// <summary>
165+
/// Attempts to convert a <see cref="PyObject"/> into an <see cref="IndicatorBase"/>.
166+
/// Supports indicators based on <see cref="IndicatorDataPoint"/>, <see cref="IBaseDataBar"/>, and <see cref="TradeBar"/>.
167+
/// </summary>
168+
/// <param name="pyObject">The Python object to convert.</param>
169+
/// <param name="indicator">The converted indicator if successful; otherwise, null.</param>
170+
/// <returns>True if the conversion is successful; otherwise, false.</returns>
171+
private static bool TryConvertIndicator(PyObject pyObject, out IndicatorBase indicator)
172+
{
173+
if (pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> idp))
174+
{
175+
indicator = idp;
176+
return true;
177+
}
178+
if (pyObject.TryConvert(out IndicatorBase<IBaseDataBar> idb))
179+
{
180+
indicator = idb;
181+
return true;
182+
}
183+
if (pyObject.TryConvert(out IndicatorBase<TradeBar> itb))
184+
{
185+
indicator = itb;
186+
return true;
187+
}
188+
189+
indicator = null;
190+
return false;
191+
}
192+
101193
/// <summary>
102194
/// Computes the next value of this indicator from the given state
103195
/// and returns an instance of the <see cref="IndicatorResult"/> class
@@ -130,8 +222,8 @@ protected override decimal ComputeNextValue(IndicatorDataPoint _)
130222
private void ConfigureEventHandlers()
131223
{
132224
// if either of these are constants then there's no reason
133-
bool leftIsConstant = Left.GetType().IsSubclassOfGeneric(typeof (ConstantIndicator<>));
134-
bool rightIsConstant = Right.GetType().IsSubclassOfGeneric(typeof (ConstantIndicator<>));
225+
bool leftIsConstant = Left.GetType().IsSubclassOfGeneric(typeof(ConstantIndicator<>));
226+
bool rightIsConstant = Right.GetType().IsSubclassOfGeneric(typeof(ConstantIndicator<>));
135227

136228
// wire up the Updated events such that when we get a new piece of data from both left and right
137229
// we'll call update on this indicator. It's important to note that the CompositeIndicator only uses

0 commit comments

Comments
 (0)