Skip to content

Commit 160575b

Browse files
committed
Improve PythonIndicator to support both legacy and new custom python indicators
1 parent 3a8e48a commit 160575b

File tree

5 files changed

+105
-29
lines changed

5 files changed

+105
-29
lines changed

Algorithm/QCAlgorithm.Python.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2046,7 +2046,7 @@ private PythonIndicator WrapPythonIndicator(PyObject pyObject, PythonIndicator c
20462046

20472047
if (pythonIndicator == null)
20482048
{
2049-
pythonIndicator = new PythonIndicator(pyObject, false);
2049+
pythonIndicator = new PythonIndicator(pyObject);
20502050
}
20512051
else
20522052
{

Indicators/PythonIndicator.cs

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@ public class PythonIndicator : IndicatorBase<IBaseData>, IIndicatorWarmUpPeriodP
2929
private PyObject _instance;
3030
private bool _isReady;
3131
private bool _pythonIsReadyProperty;
32-
private bool _useNewInitialization;
33-
private bool _isInstanceSet;
32+
private bool _isLegacyMode;
3433
private BasePythonWrapper<IIndicator> _indicatorWrapper;
3534

3635
/// <summary>
@@ -55,12 +54,23 @@ public PythonIndicator(params PyObject[] args)
5554
/// Initializes a new instance of the PythonIndicator class using the specified name.
5655
/// </summary>
5756
/// <param name="indicator">The python implementation of <see cref="IndicatorBase{IBaseDataBar}"/></param>
58-
/// <param name="useNewInitialization">Whether to use the new initialization method</param>
59-
public PythonIndicator(PyObject indicator, bool useNewInitialization = true)
57+
public PythonIndicator(PyObject indicator)
6058
: base(GetIndicatorName(indicator))
6159
{
62-
_useNewInitialization = useNewInitialization;
63-
_instance = indicator;
60+
SetInstance(indicator);
61+
_isLegacyMode = false;
62+
63+
// Check if the instance has the method ComputeNextValue
64+
// If not, we need to use the legacy mode
65+
if (_indicatorWrapper.GetMethod(nameof(ComputeNextValue), true) == null)
66+
{
67+
_isLegacyMode = true;
68+
}
69+
70+
if (_isLegacyMode)
71+
{
72+
SetIndicator(indicator);
73+
}
6474
}
6575

6676
/// <summary>
@@ -69,10 +79,8 @@ public PythonIndicator(PyObject indicator, bool useNewInitialization = true)
6979
/// <param name="indicator">The python implementation of <see cref="IndicatorBase{IBaseDataBar}"/></param>
7080
public void SetIndicator(PyObject indicator)
7181
{
72-
_instance = indicator;
73-
_indicatorWrapper = new BasePythonWrapper<IIndicator>(indicator, validateInterface: false);
74-
var requiredAttributes = new[] { "IsReady", _useNewInitialization ? "ComputeNextValue" : "Update", "Value" };
75-
82+
SetInstance(indicator);
83+
var requiredAttributes = new[] { "IsReady", "Update", "Value" };
7684
foreach (var attributeName in requiredAttributes)
7785
{
7886
if (!_indicatorWrapper.HasAttr(attributeName))
@@ -99,17 +107,15 @@ public void SetIndicator(PyObject indicator)
99107
}
100108
}
101109
WarmUpPeriod = GetIndicatorWarmUpPeriod();
102-
_isInstanceSet = true;
103110
}
104111

105-
private bool CheckInstance()
112+
private void SetInstance(PyObject instance)
106113
{
107-
if (_instance != null && !_isInstanceSet)
114+
if (_instance == null)
108115
{
109-
SetIndicator(_instance);
116+
_instance = instance;
117+
_indicatorWrapper = new BasePythonWrapper<IIndicator>(instance, validateInterface: false);
110118
}
111-
112-
return _isInstanceSet;
113119
}
114120

115121
/// <summary>
@@ -150,17 +156,16 @@ public override bool IsReady
150156
/// <returns>A new value for this indicator</returns>
151157
protected override decimal ComputeNextValue(IBaseData input)
152158
{
153-
CheckInstance();
154-
if (_useNewInitialization)
155-
{
156-
return _indicatorWrapper.InvokeMethod<decimal>("ComputeNextValue", input);
157-
}
158-
else
159+
if (_isLegacyMode)
159160
{
160161
_isReady = _indicatorWrapper.InvokeMethod<bool?>(nameof(Update), input)
161162
?? _indicatorWrapper.GetProperty<bool>(nameof(IsReady));
162163
return _indicatorWrapper.GetProperty<decimal>("Value");
163164
}
165+
else
166+
{
167+
return _indicatorWrapper.InvokeMethod<decimal>(nameof(ComputeNextValue), input);
168+
}
164169
}
165170

166171
/// <summary>

Tests/Indicators/PythonIndicatorNoinheritanceTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def __init__(self, name, period):
6767
var indicator = module.GetAttr("CustomSimpleMovingAverage")
6868
.Invoke("custom".ToPython(), 14.ToPython());
6969

70-
return new PythonIndicator(indicator, false);
70+
return new PythonIndicator(indicator);
7171
}
7272
}
7373

