Files
2026-07-13 13:02:50 +08:00

347 lines
16 KiB
C#

/*
* QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
* Lean Algorithmic Trading Engine v2.0. Copyright 2014 QuantConnect Corporation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Python;
using QuantConnect.Securities;
using QuantConnect.Util;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Tests.Common.Util
{
[TestFixture]
public class PythonUtilTests
{
[TestCase(false)]
[TestCase(true)]
public void ToActionFailure(bool typeAnnotations)
{
using (Py.GIL())
{
var action = PyModule.FromString("ToAction", @"
from AlgorithmImports import *
def Test1():
pass
def Test2() -> None:
pass
");
var testMethod = action.GetAttr(typeAnnotations ? "Test2" : "Test1");
var result = PythonUtil.ToAction<SecurityType>(testMethod);
Assert.IsNull(result);
}
}
[TestCase(false)]
[TestCase(true)]
public void ToActionSuccess(bool typeAnnotations)
{
using (Py.GIL())
{
var action = PyModule.FromString("ToAction", @"
from AlgorithmImports import *
def Test1(securityType):
if securityType != SecurityType.Equity:
raise ValueError('Unexpected SecurityType!')
def Test2(securityType: SecurityType) -> None:
if securityType != SecurityType.Equity:
raise ValueError('Unexpected SecurityType!')
");
var testMethod = action.GetAttr(typeAnnotations ? "Test2" : "Test1");
var result = PythonUtil.ToAction<SecurityType>(testMethod);
Assert.IsNotNull(action);
Assert.DoesNotThrow(() => result(SecurityType.Equity));
}
}
[Test]
public void ConvertToSymbolsTest()
{
var expected = new List<Symbol>
{
Symbol.Create("AIG", SecurityType.Equity, Market.USA),
Symbol.Create("BAC", SecurityType.Equity, Market.USA),
Symbol.Create("IBM", SecurityType.Equity, Market.USA),
Symbol.Create("GOOG", SecurityType.Equity, Market.USA)
};
using (Py.GIL())
{
// Test Python String
using var pyString = new PyString("AIG");
var test1 = PythonUtil.ConvertToSymbols(pyString);
Assert.IsTrue(typeof(List<Symbol>) == test1.GetType());
Assert.AreEqual(expected.FirstOrDefault(), test1.FirstOrDefault());
// Test Python List of Strings
var list = (new List<string> { "AIG", "BAC", "IBM", "GOOG" }).ToPyList();
var test2 = PythonUtil.ConvertToSymbols(list);
Assert.IsTrue(typeof(List<Symbol>) == test2.GetType());
Assert.IsTrue(test2.SequenceEqual(expected));
// Test Python Symbol
var test3 = PythonUtil.ConvertToSymbols(expected.FirstOrDefault().ToPython());
Assert.IsTrue(typeof(List<Symbol>) == test3.GetType());
Assert.AreEqual(expected.FirstOrDefault(), test3.FirstOrDefault());
// Test Python List of Symbols
var test4 = PythonUtil.ConvertToSymbols(expected.ToPyList());
Assert.IsTrue(typeof(List<Symbol>) == test4.GetType());
Assert.IsTrue(test4.SequenceEqual(expected));
}
}
[TestCase("SyntaxError : invalid syntax (BasicTemplateAlgorithm.py, line 33)", "SyntaxError : invalid syntax (BasicTemplateAlgorithm.py, line 32)", 1)]
[TestCase("SyntaxError : invalid syntax (BasicTemplateAlgorithm.py, line 1)", "SyntaxError : invalid syntax (BasicTemplateAlgorithm.py, line 32)", -31)]
[TestCase("NameError : name 's' is not defined", "NameError : name 's' is not defined", -31)]
public void ParsesPythonExceptionMessage(string expected, string original, int shift)
{
var originalShiftValue = PythonUtil.ExceptionLineShift;
PythonUtil.ExceptionLineShift = shift;
var result = PythonUtil.PythonExceptionMessageParser(original);
PythonUtil.ExceptionLineShift = originalShiftValue;
Assert.AreEqual(expected, result);
}
[TestCase(@"
at Initialize
s
===
in BasicTemplateAlgorithm.py: line 20
",
@" File ""D:/QuantConnect/MyLean/Lean/Algorithm.Python\BasicTemplateAlgorithm.py"", line 30, in Initialize
s
===
at Python.Runtime.PyObject.Invoke(PyTuple args, PyDict kw)
at Python.Runtime.PyObject.InvokeMethod(String name, PyTuple args, PyDict kw)
at Python.Runtime.PyObject.TryInvokeMember(InvokeMemberBinder binder, Object[] args, Object& result)
at CallSite.Target(Closure , CallSite , Object )
at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid1[T0](CallSite site, T0 arg0)
at QuantConnect.AlgorithmFactory.Python.Wrappers.AlgorithmPythonWrapper.Initialize() in D:\QuantConnect\MyLean\Lean\AlgorithmFactory\Python\Wrappers\AlgorithmPythonWrapper.cs:line 528
at QuantConnect.Lean.Engine.Setup.BacktestingSetupHandler.<>c__DisplayClass27_0.<Setup>b__0() in D:\QuantConnect\MyLean\Lean\Engine\Setup\BacktestingSetupHandler.cs:line 186",
-10)]
[TestCase(@"
at Initialize
self.SetEndDate(201, 1)
===
in BasicTemplateAlgorithm.py: line 40
",
@" File ""D:/QuantConnect/MyLean/Lean/Algorithm.Python\BasicTemplateAlgorithm.py"", line 30, in Initialize
self.SetEndDate(201, 1)
===
at Python.Runtime.PyObject.Invoke(PyTuple args, PyDict kw)
at Python.Runtime.PyObject.InvokeMethod(String name, PyTuple args, PyDict kw)
at Python.Runtime.PyObject.TryInvokeMember(InvokeMemberBinder binder, Object[] args, Object& result)
at CallSite.Target(Closure , CallSite , Object )
at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid1[T0](CallSite site, T0 arg0)
at QuantConnect.AlgorithmFactory.Python.Wrappers.AlgorithmPythonWrapper.Initialize() in D:\QuantConnect\MyLean\Lean\AlgorithmFactory\Python\Wrappers\AlgorithmPythonWrapper.cs:line 528
at QuantConnect.Lean.Engine.Setup.BacktestingSetupHandler.<>c__DisplayClass27_0.<Setup>b__0() in D:\QuantConnect\MyLean\Lean\Engine\Setup\BacktestingSetupHandler.cs:line 186",
10)]
[TestCase(@"
at <module>
class BasicTemplateAlgorithm(QCAlgorithm):
in BasicTemplateAlgorithm.py: line 23
",
@" File ""D:/QuantConnect/MyLean/Lean/Algorithm.Python\BasicTemplateAlgorithm.py"", line 23, in <module>
class BasicTemplateAlgorithm(QCAlgorithm):
at Python.Runtime.PythonException.ThrowLastAsClrException()
at Python.Runtime.NewReferenceExtensions.BorrowOrThrow(NewReference& reference)
at Python.Runtime.PyModule.Import(String name)
at Python.Runtime.Py.Import(String name)
at QuantConnect.AlgorithmFactory.Python.Wrappers.AlgorithmPythonWrapper..ctor(String moduleName) in D:\QuantConnect\MyLean\Lean\AlgorithmFactory\Python\Wrappers\AlgorithmPythonWrapper.cs:line 74",
0)]
[TestCase(@"
at wrapped_function
raise KeyError(f""No key found for either mapped or original key. Mapped Key: {mKey}; Original Key: {oKey}"")
in PandasMapper.py: line 76
at HistoryCall
history.loc['QQQ'] # <--- raises Key Error
in TestAlgorithm.py: line 27
at OnData
self.HistoryCall()
in TestAlgorithm.py: line 23
",
@" File ""/home/user/QuantConnect/Lean/Launcher/bin/Debug/PandasMapper.py"", line 76, in wrapped_function
raise KeyError(f""No key found for either mapped or original key. Mapped Key: {mKey}; Original Key: {oKey}"")
File ""/home/user/QuantConnect/Lean/Algorithm.Python/TestAlgorithm.py"", line 27, in HistoryCall
history.loc['QQQ'] # <--- raises Key Error
File ""/home/user/QuantConnect/Lean/Algorithm.Python/TestAlgorithm.py"", line 23, in OnData
self.HistoryCall()
at Python.Runtime.PythonException.ThrowLastAsClrException()
at Python.Runtime.PyObject.Invoke(PyTuple args, PyDict kw)
at Python.Runtime.PyObject.TryInvoke(InvokeBinder binder, Object[] args, Object& result)
at CallSite.Target(Closure , CallSite , Object , PythonSlice )
at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite site, T0 arg0, T1 arg1)
at QuantConnect.AlgorithmFactory.Python.Wrappers.AlgorithmPythonWrapper.OnData(Slice slice) in /home/user/QuantConnect/Lean/AlgorithmFactory/Python/Wrappers/AlgorithmPythonWrapper.cs:line 587
at QuantConnect.Lean.Engine.AlgorithmManager.Run(AlgorithmNodePacket job, IAlgorithm algorithm, ISynchronizer synchronizer, ITransactionHandler transactions, IResultHandler results, IRealTimeHandler realtime, ILeanManager leanManager, IAlphaHandler alphas, CancellationToken token) in /home/user/QuantConnect/Lean/Engine/AlgorithmManager.cs:line 523",
0)]
[TestCase(@"
at OnData
raise ValueError(""""ASD"""")
in BasicTemplateAlgorithm.py: line 43
",
@" File ""D:\QuantConnect/MyLean/Lean/Algorithm.Python\BasicTemplateAlgorithm.py"", line 43, in OnData
raise ValueError(""""ASD"""")
at Python.Runtime.PythonException.ThrowLastAsClrException() in D:\QuantConnect\MyLean\pythonnet\src\runtime\PythonException.cs:line 52
at Python.Runtime.PyObject.Invoke(PyTuple args, PyDict kw) in D:\QuantConnect\MyLean\pythonnet\src\runtime\PythonTypes\PyObject.cs:line 837
at Python.Runtime.PyObject.TryInvoke(InvokeBinder binder, Object[] args, Object& result) in D:\QuantConnect\MyLean\pythonnet\src\runtime\PythonTypes\PyObject.cs:line 1320
at CallSite.Target(Closure , CallSite , Object , PythonSlice )
at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite site, T0 arg0, T1 arg1)
at QuantConnect.AlgorithmFactory.Python.Wrappers.AlgorithmPythonWrapper.OnData(Slice slice) in D:\QuantConnect\MyLean\Lean\AlgorithmFactory\Python\Wrappers\AlgorithmPythonWrapper.cs:line 693
at QuantConnect.Lean.Engine.AlgorithmManager.Run(AlgorithmNodePacket job, IAlgorithm algorithm, ISynchronizer synchronizer, ITransactionHandler transactions, IResultHandler results, IRealTimeHandler realtime, ILeanManager leanManager, CancellationToken token) in D:\QuantConnect\MyLean\Lean\Engine\AlgorithmManager.cs:line 526",
0)]
[TestCase(@"
at on_data
self.set_holdings(""SPY"", 1 / None)
~~^~~~~~
in BasicTemplateAlgorithm.py: line 46
",
@" File ""C:\Users/user/QuantConnect/Lean/Algorithm.Python\BasicTemplateAlgorithm.py"", line 46, in on_data
self.set_holdings(""SPY"", 1 / None)
~~^~~~~~
at Python.Runtime.PythonException.ThrowLastAsClrException()
at Python.Runtime.PyObject.Invoke(PyTuple args, PyDict kw)
at Python.Runtime.PyObject.TryInvoke(InvokeBinder binder, Object[] args, Object& result)
at CallSite.Target(Closure , CallSite , Object , Slice )
at System.Dynamic.UpdateDelegates.UpdateAndExecuteVoid2[T0,T1](CallSite site, T0 arg0, T1 arg1)
at QuantConnect.AlgorithmFactory.Python.Wrappers.AlgorithmPythonWrapper.OnData(Slice slice) in C:\Users\user\QuantConnect\Lean\AlgorithmFactory\Python\Wrappers\AlgorithmPythonWrapper.cs:line 759
at QuantConnect.Lean.Engine.AlgorithmManager.Run(AlgorithmNodePacket job, IAlgorithm algorithm, ISynchronizer synchronizer, ITransactionHandler transactions, IResultHandler results, IRealTimeHandler realtime, ILeanManager leanManager, CancellationToken token) in C:\Users\user\QuantConnect\Lean\Engine\AlgorithmManager.cs:line 524",
0)]
public void ParsesPythonExceptionStackTrace(string expected, string original, int shift)
{
var originalShiftValue = PythonUtil.ExceptionLineShift;
PythonUtil.ExceptionLineShift = shift;
var result = PythonUtil.PythonExceptionStackParser(original);
PythonUtil.ExceptionLineShift = originalShiftValue;
Assert.AreEqual(expected, result);
}
[TestCase(true)]
[TestCase(false)]
public void BothInheritedAndNonInheritedClassesWork(bool inherited)
{
using (Py.GIL())
{
string pythonCode = @"
from AlgorithmImports import *
class PurePythonBuyingPowerModel:
def GetMaximumOrderQuantityForTargetBuyingPower(self, parameters):
return GetMaximumOrderQuantityResult(100)
def GetMaximumOrderQuantityForDeltaBuyingPower(self, parameters):
return GetMaximumOrderQuantityResult(200)
def HasSufficientBuyingPowerForOrder(self, parameters):
return HasSufficientBuyingPowerForOrderResult(True)
def GetReservedBuyingPowerForPosition(self, parameters):
return ReservedBuyingPowerForPosition(0)
def GetLeverage(self, security):
return 1.0
def GetBuyingPower(self, parameters):
return BuyingPower(1000)
def SetLeverage(self, security, leverage):
pass
def GetMaintenanceMargin(self, parameters):
return None
def GetInitialMarginRequirement(self, parameters):
return None
def GetInitialMarginRequiredForOrder(self, parameters):
return None
class InheritedBuyingPowerModel(SecurityMarginModel):
def GetMaximumOrderQuantityForTargetBuyingPower(self, parameters):
return GetMaximumOrderQuantityResult(200)
";
var module = PyModule.FromString("TestModels", pythonCode);
PyObject pyObject = null;
if (inherited)
{
pyObject = module.GetAttr("InheritedBuyingPowerModel").Invoke();
}
else
{
pyObject = module.GetAttr("PurePythonBuyingPowerModel").Invoke();
}
var result = PythonUtil.CreateInstanceOrWrapper<IBuyingPowerModel>(
pyObject,
py => new BuyingPowerModelPythonWrapper(py)
);
Assert.IsNotNull(result);
Assert.IsInstanceOf<BuyingPowerModelPythonWrapper>(result);
}
}
[Test]
public void MissingRequiredMethodThrowsException()
{
using (Py.GIL())
{
string pythonCode = @"
class IncompleteBuyingPowerModel:
def GetMaximumOrderQuantityForTargetBuyingPower(self, parameters):
return GetMaximumOrderQuantityResult(100)
def HasSufficientBuyingPowerForOrder(self, parameters):
return HasSufficientBuyingPowerForOrderResult(True)
";
var module = PyModule.FromString("TestModels", pythonCode);
var purePython = module.GetAttr("IncompleteBuyingPowerModel").Invoke();
Assert.Throws<NotImplementedException>(() =>
PythonUtil.CreateInstanceOrWrapper<IBuyingPowerModel>(purePython, py => new BuyingPowerModelPythonWrapper(py)));
}
}
[Test]
public void PureCSharpClassReturnsDirectInstanceWithoutWrapper()
{
using (Py.GIL())
{
// Create pure C# instance
var csharpObject = new BuyingPowerModel();
var pyObject = csharpObject.ToPython();
// Should return the same C# instance, not a wrapper
var result = PythonUtil.CreateInstanceOrWrapper<IBuyingPowerModel>(
pyObject,
py => new BuyingPowerModelPythonWrapper(py)
);
Assert.IsNotNull(result);
Assert.AreSame(csharpObject, result);
Assert.IsNotInstanceOf<BuyingPowerModelPythonWrapper>(result);
Assert.IsInstanceOf<BuyingPowerModel>(result);
}
}
}
}