Skip to content

Commit 7de315b

Browse files
committed
Solve review comments
1 parent abfc437 commit 7de315b

File tree

3 files changed

+172
-11
lines changed

3 files changed

+172
-11
lines changed

Common/Util/PythonUtil.cs

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
using QuantConnect.Data.Fundamental;
2222
using System.Text.RegularExpressions;
2323
using QuantConnect.Data.UniverseSelection;
24-
using System.IO;
2524
using System.Globalization;
25+
using System.Reflection;
2626

2727
namespace QuantConnect.Util
2828
{
@@ -365,24 +365,35 @@ public static IEnumerable<Symbol> ConvertToSymbols(PyObject input)
365365
/// <summary>
366366
/// Creates either a pure C# model instance or a Python wrapper based on the input PyObject
367367
/// </summary>
368-
/// <typeparam name="TInterface">The interface type expected</typeparam>
368+
/// <typeparam name="T">The interface type expected</typeparam>
369369
/// <typeparam name="TWrapper">The Python wrapper type for TInterface</typeparam>
370370
/// <param name="pyObject">The Python object to convert</param>
371371
/// <returns>Either a pure C# instance or a Python wrapper implementing TInterface</returns>
372-
public static TInterface CreateModelOrWrapper<TInterface, TWrapper>(PyObject pyObject)
373-
where TInterface : class
374-
where TWrapper : TInterface
372+
public static T CreateModelOrWrapper<T, TWrapper>(PyObject pyObject)
373+
where T : class
374+
where TWrapper : T
375375
{
376376
using (Py.GIL())
377377
{
378378
// This is a pure C# object
379-
if (pyObject.TryConvert<TInterface>(out var model))
379+
if (pyObject.TryConvert<T>(out var model))
380380
{
381381
return model;
382382
}
383383

384384
// Create the appropriate Python wrapper
385-
return (TInterface)Activator.CreateInstance(typeof(TWrapper), pyObject);
385+
try
386+
{
387+
return (T)Activator.CreateInstance(typeof(TWrapper), pyObject);
388+
}
389+
catch (TargetInvocationException ex)
390+
{
391+
if (ex.InnerException != null)
392+
{
393+
throw ex.InnerException;
394+
}
395+
throw;
396+
}
386397
}
387398
}
388399
}

Tests/Common/Util/PythonUtilTests.cs

Lines changed: 153 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@
1313
* limitations under the License.
1414
*/
1515

16-
using System.Linq;
17-
using Python.Runtime;
1816
using NUnit.Framework;
17+
using Python.Runtime;
18+
using QuantConnect.Python;
19+
using QuantConnect.Securities;
1920
using QuantConnect.Util;
21+
using System;
2022
using System.Collections.Generic;
23+
using System.Linq;
2124