Tests/Indicators/PythonIndicatorNoinheritanceTestsLegacy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def __init__(self, name, period):
6666
var indicator = module.GetAttr("CustomSimpleMovingAverage")
6767
.Invoke("custom".ToPython(), 14.ToPython());
6868

69-
return new PythonIndicator(indicator, false);
69+
return new PythonIndicator(indicator);
7070
}
7171
}
7272

Tests/Indicators/PythonIndicatorTests.cs

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def __init__(self, name, period):
8080

8181
protected override IndicatorBase<IBaseData> CreateIndicator()
8282
{
83-
return new PythonIndicator(CreatePythonIndicator(), false);
83+
return new PythonIndicator(CreatePythonIndicator());
8484
}
8585

8686
protected override string TestFileName => "spy_with_indicators.txt";
@@ -362,7 +362,7 @@ def Update(self, input):
362362
);
363363
var pythonIndicator = module.GetAttr("CustomSimpleMovingAverage")
364364
.Invoke("custom".ToPython(), 14.ToPython());
365-
var SMAWithWarmUpPeriod = new PythonIndicator(pythonIndicator, false);
365+
var SMAWithWarmUpPeriod = new PythonIndicator(pythonIndicator);
366366
var reference = new DateTime(2000, 1, 1, 0, 0, 0);
367367
var period = ((IIndicatorWarmUpPeriodProvider)SMAWithWarmUpPeriod).WarmUpPeriod;
368368

@@ -404,7 +404,7 @@ def Update(self, input):
404404
);
405405
var pythonIndicator = module.GetAttr("CustomSimpleMovingAverage")
406406
.Invoke("custom".ToPython(), 14.ToPython());
407-
var indicator = new PythonIndicator(pythonIndicator, false);
407+
var indicator = new PythonIndicator(pythonIndicator);
408408

409409
Assert.AreEqual(0, indicator.WarmUpPeriod);
410410
}
@@ -421,7 +421,7 @@ public void PythonIndicatorDoesntRequireWrappingToWork()
421421
using (Py.GIL())
422422
{
423423
using dynamic customSma = CreatePythonIndicator(period);
424-
var wrapper = new PythonIndicator(customSma, false);
424+
var wrapper = new PythonIndicator(customSma);
425425

426426
for (int i = 0; i < data.Length; i++)
427427
{
@@ -513,5 +513,76 @@ public override void AcceptsRenkoBarsAsInput()
513513
public override void AcceptsVolumeRenkoBarsAsInput()
514514
{
515515
}
516+
517+
[Test]
518+
public void UpdatedEventFiresCorrectlyWithCustomPythonIndicator()
519+
{
520+
using (Py.GIL())
521+
{
522+
var module = PyModule.FromString(
523+
Guid.NewGuid().ToString(),
524+
$@"
525+
from AlgorithmImports import *
526+
from collections import deque
527+
528+
class CustomSimpleMovingAverage(PythonIndicator):
529+
def __init__(self, name, period):
530+
super().__init__(self)
531+
self.name = name
532+
self.value = 0
533+
self.period = period
534+
self.warm_up_period = period
535+
self.queue = deque(maxlen=period)
536+
537+
@property
538+
def is_ready(self):
539+
return len(self.queue) >= self.period
540+
541+
# compute_next_value method is mandatory
542+
def compute_next_value(self, input):
543+
self.queue.appendleft(input.Value)
544+
count = len(self.queue)
545+
self.value = np.sum(self.queue) / count
546+
return self.value
547+
548+
class IndicatorUpdater:
549+
def __init__(self, period=3):
550+
self.count = 0
551+
self.indicator = CustomSimpleMovingAverage('SMA', period)
552+
self.indicator.updated += self._on_update
553+
554+
def _on_update(self, sender, consolidated):
555+
self.count += 1
556+
557+
def update_indicator(self):
558+
bar1 = TradeBar()
559+
bar1.value = 1
560+
bar2 = TradeBar()
561+
bar2.value = 2
562+
bar3 = TradeBar()
563+
bar3.value = 3
564+
bar4 = TradeBar()
565+
bar4.value = 4
566+
self.indicator.update(bar1)
567+
self.indicator.update(bar2)
568+
self.indicator.update(bar3)
569+
self.indicator.update(bar4)
570+
571+
def get_indicator_status(self):
572+
return self.indicator.is_ready
573+
"
574+
);
575+
var period = 3;
576+
dynamic updater = module.GetAttr("IndicatorUpdater")
577+
.Invoke(period.ToPython());
578+
updater.update_indicator();
579+
var count = updater.count.As<int>();
580+
var isReady = updater.get_indicator_status().As<bool>();
581+
var indicatorValue = updater.indicator.value.As<decimal>();
582+
Assert.AreEqual(4, count);
583+
Assert.IsTrue(isReady);
584+
Assert.AreEqual(3.0m, indicatorValue);
585+
}
586+
}
516587
}
517588
}

0 commit comments

Comments
 (0)