|
14 | 14 | */ |
15 | 15 |
|
16 | 16 | using System; |
| 17 | +using Python.Runtime; |
| 18 | +using QuantConnect.Data.Market; |
| 19 | +using QuantConnect.Python; |
17 | 20 |
|
18 | 21 | namespace QuantConnect.Indicators |
19 | 22 | { |
@@ -64,7 +67,8 @@ public override bool IsReady |
64 | 67 | /// <summary> |
65 | 68 | /// Resets this indicator to its initial state |
66 | 69 | /// </summary> |
67 | | - public override void Reset() { |
| 70 | + public override void Reset() |
| 71 | + { |
68 | 72 | Left.Reset(); |
69 | 73 | Right.Reset(); |
70 | 74 | base.Reset(); |
@@ -98,6 +102,94 @@ public CompositeIndicator(IndicatorBase left, IndicatorBase right, IndicatorComp |
98 | 102 | : this($"COMPOSE({left.Name},{right.Name})", left, right, composer) |
99 | 103 | { } |
100 | 104 |
|
| 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 | + |
101 | 193 | /// <summary> |
102 | 194 | /// Computes the next value of this indicator from the given state |
103 | 195 | /// and returns an instance of the <see cref="IndicatorResult"/> class |
@@ -130,8 +222,8 @@ protected override decimal ComputeNextValue(IndicatorDataPoint _) |
130 | 222 | private void ConfigureEventHandlers() |
131 | 223 | { |
132 | 224 | // 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<>)); |
135 | 227 |
|
136 | 228 | // wire up the Updated events such that when we get a new piece of data from both left and right |
137 | 229 | // we'll call update on this indicator. It's important to note that the CompositeIndicator only uses |
|
0 commit comments