2225
namespace QuantConnect.Tests.Common.Util
2326
{
@@ -88,7 +91,7 @@ public void ConvertToSymbolsTest()
8891
Assert.AreEqual(expected.FirstOrDefault(), test1.FirstOrDefault());
8992

9093
// Test Python List of Strings
91-
var list = (new List<string> {"AIG", "BAC", "IBM", "GOOG"}).ToPyList();
94+
var list = (new List<string> { "AIG", "BAC", "IBM", "GOOG" }).ToPyList();
9295
var test2 = PythonUtil.ConvertToSymbols(list);
9396
Assert.IsTrue(typeof(List<Symbol>) == test2.GetType());
9497
Assert.IsTrue(test2.SequenceEqual(expected));
@@ -231,5 +234,152 @@ public void ParsesPythonExceptionStackTrace(string expected, string original, in
231234
PythonUtil.ExceptionLineShift = originalShiftValue;
232235
Assert.AreEqual(expected, result);
233236
}
237+
238+
[Test]
239+
public void PurePythonClassWithRequiredMethodsWorks()
240+
{
241+
using (Py.GIL())
242+
{
243+
// Pure Python class that implements the required methods
244+
string pythonCode = @"
245+
from AlgorithmImports import *
246+
247+
class PurePythonBuyingPowerModel:
248+
def GetMaximumOrderQuantityForTargetBuyingPower(self, parameters):
249+
return GetMaximumOrderQuantityResult(100)
250+
251+
def GetMaximumOrderQuantityForDeltaBuyingPower(self, parameters):
252+
return GetMaximumOrderQuantityResult(200)
253+
254+
def HasSufficientBuyingPowerForOrder(self, parameters):
255+
return HasSufficientBuyingPowerForOrderResult(True)
256+
257+
def GetReservedBuyingPowerForPosition(self, parameters):
258+
return ReservedBuyingPowerForPosition(0)
259+
260+
def GetLeverage(self, security):
261+
return 1.0
262+
263+
def GetBuyingPower(self, parameters):
264+
return BuyingPower(1000)
265+
266+
def SetLeverage(self, security, leverage):
267+
pass
268+
269+
def GetMaintenanceMargin(self, parameters):
270+
return None
271+
272+
def GetInitialMarginRequirement(self, parameters):
273+
return None
274+
275+
def GetInitialMarginRequiredForOrder(self, parameters):
276+
return None
277+
";
278+
var module = PyModule.FromString("TestModels", pythonCode);
279+
var pyObject = module.GetAttr("PurePythonBuyingPowerModel").Invoke();
280+
281+
var result = PythonUtil.CreateModelOrWrapper<IBuyingPowerModel, BuyingPowerModelPythonWrapper>(pyObject);
282+
283+
Assert.IsNotNull(result);
284+
Assert.IsInstanceOf<BuyingPowerModelPythonWrapper>(result);
285+
}
286+
}
287+
288+
[Test]
289+
public void BothInheritedAndNonInheritedClassesWork()
290+
{
291+
using (Py.GIL())
292+
{
293+
string pythonCode = @"
294+
from AlgorithmImports import *
295+
296+
class PurePythonBuyingPowerModel:
297+
def GetMaximumOrderQuantityForTargetBuyingPower(self, parameters):
298+
return GetMaximumOrderQuantityResult(100)
299+
300+
def GetMaximumOrderQuantityForDeltaBuyingPower(self, parameters):
301+
return GetMaximumOrderQuantityResult(200)
302+
303+
def HasSufficientBuyingPowerForOrder(self, parameters):
304+
return HasSufficientBuyingPowerForOrderResult(True)
305+
306+
def GetReservedBuyingPowerForPosition(self, parameters):
307+
return ReservedBuyingPowerForPosition(0)
308+
309+
def GetLeverage(self, security):
310+
return 1.0
311+
312+
def GetBuyingPower(self, parameters):
313+
return BuyingPower(1000)
314+
315+
def SetLeverage(self, security, leverage):
316+
pass
317+
318+
def GetMaintenanceMargin(self, parameters):
319+
return None
320+
321+
def GetInitialMarginRequirement(self, parameters):
322+
return None
323+
324+
def GetInitialMarginRequiredForOrder(self, parameters):
325+
return None
326+
327+
class InheritedBuyingPowerModel(SecurityMarginModel):
328+
def GetMaximumOrderQuantityForTargetBuyingPower(self, parameters):
329+
return GetMaximumOrderQuantityResult(200)
330+
";
331+
var module = PyModule.FromString("TestModels", pythonCode);
332+
var purePython = module.GetAttr("PurePythonBuyingPowerModel").Invoke();
333+
var inherited = module.GetAttr("InheritedBuyingPowerModel").Invoke();
334+
335+
var result1 = PythonUtil.CreateModelOrWrapper<IBuyingPowerModel, BuyingPowerModelPythonWrapper>(purePython);
336+
var result2 = PythonUtil.CreateModelOrWrapper<IBuyingPowerModel, BuyingPowerModelPythonWrapper>(inherited);
337+
338+
Assert.IsNotNull(result1);
339+
Assert.IsNotNull(result2);
340+
Assert.IsInstanceOf<BuyingPowerModelPythonWrapper>(result1);
341+
Assert.IsInstanceOf<BuyingPowerModelPythonWrapper>(result2);
342+
}
343+
}
344+
345+
[Test]
346+
public void MissingRequiredMethodThrowsException()
347+
{
348+
using (Py.GIL())
349+
{
350+
string pythonCode = @"
351+
class IncompleteBuyingPowerModel:
352+
def GetMaximumOrderQuantityForTargetBuyingPower(self, parameters):
353+
return GetMaximumOrderQuantityResult(100)
354+
355+
def HasSufficientBuyingPowerForOrder(self, parameters):
356+
return HasSufficientBuyingPowerForOrderResult(True)
357+
";
358+
var module = PyModule.FromString("TestModels", pythonCode);
359+
var purePython = module.GetAttr("IncompleteBuyingPowerModel").Invoke();
360+
361+
Assert.Throws<NotImplementedException>(() =>
362+
PythonUtil.CreateModelOrWrapper<IBuyingPowerModel, BuyingPowerModelPythonWrapper>(purePython));
363+
}
364+
}
365+
366+
[Test]
367+
public void PureCSharpClassReturnsDirectInstanceWithoutWrapper()
368+
{
369+
using (Py.GIL())
370+
{
371+
// Create pure C# instance
372+
var csharpObject = new BuyingPowerModel();
373+
var pyObject = csharpObject.ToPython();
374+
375+
// Should return the same C# instance, not a wrapper
376+
var result = PythonUtil.CreateModelOrWrapper<IBuyingPowerModel, BuyingPowerModelPythonWrapper>(pyObject);
377+
378+
Assert.IsNotNull(result);
379+
Assert.AreSame(csharpObject, result);
380+
Assert.IsNotInstanceOf<BuyingPowerModelPythonWrapper>(result);
381+
Assert.IsInstanceOf<BuyingPowerModel>(result);
382+
}
383+
}
234384
}
235385
}

Tests/Python/SecurityCustomModelTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ public void SetBuyingPowerModelFails()
7171
var code = CreateCustomBuyingPowerModelCode();
7272
code = code.Replace("GetMaximumOrderQuantityForDeltaBuyingPower", "AnotherName");
7373
var pyObject = CreateCustomBuyingPowerModel(code);
74-
Assert.Throws<TargetInvocationException>(() => spy.SetBuyingPowerModel(pyObject));
74+
Assert.Throws<NotImplementedException>(() => spy.SetBuyingPowerModel(pyObject));
7575
}
7676

7777
private PyObject CreateCustomBuyingPowerModel(string code)

0 commit comments

Comments
 (0)