Skip to content

Commit bc41249

Browse files
committed
Add new test case
1 parent cf26cab commit bc41249

File tree

3 files changed

+94
-55
lines changed

3 files changed

+94
-55
lines changed

Indicators/CompositeIndicator.cs

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,11 @@ public CompositeIndicator(IndicatorBase left, IndicatorBase right, IndicatorComp
116116
public CompositeIndicator(string name, PyObject left, PyObject right, PyObject handler)
117117
: base(name)
118118
{
119-
if (!TryConvertIndicator(left, out var leftIndicator))
119+
if (!left.TryConvertIndicator(out var leftIndicator))
120120
{
121121
throw new ArgumentException($"The left argument should be a QuantConnect Indicator object, {left} was provided.");
122122
}
123-
if (!TryConvertIndicator(right, out var rightIndicator))
123+
if (!right.TryConvertIndicator(out var rightIndicator))
124124
{
125125
throw new ArgumentException($"The right argument should be a QuantConnect Indicator object, {right} was provided.");
126126
}
@@ -161,35 +161,6 @@ private static IndicatorComposer CreateComposerFromPyObject(PyObject handler)
161161
return new IndicatorComposer(composer);
162162
}
163163

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-
indicator = null;
174-
if (pyObject.TryConvert(out IndicatorBase<IBaseData> ibd))
175-
{
176-
indicator = ibd;
177-
}
178-
else if (pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> idp))
179-
{
180-
indicator = idp;
181-
}
182-
else if (pyObject.TryConvert(out IndicatorBase<IBaseDataBar> idb))
183-
{
184-
indicator = idb;
185-
}
186-
else if (pyObject.TryConvert(out IndicatorBase<TradeBar> itb))
187-
{
188-
indicator = itb;
189-
}
190-
return indicator != null;
191-
}
192-
193164
/// <summary>
194165
/// Computes the next value of this indicator from the given state
195166
/// and returns an instance of the <see cref="IndicatorResult"/> class

Indicators/IndicatorExtensions.cs

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using QuantConnect.Data;
2020
using Python.Runtime;
2121
using QuantConnect.Util;
22+
using QuantConnect.Data.Market;
2223

