Skip to content

Commit 88b32b8

Browse files
authored
Python Support for CompositeIndicator (#8598)
* Initial solution * Add python support for CompositeIndicator * Add unit tests and update CompositeIndicator * Add new test case * Update TryConvertToIndicator logic * Solve review comments * Resolve review comments * Remove unnessary case * Use GetIndicatorAsManagedObject instead of ConvertToIndicator * Add regression tests
1 parent fef5c8e commit 88b32b8

File tree

5 files changed

+317
-17
lines changed

5 files changed

+317
-17
lines changed
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
3+
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Collections.Generic;
18+
using QuantConnect.Data;
19+
using QuantConnect.Indicators;
20+
using QuantConnect.Interfaces;
21+
22+
namespace QuantConnect.Algorithm.CSharp
23+
{
24+
/// <summary>
25+
/// This algorithm tests the functionality of the CompositeIndicator,
26+
/// using either a lambda expression or a method reference.
27+
/// </summary>
28+
public class CompositeIndicatorWorksAsExpectedRegressionAlgorithm : QCAlgorithm, IRegressionAlgorithmDefinition
29+
{
30+
private CompositeIndicator _compositeMinDirect;
31+
private CompositeIndicator _compositeMinMethod;
32+
private bool _dataReceived;
33+
34+
public override void Initialize()
35+
{
36+
SetStartDate(2013, 10, 4);
37+
SetEndDate(2013, 10, 5);
38+
AddEquity("SPY", Resolution.Minute);
39+
40+
var closePrice = Identity("SPY", Resolution.Minute, Field.Close);
41+
var lowPrice = MIN("SPY", 420, Resolution.Minute, Field.Low);
42+
43+
_compositeMinDirect = new CompositeIndicator("CompositeMinDirect", closePrice, lowPrice, (l, r) => new IndicatorResult(Math.Min(l.Current.Value, r.Current.Value)));
44+
_compositeMinMethod = new CompositeIndicator("CompositeMinMethod", closePrice, lowPrice, Composer);
45+
46+
_dataReceived = false;
47+
}
48+
49+
private IndicatorResult Composer(IndicatorBase l, IndicatorBase r)
50+
{
51+
return new IndicatorResult(Math.Min(l.Current.Value, r.Current.Value));
52+
}
53+
54+
public override void OnData(Slice data)
55+
{
56+
_dataReceived = true;
57+
58+
if (_compositeMinDirect.Current.Value != _compositeMinMethod.Current.Value)
59+
{
60+
throw new RegressionTestException($"Values of indicators differ: {_compositeMinDirect.Current.Value} | {_compositeMinMethod.Current.Value}");
61+
}
62+
}
63+
64+
public override void OnEndOfAlgorithm()
65+
{
66+
if (!_dataReceived)
67+
{
68+
throw new RegressionTestException("No data was processed during the algorithm execution.");
69+
}
70+
}
71+
72+
/// <summary>
73+
/// This is used by the regression test system to indicate if the open source Lean repository has the required data to run this algorithm.
74+
/// </summary>
75+
public bool CanRunLocally { get; } = true;
76+
77+
/// <summary>
78+
/// This is used by the regression test system to indicate which languages this algorithm is written in.
79+
/// </summary>
80+
public List<Language> Languages { get; } = new() { Language.CSharp, Language.Python };
81+
82+
/// <summary>
83+
/// Data Points count of all timeslices of algorithm
84+
/// </summary>
85+
public long DataPoints => 795;
86+
87+
/// <summary>
88+
/// Data Points count of the algorithm history
89+
/// </summary>
90+
public int AlgorithmHistoryDataPoints => 0;
91+
92+
/// <summary>
93+
/// Final status of the algorithm
94+
/// </summary>
95+
public AlgorithmStatus AlgorithmStatus => AlgorithmStatus.Completed;
96+
97+
/// <summary>
98+
/// This is used by the regression test system to indicate what the expected statistics are from running the algorithm
99+
/// </summary>
100+
public Dictionary<string, string> ExpectedStatistics => new Dictionary<string, string>
101+
{
102+
{"Total Orders", "0"},
103+
{"Average Win", "0%"},
104+
{"Average Loss", "0%"},
105+
{"Compounding Annual Return", "0%"},
106+
{"Drawdown", "0%"},
107+
{"Expectancy", "0"},
108+
{"Start Equity", "100000"},
109+
{"End Equity", "100000"},
110+
{"Net Profit", "0%"},
111+
{"Sharpe Ratio", "0"},
112+
{"Sortino Ratio", "0"},
113+
{"Probabilistic Sharpe Ratio", "0%"},
114+
{"Loss Rate", "0%"},
115+
{"Win Rate", "0%"},
116+
{"Profit-Loss Ratio", "0"},
117+
{"Alpha", "0"},
118+
{"Beta", "0"},
119+
{"Annual Standard Deviation", "0"},
120+
{"Annual Variance", "0"},
121+
{"Information Ratio", "0"},
122+
{"Tracking Error", "0"},
123+
{"Treynor Ratio", "0"},
124+
{"Total Fees", "$0.00"},
125+
{"Estimated Strategy Capacity", "$0"},
126+
{"Lowest Capacity Asset", ""},
127+
{"Portfolio Turnover", "0%"},
128+
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
129+
};
130+
}
131+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
2+
# Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
7+
#
8+
# Unless required by applicable law or agreed to in writing, software
9+
# distributed under the License is distributed on an "AS IS" BASIS,
10+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
# See the License for the specific language governing permissions and
12+
# limitations under the License.
13+
14+
from AlgorithmImports import *
15+
16+
### <summary>
17+
### This algorithm tests the functionality of the CompositeIndicator
18+
### using either a lambda expression or a method reference.
19+
### </summary>
20+
class CompositeIndicatorWorksAsExpectedRegressionAlgorithm(QCAlgorithm):
21+
def initialize(self):
22+
self.set_start_date(2013, 10, 4)
23+
self.set_end_date(2013, 10, 5)
24+
self.add_equity("SPY", Resolution.MINUTE)
25+
close = self.identity("SPY", Resolution.MINUTE, Field.CLOSE)
26+
low = self.min("SPY", 420, Resolution.MINUTE, Field.LOW)
27+
self.composite_min_direct = CompositeIndicator("CompositeMinDirect", close, low, lambda l, r: IndicatorResult(min(l.current.value, r.current.value)))
28+
self.composite_min_method = CompositeIndicator("CompositeMinMethod", close, low, self.composer)
29+
30+
self.data_received = False
31+
32+
def composer(self, l, r):
33+
return IndicatorResult(min(l.current.value, r.current.value))
34+
35+
def on_data(self, data):
36+
self.data_received = True
37+
if self.composite_min_direct.current.value != self.composite_min_method.current.value:
38+
raise Exception(f"Values of indicators differ: {self.composite_min_direct.current.value} | {self.composite_min_method.current.value}")
39+
40+
def on_end_of_algorithm(self):
41+
if not self.data_received:
42+
raise Exception("No data was processed during the algorithm execution.")

Indicators/CompositeIndicator.cs

Lines changed: 41 additions & 4 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;
19+
using QuantConnect.Data.Market;
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();
@@ -84,6 +88,7 @@ public CompositeIndicator(string name, IndicatorBase left, IndicatorBase right,
8488
_composer = composer;
8589
Left = left;
8690
Right = right;
91+
Name ??= $"COMPOSE({Left.Name},{Right.Name})";
8792
ConfigureEventHandlers();
8893
}
8994

@@ -95,7 +100,39 @@ public CompositeIndicator(string name, IndicatorBase left, IndicatorBase right,
95100
/// <param name="right">The right indicator for the 'composer'</param>
96101
/// <param name="composer">Function used to compose the left and right indicators</param>
97102
public CompositeIndicator(IndicatorBase left, IndicatorBase right, IndicatorComposer composer)
98-
: this($"COMPOSE({left.Name},{right.Name})", left, right, composer)
103+
: this(null, left, right, composer)
104+
{ }
105+
106+
/// <summary>
107+
/// Initializes a new instance of <see cref="CompositeIndicator"/> using two indicators
108+
/// and a custom function.
109+
/// </summary>
110+
/// <param name="name">The name of the composite indicator.</param>
111+
/// <param name="left">The first indicator in the composition.</param>
112+
/// <param name="right">The second indicator in the composition.</param>
113+
/// <param name="handler">A Python function that processes the indicator values.</param>
114+
/// <exception cref="ArgumentException">
115+
/// Thrown if the provided left or right indicator is not a valid QuantConnect Indicator object.
116+
/// </exception>
117+
public CompositeIndicator(string name, PyObject left, PyObject right, PyObject handler)
118+
: this(
119+
name,
120+
(IndicatorBase)left.GetIndicatorAsManagedObject(),
121+
(IndicatorBase)right.GetIndicatorAsManagedObject(),
122+
new IndicatorComposer(handler.ConvertToDelegate<Func<IndicatorBase, IndicatorBase, IndicatorResult>>())
123+
)
124+
{
125+
}
126+
127+
/// <summary>
128+
/// Initializes a new instance of <see cref="CompositeIndicator"/> using two indicators
129+
/// and a custom function.
130+
/// </summary>
131+
/// <param name="left">The first indicator in the composition.</param>
132+
/// <param name="right">The second indicator in the composition.</param>
133+
/// <param name="handler">A Python function that processes the indicator values.</param>
134+
public CompositeIndicator(PyObject left, PyObject right, PyObject handler)
135+
: this(null, left, right, handler)
99136
{ }
100137

101138
/// <summary>
@@ -130,8 +167,8 @@ protected override decimal ComputeNextValue(IndicatorDataPoint _)
130167
private void ConfigureEventHandlers()
131168
{
132169
// 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<>));
170+
bool leftIsConstant = Left.GetType().IsSubclassOfGeneric(typeof(ConstantIndicator<>));
171+
bool rightIsConstant = Right.GetType().IsSubclassOfGeneric(typeof(ConstantIndicator<>));
135172

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

Indicators/IndicatorExtensions.cs

Lines changed: 13 additions & 11 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,7 @@ 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);
283285
}
284286

285287
/// <summary>Creates a new ExponentialMovingAverage indicator with the specified period and smoothingFactor from the left indicator
@@ -418,7 +420,7 @@ public static SimpleMovingAverage SMA(PyObject left, int period, bool waitForFir
418420
return SMA(indicator, period, waitForFirstToReady);
419421
}
420422

421-
/// <summary>
423+
/// <summary>
422424
/// Creates a new CompositeIndicator such that the result will be the ratio of the left to the constant
423425
/// </summary>
424426
/// <remarks>
@@ -562,7 +564,7 @@ public static object Plus(PyObject left, PyObject right, string name = "")
562564
return Plus(indicatorLeft, indicatorRight, name);
563565
}
564566

565-
private static dynamic GetIndicatorAsManagedObject(PyObject indicator)
567+
internal static dynamic GetIndicatorAsManagedObject(this PyObject indicator)
566568
{
567569
if (indicator.TryConvert(out PythonIndicator pythonIndicator, true))
568570
{

0 commit comments

Comments
 (0)