2324
namespace QuantConnect.Indicators
2425
{
@@ -98,7 +99,8 @@ public static CompositeIndicator WeightedBy<T, TWeight>(this IndicatorBase<T> va
9899
denominator.Update(consolidated);
99100
};
100101

101-
var resetCompositeIndicator = new ResetCompositeIndicator(numerator, denominator, GetOverIndicatorComposer(), () => {
102+
var resetCompositeIndicator = new ResetCompositeIndicator(numerator, denominator, GetOverIndicatorComposer(), () =>
103+
{
102104
x.Reset();
103105
y.Reset();
104106
});
@@ -132,7 +134,7 @@ public static CompositeIndicator Plus(this IndicatorBase left, decimal constant)
132134
/// <returns>The sum of the left and right indicators</returns>
133135
public static CompositeIndicator Plus(this IndicatorBase left, IndicatorBase right)
134136
{
135-
return new (left, right, (l, r) => l.Current.Value + r.Current.Value);
137+
return new(left, right, (l, r) => l.Current.Value + r.Current.Value);
136138
}
137139

138140
/// <summary>
@@ -147,7 +149,7 @@ public static CompositeIndicator Plus(this IndicatorBase left, IndicatorBase rig
147149
/// <returns>The sum of the left and right indicators</returns>
148150
public static CompositeIndicator Plus(this IndicatorBase left, IndicatorBase right, string name)
149151
{
150-
return new (name, left, right, (l, r) => l.Current.Value + r.Current.Value);
152+
return new(name, left, right, (l, r) => l.Current.Value + r.Current.Value);
151153
}
152154

153155
/// <summary>
@@ -176,7 +178,7 @@ public static CompositeIndicator Minus(this IndicatorBase left, decimal constant
176178
/// <returns>The difference of the left and right indicators</returns>
177179
public static CompositeIndicator Minus(this IndicatorBase left, IndicatorBase right)
178180
{
179-
return new (left, right, (l, r) => l.Current.Value - r.Current.Value);
181+
return new(left, right, (l, r) => l.Current.Value - r.Current.Value);
180182
}
181183

182184
/// <summary>
@@ -191,7 +193,7 @@ public static CompositeIndicator Minus(this IndicatorBase left, IndicatorBase ri
191193
/// <returns>The difference of the left and right indicators</returns>
192194
public static CompositeIndicator Minus(this IndicatorBase left, IndicatorBase right, string name)
193195
{
194-
return new (name, left, right, (l, r) => l.Current.Value - r.Current.Value);
196+
return new(name, left, right, (l, r) => l.Current.Value - r.Current.Value);
195197
}
196198

197199
/// <summary>
@@ -220,7 +222,7 @@ public static CompositeIndicator Over(this IndicatorBase left, decimal constant)
220222
/// <returns>The ratio of the left to the right indicator</returns>
221223
public static CompositeIndicator Over(this IndicatorBase left, IndicatorBase right)
222224
{
223-
return new (left, right, GetOverIndicatorComposer());
225+
return new(left, right, GetOverIndicatorComposer());
224226
}
225227

226228
/// <summary>
@@ -235,7 +237,7 @@ public static CompositeIndicator Over(this IndicatorBase left, IndicatorBase rig
235237
/// <returns>The ratio of the left to the right indicator</returns>
236238
public static CompositeIndicator Over(this IndicatorBase left, IndicatorBase right, string name)
237239
{
238-
return new (name, left, right, GetOverIndicatorComposer());
240+
return new(name, left, right, GetOverIndicatorComposer());
239241
}
240242

241243
/// <summary>
@@ -264,7 +266,7 @@ public static CompositeIndicator Times(this IndicatorBase left, decimal constant
264266
/// <returns>The product of the left to the right indicators</returns>
265267
public static CompositeIndicator Times(this IndicatorBase left, IndicatorBase right)
266268
{
267-
return new (left, right, (l, r) => l.Current.Value * r.Current.Value);
269+
return new(left, right, (l, r) => l.Current.Value * r.Current.Value);
268270
}
269271

270272
/// <summary>
@@ -279,7 +281,23 @@ public static CompositeIndicator Times(this IndicatorBase left, IndicatorBase ri
279281
/// <returns>The product of the left to the right indicators</returns>
280282
public static CompositeIndicator Times(this IndicatorBase left, IndicatorBase right, string name)
281283
{
282-
return new (name, left, right, (l, r) => l.Current.Value * r.Current.Value);
284+
return new(name, left, right, (l, r) => l.Current.Value * r.Current.Value);
285+
}
286+
287+
/// <summary>
288+
/// Attempts to convert a <see cref="PyObject"/> into an <see cref="IndicatorBase"/>.
289+
/// </summary>
290+
/// <param name="pyObject">The Python object to convert.</param>
291+
/// <param name="indicator">The resulting indicator if successful; otherwise, null.</param>
292+
/// <returns><c>true</c> if the conversion succeeds; otherwise, <c>false</c>.</returns>
293+
public static bool TryConvertIndicator(this PyObject pyObject, out IndicatorBase indicator)
294+
{
295+
indicator = null;
296+
297+
return pyObject.TryConvert(out IndicatorBase<IBaseData> ibd) && (indicator = ibd) != null ||
298+
pyObject.TryConvert(out IndicatorBase<IndicatorDataPoint> idp) && (indicator = idp) != null ||
299+
pyObject.TryConvert(out IndicatorBase<IBaseDataBar> idb) && (indicator = idb) != null ||
300+
pyObject.TryConvert(out IndicatorBase<TradeBar> itb) && (indicator = itb) != null;
283301
}
284302

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

421-
/// <summary>
439+
/// <summary>
422440
/// Creates a new CompositeIndicator such that the result will be the ratio of the left to the constant
423441
/// </summary>
424442
/// <remarks>

Tests/Indicators/CompositeIndicatorTests.cs

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -96,12 +96,24 @@ public virtual void ResetsProperly()
9696
Assert.AreEqual(right.PeriodsSinceMinimum, 0);
9797
}
9898

99-
[TestCase("sum", 5, 10, 15)]
100-
[TestCase("min", -12, 52, -12)]
101-
public virtual void PythonCompositeIndicatorConstructorValidatesBehavior(string operation, decimal leftValue, decimal rightValue, decimal expectedValue)
99+
[TestCase("sum", 5, 10, 15, false)]
100+
[TestCase("min", -12, 52, -12, false)]
101+
[TestCase("sum", 5, 10, 15, true)]
102+
[TestCase("min", -12, 52, -12, true)]
103+
public virtual void PythonCompositeIndicatorConstructorValidatesBehavior(string operation, decimal leftValue, decimal rightValue, decimal expectedValue, bool usePythonIndicator)
102104
{
103-
var left = new SimpleMovingAverage("SMA", 10);
104-
var right = new SimpleMovingAverage("SMA", 10);
105+
IndicatorBase left;
106+
IndicatorBase right;
107+
if (usePythonIndicator)
108+
{
109+
left = new PythonIndicator(CreatePyObjectIndicator(10));
110+
right = new PythonIndicator(CreatePyObjectIndicator(10));
111+
}
112+
else
113+
{
114+
left = new SimpleMovingAverage("SMA", 10);
115+
right = new SimpleMovingAverage("SMA", 10);
116+
}
105117
using (Py.GIL())
106118
{
107119
var testModule = PyModule.FromString("testModule",
@@ -123,25 +135,63 @@ def update_indicators(left, right, value_left, value_right):
123135
right.Update(IndicatorDataPoint(DateTime.Now, value_right))
124136
");
125137

126-
var createCompositeIndicator = testModule.GetAttr("create_composite_indicator");
127-
var updateIndicators = testModule.GetAttr("update_indicators");
138+
using var createCompositeIndicator = testModule.GetAttr("create_composite_indicator");
139+
using var updateIndicators = testModule.GetAttr("update_indicators");
128140

129-
var leftPy = left.ToPython();
130-
var rightPy = right.ToPython();
141+
using var leftPy = left.ToPython();
142+
using var rightPy = right.ToPython();
131143

132144
// Create composite indicator using Python logic
133-
var composite = createCompositeIndicator.Invoke(leftPy, rightPy, operation.ToPython());
145+
using var composite = createCompositeIndicator.Invoke(leftPy, rightPy, operation.ToPython());
134146

135147
// Update the indicator with sample values (left, right)
136148
updateIndicators.Invoke(leftPy, rightPy, leftValue.ToPython(), rightValue.ToPython());
137149

138150
// Verify composite indicator name and properties
139-
Assert.AreEqual($"COMPOSE({left.Name},{right.Name})", composite.GetAttr("Name").ToString());
140-
Assert.AreEqual(left, composite.GetAttr("Left").As<IndicatorBase>());
141-
Assert.AreEqual(right, composite.GetAttr("Right").As<IndicatorBase>());
151+
using var name = composite.GetAttr("Name");
152+
using var typeLeft = composite.GetAttr("Left");
153+
using var typeRight = composite.GetAttr("Right");
154+
Assert.AreEqual($"COMPOSE({left.Name},{right.Name})", name.ToString());
155+
Assert.AreEqual(left, typeLeft.As<IndicatorBase>());
156+
Assert.AreEqual(right, typeRight.As<IndicatorBase>());
142157

143158
// Validate the composite indicator computed value
144-
Assert.AreEqual(expectedValue, composite.GetAttr("Current").GetAttr("Value").As<decimal>());
159+
using var value = composite.GetAttr("Current").GetAttr("Value");
160+
Assert.AreEqual(expectedValue, value.As<decimal>());
161+
}
162+
}
163+
164+
165+
private static PyObject CreatePyObjectIndicator(int period)
166+
{
167+
using (Py.GIL())
168+
{
169+
var module = PyModule.FromString(
170+
"custom_indicator",
171+
@"
172+
from AlgorithmImports import *
173+
from collections import deque
174+
175+
class CustomSimpleMovingAverage(PythonIndicator):
176+
def __init__(self, period):
177+
self.Name = 'CustomSMA'
178+
self.Value = 0
179+
self.Period = period
180+
self.WarmUpPeriod = period
181+
self.queue = deque(maxlen=period)
182+
183+
def Update(self, input):
184+
self.queue.appendleft(input.Value)
185+
count = len(self.queue)
186+
self.Value = sum(self.queue) / count
187+
return count == self.queue.maxlen
188+
"
189+
);
190+
191+
var indicator = module.GetAttr("CustomSimpleMovingAverage")
192+
.Invoke(period.ToPython());
193+
194+
return indicator;
145195
}
146196
}
147197

0 commit comments

Comments
 (0)