chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:02:50 +08:00
commit 0fc60fdcb1
5008 changed files with 910633 additions and 0 deletions
+163
View File
@@ -0,0 +1,163 @@
/*
* 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.AlgorithmFactory.Python.Wrappers;
using System;
using System.Collections.Generic;
using System.IO;
using QuantConnect.Orders;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class AlgorithmPythonWrapperTests
{
private string _baseCode;
[SetUp]
public void Setup()
{
_baseCode = File.ReadAllText(Path.Combine("./RegressionAlgorithms", "Test_AlgorithmPythonWrapper.py"));
}
[TestCase("")]
[TestCase("def OnEndOfDay(self): pass")]
[TestCase("def OnEndOfDay(self, symbol): pass")]
public void CallOnEndOfDayDoesNotThrow(string code)
{
// If we define either one or the other overload of OnEndOfDay.
// the algorithm will not throw or log the error
using (Py.GIL())
{
var algorithm = GetAlgorithm(code);
Assert.Null(algorithm.RunTimeError);
Assert.DoesNotThrow(() => algorithm.OnEndOfDay());
Assert.Null(algorithm.RunTimeError);
Assert.DoesNotThrow(() => algorithm.OnEndOfDay(Symbols.SPY));
Assert.Null(algorithm.RunTimeError);
}
}
[Test]
[TestCase("def OnEndOfDay(self): self.Name = 'EOD'\r\n def OnEndOfDay(self, symbol): self.Name = 'EODSymbol'", "EODSymbol")]
[TestCase("def OnEndOfDay(self, symbol): self.Name = 'EODSymbol'\r\n def OnEndOfDay(self): self.Name = 'EOD'", "EOD")]
public void OnEndOfDayBothImplemented(string code, string expectedImplementation)
{
// If we implement both OnEndOfDay functions we expect it to not throw,
// but only the latest will be seen and used.
// To test this we will have the functions set something we can verify such as Algo name
using (Py.GIL())
{
var algorithm = GetAlgorithm(code);
Assert.Null(algorithm.RunTimeError);
Assert.DoesNotThrow(() => algorithm.OnEndOfDay());
Assert.Null(algorithm.RunTimeError);
Assert.DoesNotThrow(() => algorithm.OnEndOfDay(Symbols.SPY));
Assert.Null(algorithm.RunTimeError);
// Check the name
Assert.AreEqual(expectedImplementation, algorithm.Name);
// Check the wrapper EOD Implemented variables to confirm
switch (expectedImplementation)
{
case "EOD":
Assert.IsTrue(algorithm.IsOnEndOfDayImplemented);
Assert.IsFalse(algorithm.IsOnEndOfDaySymbolImplemented);
break;
case "EODSymbol":
Assert.IsTrue(algorithm.IsOnEndOfDaySymbolImplemented);
Assert.IsFalse(algorithm.IsOnEndOfDayImplemented);
break;
}
}
}
[Test]
public void CallOnEndOfDayExceptionNoParameter()
{
// When we define OnEndOfDay without a parameter and it has an user error (divide by zero)
// it doesn't throw and stop the algorithm, but set its RuntimeError
using (Py.GIL())
{
var algorithm = GetAlgorithm("def OnEndOfDay(self): 1/0");
Assert.Null(algorithm.RunTimeError);
Assert.DoesNotThrow(() => algorithm.OnEndOfDay());
Assert.NotNull(algorithm.RunTimeError);
}
}
[TestCase("", false)]
[TestCase("def OnMarginCall(self, orders): pass", true)]
[TestCase("def OnMarginCall(self, orders): return orders", false)]
public void OnMarginCall(string code, bool throws)
{
using (Py.GIL())
{
var algorithm = GetAlgorithm(code);
Assert.Null(algorithm.RunTimeError);
var order = new SubmitOrderRequest(OrderType.Limit,
SecurityType.Base,
Symbol.Empty,
1,
1,
1,
DateTime.UtcNow,
"");
if (throws)
{
Assert.Throws<Exception>(() => algorithm.OnMarginCall(new List<SubmitOrderRequest> { order }));
}
else
{
Assert.DoesNotThrow(() => algorithm.OnMarginCall(new List<SubmitOrderRequest> { order }));
}
}
}
[Test]
public void CallOnEndOfDayExceptionWithParameter()
{
// When we define OnEndOfDay with the Symbol parameter and it has an user error (divide by zero)
// it doesn't throw and stop the algorithm, but set its RuntimeError
using (Py.GIL())
{
var algorithm = GetAlgorithm("def OnEndOfDay(self, symbol): 1/0");
Assert.Null(algorithm.RunTimeError);
Assert.DoesNotThrow(() => algorithm.OnEndOfDay(Symbols.SPY));
Assert.NotNull(algorithm.RunTimeError);
}
}
private AlgorithmPythonWrapper GetAlgorithm(string code)
{
code = $"{_baseCode}{Environment.NewLine} {code}";
using (Py.GIL())
{
PyModule.FromString("Test_AlgorithmPythonWrapper", code);
return new AlgorithmPythonWrapper("Test_AlgorithmPythonWrapper");
}
}
}
}
+608
View File
@@ -0,0 +1,608 @@
/*
* 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.Orders.Fees;
using QuantConnect.Python;
using System;
using System.Collections.Generic;
using System.Linq;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class BasePythonWrapperTests
{
[Test]
public void EqualsReturnsTrueForWrapperAndUnderlyingModel()
{
using var _ = Py.GIL();
var module = PyModule.FromString("EqualsReturnsTrueForWrapperAndUnderlyingModel", @"
from clr import AddReference
AddReference('QuantConnect.Tests')
from QuantConnect.Tests.Python import BasePythonWrapperTests
class PythonDerivedTestModel(BasePythonWrapperTests.TestModel):
pass
class PythonTestModel:
pass
");
var pyDerivedModel = module.GetAttr("PythonDerivedTestModel").Invoke();
var wrapper = new BasePythonWrapper<ITestModel>(pyDerivedModel);
var pyModel = module.GetAttr("PythonTestModel").Invoke();
Assert.IsTrue(wrapper.Equals(pyDerivedModel));
Assert.IsTrue(wrapper.Equals(new BasePythonWrapper<ITestModel>(pyDerivedModel)));
Assert.IsFalse(wrapper.Equals(pyModel));
}
[TestFixture]
public class RuntimeChecks
{
[TestFixture]
public class InvokingMethod
{
public interface ITestInvokeMethodModel
{
int IntReturnTypeMethod();
}
[Test]
public void ThrowsWhenWhenWrongReturnType([Values] bool withValidReturnType)
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(ThrowsWhenWhenWrongReturnType), @"
class PythonTestInvokeMethodModel():
def __init__(self):
self._return_valid_type = True
def set_return_valid_type(self, value):
self._return_valid_type = value
def int_return_type_method(self):
if self._return_valid_type:
return 1
# Should return a integer to properly match the interface
return ""string""
");
using var pyInstance = module.GetAttr("PythonTestInvokeMethodModel").Invoke();
using var pyWithValidReturnType = withValidReturnType.ToPython();
pyInstance.GetAttr("set_return_valid_type").Invoke(pyWithValidReturnType);
var wrapper = new BasePythonWrapper<ITestInvokeMethodModel>(pyInstance);
if (withValidReturnType)
{
var result = -1;
Assert.DoesNotThrow(() => result = wrapper.InvokeMethod<int>("IntReturnTypeMethod"));
Assert.AreEqual(1, result);
}
else
{
Assert.Throws<InvalidCastException>(() => wrapper.InvokeMethod<int>("IntReturnTypeMethod"));
}
}
[TestFixture]
public class WithOutParameters
{
public interface ITestInvokeMethodWithOutParamsModel
{
DateTime MethodWithOutParams(out int intOutParam, out string stringOutParam);
}
[Test]
public void ThrowsWhenWrongOutParamType([Values] bool withValidOutParamsTypes)
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(ThrowsWhenWrongOutParamType), @"
from datetime import datetime
class PythonTestInvokeMethodWithOutParamsModel():
def __init__(self):
self._return_valid_out_param_type = True
def set_return_valid_out_param_type(self, value):
self._return_valid_out_param_type = value
def method_with_out_params(self, int_out_param, string_out_param):
if self._return_valid_out_param_type:
int_out_param = 1
string_out_param = 'string'
else:
int_out_param = 'string' # Invalid type
string_out_param = 'string'
return datetime(2024, 6, 21), int_out_param, string_out_param
");
using var pyInstance = module.GetAttr("PythonTestInvokeMethodWithOutParamsModel").Invoke();
using var pyWithValidOutParamsTypes = withValidOutParamsTypes.ToPython();
pyInstance.GetAttr("set_return_valid_out_param_type").Invoke(pyWithValidOutParamsTypes);
var wrapper = new BasePythonWrapper<ITestInvokeMethodWithOutParamsModel>(pyInstance);
AssertInvoke(wrapper, withValidOutParamsTypes);
}
[Test]
public void ThrowsWhenWrongOutParamCount([Values] bool withValidOutParamCount)
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(ThrowsWhenWrongOutParamCount), @"
from datetime import datetime
class PythonTestInvokeMethodWithOutParamsModel():
def __init__(self):
self._return_valid_out_params_count = True
def set_return_valid_out_params_count(self, value):
self._return_valid_out_params_count = value
def method_with_out_params(self, int_out_param, string_out_param):
int_out_param = 1
string_out_param = 'string'
if self._return_valid_out_params_count:
return datetime(2024, 6, 21), int_out_param, string_out_param
else:
return datetime(2024, 6, 21), int_out_param
");
using var pyInstance = module.GetAttr("PythonTestInvokeMethodWithOutParamsModel").Invoke();
using var pyWithValidOutParamCount = withValidOutParamCount.ToPython();
pyInstance.GetAttr("set_return_valid_out_params_count").Invoke(pyWithValidOutParamCount);
var wrapper = new BasePythonWrapper<ITestInvokeMethodWithOutParamsModel>(pyInstance);
AssertInvoke<ArgumentException>(wrapper, withValidOutParamCount);
}
[Test]
public void ThrowsWhenReturnedTypeIsNotATuple([Values] bool withValidReturnType)
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(ThrowsWhenWrongReturnType), @"
from datetime import datetime
class PythonTestInvokeMethodWithOutParamsModel():
def __init__(self):
self._use_valid_return_type = True
def set_use_valid_return_type(self, value):
self._use_valid_return_type = value
def method_with_out_params(self, int_out_param, string_out_param):
int_out_param = 1
string_out_param = 'string'
if self._use_valid_return_type:
return datetime(2024, 6, 21), int_out_param, string_out_param
else:
return 1 # Invalid return type, not a tuple
");
using var pyInstance = module.GetAttr("PythonTestInvokeMethodWithOutParamsModel").Invoke();
using var pyWithValidOutParamCount = withValidReturnType.ToPython();
pyInstance.GetAttr("set_use_valid_return_type").Invoke(pyWithValidOutParamCount);
var wrapper = new BasePythonWrapper<ITestInvokeMethodWithOutParamsModel>(pyInstance);
AssertInvoke<ArgumentException>(wrapper, withValidReturnType);
}
[Test]
public void ThrowsWhenWrongReturnType([Values] bool withValidReturnType)
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(ThrowsWhenWrongReturnType), @"
from datetime import datetime
class PythonTestInvokeMethodWithOutParamsModel():
def __init__(self):
self._use_valid_return_type = True
def set_use_valid_return_type(self, value):
self._use_valid_return_type = value
def method_with_out_params(self, int_out_param, string_out_param):
int_out_param = 1
string_out_param = 'string'
if self._use_valid_return_type:
return datetime(2024, 6, 21), int_out_param, string_out_param
else:
return 1, int_out_param, string_out_param
");
using var pyInstance = module.GetAttr("PythonTestInvokeMethodWithOutParamsModel").Invoke();
using var pyWithValidOutParamCount = withValidReturnType.ToPython();
pyInstance.GetAttr("set_use_valid_return_type").Invoke(pyWithValidOutParamCount);
var wrapper = new BasePythonWrapper<ITestInvokeMethodWithOutParamsModel>(pyInstance);
AssertInvoke(wrapper, withValidReturnType);
}
private static void AssertInvoke<TException>(BasePythonWrapper<ITestInvokeMethodWithOutParamsModel> wrapper, bool validCase)
where TException : Exception
{
var outParametersTypes = new Type[] { typeof(int), typeof(string) };
var intOutParameter = -1;
var stringOutParameter = string.Empty;
if (validCase)
{
var result = wrapper.InvokeMethodWithOutParameters<DateTime>("MethodWithOutParams", outParametersTypes,
out var outParameters, intOutParameter, stringOutParameter);
Assert.AreEqual(new DateTime(2024, 6, 21), result);
Assert.AreEqual(1, outParameters[0]);
Assert.AreEqual("string", outParameters[1]);
}
else
{
Assert.Throws<TException>(() => wrapper.InvokeMethodWithOutParameters<DateTime>("MethodWithOutParams",
outParametersTypes, out var _, intOutParameter, stringOutParameter));
}
}
private static void AssertInvoke(BasePythonWrapper<ITestInvokeMethodWithOutParamsModel> wrapper, bool validCase)
{
AssertInvoke<InvalidCastException>(wrapper, validCase);
}
}
[TestFixture]
public class WithEnumerableReturnType
{
public interface ITestInvokeMethodReturningIterable
{
IEnumerable<int> Range(int min, int max);
}
[Test]
public void ThrowsWhenReturnTypeIsNotIterable([Values] bool withValidReturnType)
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(ThrowsWhenReturnTypeIsNotIterable), @"
class PythonTestInvokeMethodReturningIterable():
def __init__(self):
self._use_valid_return_type = True
def set_use_valid_return_type(self, value):
self._use_valid_return_type = value
def range(self, min, max):
if self._use_valid_return_type:
return range(min, max)
");
using var pyInstance = module.GetAttr("PythonTestInvokeMethodReturningIterable").Invoke();
using var pyWithValidReturnType = withValidReturnType.ToPython();
pyInstance.GetAttr("set_use_valid_return_type").Invoke(pyWithValidReturnType);
var wrapper = new BasePythonWrapper<ITestInvokeMethodReturningIterable>(pyInstance);
if (withValidReturnType)
{
var result = wrapper.InvokeMethodAndEnumerate<int>("Range", 5, 10).ToList();
CollectionAssert.AreEqual(new[] { 5, 6, 7, 8, 9 }, result);
}
else
{
Assert.Throws<InvalidCastException>(() => wrapper.InvokeMethodAndEnumerate<int>("Range", 5, 10).ToList());
}
}
[Test]
public void ThrowsWhenIteratorItemIsOfWrongType()
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(ThrowsWhenIteratorItemIsOfWrongType), @"
class PythonTestInvokeMethodReturningIterable():
def range(self, min, max):
for i in range(min, max):
yield i
yield 'string'
");
using var pyInstance = module.GetAttr("PythonTestInvokeMethodReturningIterable").Invoke();
var wrapper = new BasePythonWrapper<ITestInvokeMethodReturningIterable>(pyInstance);
Assert.Throws<InvalidCastException>(() => wrapper.InvokeMethodAndEnumerate<int>("Range", 5, 10).ToList());
}
}
[TestFixture]
public class WithDictionaryReturnType
{
public interface ITestInvokeMethodReturningDictionary
{
Dictionary<Symbol, List<double>> GetDictionary();
}
[TestCase(true, false)]
[TestCase(true, true)]
[TestCase(false)]
public void ThrowsWhenReturnTypeIsNotDictionary(bool withValidReturnType, bool returnNone = false)
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(ThrowsWhenReturnTypeIsNotDictionary), @"
from QuantConnect.Tests import Symbols
class PythonTestInvokeMethodReturningDictionary():
def __init__(self):
self._use_valid_return_type = True
self._return_none = False
def set_use_valid_return_type(self, value):
self._use_valid_return_type = value
def set_return_none(self, value):
self._return_none = value
def get_dictionary(self):
if self._use_valid_return_type:
if not self._return_none:
return {
Symbols.SPY: [1.1, 2.2],
Symbols.USDJPY: [3.3, 4.4, 5.5],
Symbols.SPY_C_192_Feb19_2016: [6.6],
}
else:
# None is a valid value for a Dictionary
return None
else:
return [1, 2, 3]
");
using var pyInstance = module.GetAttr("PythonTestInvokeMethodReturningDictionary").Invoke();
using var pyWithValidReturnType = withValidReturnType.ToPython();
pyInstance.GetAttr("set_use_valid_return_type").Invoke(pyWithValidReturnType);
using var pyReturnNone = returnNone.ToPython();
pyInstance.GetAttr("set_return_none").Invoke(pyReturnNone);
var wrapper = new BasePythonWrapper<ITestInvokeMethodReturningDictionary>(pyInstance);
if (withValidReturnType)
{
var result = wrapper.InvokeMethodAndGetDictionary<Symbol, List<double>>("GetDictionary");
if (returnNone)
{
Assert.IsNull(result);
}
else
{
var expectedDictionary = new Dictionary<Symbol, List<double>>()
{
{ Symbols.SPY, new() { 1.1, 2.2 } },
{ Symbols.USDJPY, new() { 3.3, 4.4, 5.5 } },
{ Symbols.SPY_C_192_Feb19_2016, new() { 6.6 } },
};
Assert.IsNotNull(result);
Assert.AreEqual(expectedDictionary.Count, result.Count);
foreach (var kvp in expectedDictionary)
{
Assert.IsTrue(result.TryGetValue(kvp.Key, out var resultValue));
CollectionAssert.AreEqual(kvp.Value, resultValue);
}
}
}
else
{
Assert.Throws<InvalidCastException>(() => wrapper.InvokeMethodAndGetDictionary<Symbol, List<double>>("GetDictionary"));
}
}
[Test]
public void ThrowsWhenDictionaryKeyIsOfWrongType()
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(ThrowsWhenDictionaryKeyIsOfWrongType), @"
from datetime import datetime
from QuantConnect.Tests import Symbols
class PythonTestInvokeMethodReturningDictionary():
def get_dictionary(self):
date = datetime(2024, 8, 14)
return {
Symbols.SPY: [1.1, 2.2],
Symbols.USDJPY: [3.3, 4.4, 5.5],
date: [6.6],
}
");
using var pyInstance = module.GetAttr("PythonTestInvokeMethodReturningDictionary").Invoke();
var wrapper = new BasePythonWrapper<ITestInvokeMethodReturningDictionary>(pyInstance);
Assert.Throws<InvalidCastException>(() => wrapper.InvokeMethodAndGetDictionary<Symbol, List<double>>("GetDictionary"));
}
[Test]
public void ThrowsWhenDictionaryValueIsOfWrongType()
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(ThrowsWhenDictionaryValueIsOfWrongType), @"
from QuantConnect.Tests import Symbols
class PythonTestInvokeMethodReturningDictionary():
def get_dictionary(self):
return {
Symbols.SPY: [1.1, 2.2],
Symbols.USDJPY: [3.3, 4.4, 5.5],
Symbols.SPY_C_192_Feb19_2016: 6.6,
}
");
using var pyInstance = module.GetAttr("PythonTestInvokeMethodReturningDictionary").Invoke();
var wrapper = new BasePythonWrapper<ITestInvokeMethodReturningDictionary>(pyInstance);
Assert.Throws<InvalidCastException>(() => wrapper.InvokeMethodAndGetDictionary<Symbol, List<double>>("GetDictionary"));
}
}
[TestFixture]
public class WrappingResult
{
public interface ITestModel
{
IFeeModel GetFeeModel();
}
public class TestModel : ITestModel
{
public IFeeModel GetFeeModel()
{
return new FeeModel();
}
}
public class TestModelPythonWrapper : BasePythonWrapper<ITestModel>
{
public TestModelPythonWrapper(PyObject pyInstance) : base(pyInstance)
{
}
}
[Test]
public void WrapsResult([Values] bool withWrappedResult)
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(WrapsResult), @"
from AlgorithmImports import *
from clr import AddReference
AddReference('QuantConnect.Tests')
from QuantConnect.Tests.Python import BasePythonWrapperTests
class PythonFeeModel(FeeModel):
pass
class PythonTestModel(BasePythonWrapperTests.RuntimeChecks.InvokingMethod.WrappingResult.TestModel):
def __init__(self):
self._use_wrapped_result = True
def set_use_wrapped_result(self, value):
self._use_wrapped_result = value
def get_fee_model(self):
if self._use_wrapped_result:
return PythonFeeModel()
return FeeModel()
");
using var pyInstance = module.GetAttr("PythonTestModel").Invoke();
using var pyWithWrappedResult = withWrappedResult.ToPython();
pyInstance.GetAttr("set_use_wrapped_result").Invoke(pyWithWrappedResult);
var wrapper = new TestModelPythonWrapper(pyInstance);
var wrappingFunctionCalled = false;
var feeModel = wrapper.InvokeMethodAndWrapResult<IFeeModel>("GetFeeModel", (pyInstance) =>
{
wrappingFunctionCalled = true;
return new FeeModelPythonWrapper(pyInstance);
});
if (withWrappedResult)
{
Assert.IsTrue(wrappingFunctionCalled);
Assert.IsInstanceOf<FeeModelPythonWrapper>(feeModel);
}
else
{
Assert.IsFalse(wrappingFunctionCalled);
Assert.IsInstanceOf<FeeModel>(feeModel);
Assert.IsNotInstanceOf<FeeModelPythonWrapper>(feeModel);
}
}
}
}
[TestFixture]
public class WorkingWithProperties
{
public interface ITestProperties
{
List<double> Numbers { get; set; }
}
[Test]
public void ThrowsWhenSettingPropertyValueOfInvalidType([Values] bool withValidType)
{
using var _ = Py.GIL();
using var module = PyModule.FromString(nameof(ThrowsWhenSettingPropertyValueOfInvalidType), @"
class PythonTestSetProperty():
def __init__(self):
self._numbers = None
self._use_valid_type = True
def set_use_valid_type(self, value):
self._use_valid_type = value
@property
def numbers(self):
return self._numbers
@numbers.setter
def numbers(self, value):
self._numbers = value
def set_valid_numbers(self):
self.numbers = [1.1, 2.2, 3.3]
def set_invalid_numbers(self):
self.numbers = 1
");
using var pyInstance = module.GetAttr("PythonTestSetProperty").Invoke();
using var pyWithValidReturnType = withValidType.ToPython();
pyInstance.GetAttr("set_use_valid_type").Invoke(pyWithValidReturnType);
var wrapper = new BasePythonWrapper<ITestProperties>(pyInstance);
if (withValidType)
{
var result = wrapper.GetProperty<List<double>>("Numbers");
// The default value is null
Assert.IsNull(result);
// set the property
pyInstance.InvokeMethod("set_valid_numbers");
result = wrapper.GetProperty<List<double>>("Numbers");
var expectedNumbers = new List<double> { 1.1, 2.2, 3.3 };
CollectionAssert.AreEqual(expectedNumbers, result);
}
else
{
pyInstance.InvokeMethod("set_invalid_numbers");
Assert.Throws<InvalidCastException>(() => wrapper.GetProperty<List<double>>("Numbers"));
}
}
}
}
public interface ITestModel
{
}
public class TestModel : ITestModel
{
}
}
}
@@ -0,0 +1,463 @@
/*
* 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 System;
using NUnit.Framework;
using Python.Runtime;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data;
using QuantConnect.Data.Consolidators;
using QuantConnect.Data.Market;
using QuantConnect.Python;
using QuantConnect.Algorithm;
using QuantConnect.Tests.Engine.DataFeeds;
using QuantConnect.Statistics;
using QuantConnect.Tests.Common.Data;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class DataConsolidatorPythonWrapperTests: BaseConsolidatorTests
{
[Test]
public void UpdatePyConsolidator()
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"from AlgorithmImports import *\n" +
"class CustomConsolidator(PythonConsolidator):\n" +
" def __init__(self):\n" +
" self.update_was_called = False\n" +
" self.input_type = QuoteBar\n" +
" self.output_type = QuoteBar\n" +
" self.consolidated = None\n" +
" self.working_data = None\n" +
" def update(self, data):\n" +
" self.update_was_called = True\n" +
" def scan(self, time):\n" +
" pass\n");
var customConsolidator = module.GetAttr("CustomConsolidator").Invoke();
using var wrapper = new DataConsolidatorPythonWrapper(customConsolidator);
var time = DateTime.Today;
var period = TimeSpan.FromMinutes(1);
var bar1 = new QuoteBar
{
Time = time,
Symbol = Symbols.SPY,
Bid = new Bar(1, 2, 0.75m, 1.25m),
LastBidSize = 3,
Ask = null,
LastAskSize = 0,
Value = 1,
Period = period
};
wrapper.Update(bar1);
bool called;
customConsolidator.GetAttr("update_was_called").TryConvert(out called);
Assert.True(called);
}
}
[Test]
public void ScanPyConsolidator()
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"from AlgorithmImports import *\n" +
"class CustomConsolidator(PythonConsolidator):\n" +
" def __init__(self):\n" +
" self.scan_was_called = False\n" +
" self.input_type = QuoteBar\n" +
" self.output_type = QuoteBar\n" +
" self.consolidated = None\n" +
" self.working_data = None\n" +
" def update(self, data):\n" +
" pass\n" +
" def scan(self, time):\n" +
" self.scan_was_called = True\n");
var customConsolidator = module.GetAttr("CustomConsolidator").Invoke();
using var wrapper = new DataConsolidatorPythonWrapper(customConsolidator);
var time = DateTime.Today;
var period = TimeSpan.FromMinutes(1);
wrapper.Scan(DateTime.Now);
bool called;
customConsolidator.GetAttr("scan_was_called").TryConvert(out called);
Assert.True(called);
}
}
[Test]
public void InputTypePyConsolidator()
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"from AlgorithmImports import *\n" +
"class CustomConsolidator(PythonConsolidator):\n" +
" def __init__(self):\n" +
" self.input_type = QuoteBar\n" +
" self.output_type = QuoteBar\n" +
" self.consolidated = None\n" +
" self.working_data = None\n" +
" def update(self, data):\n" +
" pass\n" +
" def scan(self, time):\n" +
" pass\n");
var customConsolidator = module.GetAttr("CustomConsolidator").Invoke();
using var wrapper = new DataConsolidatorPythonWrapper(customConsolidator);
var time = DateTime.Today;
var period = TimeSpan.FromMinutes(1);
var type = wrapper.InputType;
Assert.True(type == typeof(QuoteBar));
}
}
[Test]
public void OutputTypePyConsolidator()
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"from AlgorithmImports import *\n" +
"class CustomConsolidator(PythonConsolidator):\n" +
" def __init__(self):\n" +
" self.input_type = QuoteBar\n" +
" self.output_type = QuoteBar\n" +
" self.consolidated = None\n" +
" self.working_data = None\n" +
" def update(self, data):\n" +
" pass\n" +
" def scan(self, time):\n" +
" pass\n");
var customConsolidator = module.GetAttr("CustomConsolidator").Invoke();
using var wrapper = new DataConsolidatorPythonWrapper(customConsolidator);
var time = DateTime.Today;
var period = TimeSpan.FromMinutes(1);
var type = wrapper.OutputType;
Assert.True(type == typeof(QuoteBar));
}
}
[Test]
public void RunRegressionAlgorithm()
{
var parameter = new RegressionTests.AlgorithmStatisticsTestParameters("CustomConsolidatorRegressionAlgorithm",
new Dictionary<string, string> {
{PerformanceMetrics.TotalOrders, "15"},
{"Average Win", "0.42%"},
{"Average Loss", "-0.03%"},
{"Compounding Annual Return", "76.673%"},
{"Drawdown", "0.200%"},
{"Expectancy", "4.239"},
{"Net Profit", "1.203%"},
{"Sharpe Ratio", "7.908"},
{"Probabilistic Sharpe Ratio", "94.373%"},
{"Loss Rate", "62%"},
{"Win Rate", "38%"},
{"Profit-Loss Ratio", "12.97"},
{"Alpha", "0.408"},
{"Beta", "0.35"},
{"Annual Standard Deviation", "0.067"},
{"Annual Variance", "0.005"},
{"Information Ratio", "1.484"},
{"Tracking Error", "0.117"},
{"Treynor Ratio", "1.526"},
{"Total Fees", "$24.34"}
},
Language.Python,
AlgorithmStatus.Completed);
AlgorithmRunner.RunLocalBacktest(parameter.Algorithm,
parameter.Statistics,
parameter.Language,
parameter.ExpectedFinalStatus);
}
[Test]
public void WindowIsPopulatedOnConsolidation()
{
using (Py.GIL())
{
using var wrapper = CreateFedPythonWrapper(1);
Assert.AreEqual(1, wrapper.Window.Count);
Assert.IsNotNull(wrapper.Consolidated);
Assert.AreEqual(wrapper.Consolidated, wrapper[0]);
}
}
[Test]
public void WindowKeepsPreviousConsolidatedBar()
{
using (Py.GIL())
{
using var wrapper = CreateFedPythonWrapper(1);
var firstConsolidated = wrapper.Consolidated;
FeedConsolidation(wrapper, 1);
Assert.AreEqual(2, wrapper.Window.Count);
Assert.AreNotEqual(firstConsolidated, wrapper[0]);
Assert.AreEqual(firstConsolidated, wrapper[1]);
Assert.AreEqual(firstConsolidated, wrapper.Previous);
}
}
[Test]
public void CanIterateOverConsolidatedBars()
{
using (Py.GIL())
{
using var wrapper = CreateFedPythonWrapper(2);
var bars = wrapper.ToList();
Assert.AreEqual(2, bars.Count);
Assert.AreEqual(wrapper[0], bars[0]);
Assert.AreEqual(wrapper[1], bars[1]);
}
}
[Test]
public void ResetClearsWindow()
{
using (Py.GIL())
{
using var wrapper = CreateFedPythonWrapper(1);
wrapper.Reset();
Assert.AreEqual(0, wrapper.Window.Count);
Assert.IsNull(wrapper.Consolidated);
}
}
private static DataConsolidatorPythonWrapper CreateFedPythonWrapper(int consolidations)
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"from AlgorithmImports import *\n" +
"class CustomConsolidator(QuoteBarConsolidator):\n" +
" def __init__(self):\n" +
" super().__init__(timedelta(minutes=2))\n");
var wrapper = new DataConsolidatorPythonWrapper(module.GetAttr("CustomConsolidator").Invoke());
FeedConsolidation(wrapper, consolidations);
return wrapper;
}
private static void FeedConsolidation(DataConsolidatorPythonWrapper wrapper, int consolidations)
{
var offset = wrapper.Window.Count * 2;
var time = DateTime.Today;
for (var i = 0; i < consolidations; i++)
{
var bar = new QuoteBar { Time = time.AddMinutes(offset + i * 2), Symbol = Symbols.SPY, Bid = new Bar(1, 2, 0.75m, 1.25m), LastBidSize = 3, Value = 1, Period = TimeSpan.FromMinutes(1) };
wrapper.Update(bar);
wrapper.Scan(time.AddMinutes(offset + (i + 1) * 2));
}
}
[Test]
public void PythonConsolidatorSubclassExposesWindow()
{
// custom consolidators that inherit PythonConsolidator (the documented path) must expose the
// rolling window just like the C# consolidators, not only the ones routed through the wrapper
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"from AlgorithmImports import *\n" +
"class CustomConsolidator(PythonConsolidator):\n" +
" def __init__(self):\n" +
" self.input_type = QuoteBar\n" +
" self.output_type = QuoteBar\n" +
" self.consolidated = None\n" +
" self.working_data = None\n" +
" def update(self, data):\n" +
" pass\n" +
" def scan(self, time):\n" +
" pass\n");
var customConsolidator = module.GetAttr("CustomConsolidator").Invoke();
var consolidator = customConsolidator.As<PythonConsolidator>();
var time = DateTime.Today;
var bar1 = new QuoteBar { Time = time, Symbol = Symbols.SPY, Bid = new Bar(1, 2, 0.75m, 1.25m), Value = 1, Period = TimeSpan.FromMinutes(1) };
var bar2 = new QuoteBar { Time = time.AddMinutes(1), Symbol = Symbols.SPY, Bid = new Bar(1, 2, 0.75m, 1.25m), Value = 2, Period = TimeSpan.FromMinutes(1) };
consolidator.OnDataConsolidated(customConsolidator, bar1);
consolidator.OnDataConsolidated(customConsolidator, bar2);
Assert.AreEqual(2, consolidator.Window.Count);
Assert.AreEqual(bar2, consolidator[0]);
Assert.AreEqual(bar1, consolidator.Previous);
consolidator.Reset();
Assert.AreEqual(0, consolidator.Window.Count);
}
}
[Test]
public void AttachAndTriggerEvent()
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"from AlgorithmImports import *\n" +
"class ImplementingClass():\n" +
" def __init__(self):\n" +
" self.EventCalled = False\n" +
" self.Consolidator = CustomConsolidator(timedelta(minutes=2))\n" +
" self.Consolidator.DataConsolidated += self.ConsolidatorEvent\n" +
" def ConsolidatorEvent(self, sender, bar):\n" +
" self.EventCalled = True\n" +
"class CustomConsolidator(QuoteBarConsolidator):\n" +
" def __init__(self,span):\n" +
" super().__init__(span)\n" +
" self.Span = span\n");
var implementingClass = module.GetAttr("ImplementingClass").Invoke();
var customConsolidator = implementingClass.GetAttr("Consolidator");
using var wrapper = new DataConsolidatorPythonWrapper(customConsolidator);
bool called;
implementingClass.GetAttr("EventCalled").TryConvert(out called);
Assert.False(called);
var time = DateTime.Today;
var period = TimeSpan.FromMinutes(1);
var bar1 = new QuoteBar
{
Time = time,
Symbol = Symbols.SPY,
Bid = new Bar(1, 2, 0.75m, 1.25m),
LastBidSize = 3,
Ask = null,
LastAskSize = 0,
Value = 1,
Period = period
};
wrapper.Update(bar1);
wrapper.Scan(time.AddMinutes(2));
implementingClass.GetAttr("EventCalled").TryConvert(out called);
Assert.True(called);
}
}
[Test]
public void SubscriptionManagedDoesNotWrapCSharpConsolidators()
{
//Setup algorithm and Equity
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
var spy = algorithm.AddEquity("SPY").Symbol;
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"from AlgorithmImports import *\n" +
"consolidator = QuoteBarConsolidator(timedelta(5))");
var pyConsolidator = module.GetAttr("consolidator");
algorithm.SubscriptionManager.AddConsolidator(spy, pyConsolidator);
pyConsolidator.TryConvert(out IDataConsolidator consolidator);
algorithm.SubscriptionManager.RemoveConsolidator(spy, consolidator);
var count = algorithm.SubscriptionManager
.SubscriptionDataConfigService
.GetSubscriptionDataConfigs(spy)
.Sum(x => x.Consolidators.Count);
Assert.AreEqual(0, count);
consolidator.Dispose();
}
}
protected override IEnumerable<IBaseData> GetTestValues()
{
var time = DateTime.Today;
return new List<QuoteBar>()
{
new QuoteBar(){Time = time, Symbol = Symbols.SPY, Bid = new Bar(1, 2, 0.5m, 1.75m), Ask = new Bar(2.2m, 4.4m, 3.3m, 3.3m), LastBidSize = 10, LastAskSize = 0 },
new QuoteBar(){Time = time, Symbol = Symbols.SPY, Bid = new Bar(0, 4, 0.4m, 3.75m), Ask = new Bar(2.3m, 9.4m, 2.3m, 4.5m), LastBidSize = 5, LastAskSize = 4 },
new QuoteBar(){Time = time, Symbol = Symbols.SPY, Bid = new Bar(2, 2, 0.9m, 1.45m), Ask = new Bar(2.7m, 8.4m, 3.6m, 3.6m), LastBidSize = 8, LastAskSize = 4 },
new QuoteBar(){Time = time, Symbol = Symbols.SPY, Bid = new Bar(2, 6, 2.5m, 5.55m), Ask = new Bar(3.2m, 6.4m, 2.3m, 5.3m), LastBidSize = 9, LastAskSize = 4 },
new QuoteBar(){Time = time, Symbol = Symbols.SPY, Bid = new Bar(1, 2, 1.5m, 0.34m), Ask = new Bar(3.6m, 9.4m, 3.7m, 3.8m), LastBidSize = 5, LastAskSize = 8 },
new QuoteBar(){Time = time, Symbol = Symbols.SPY, Bid = new Bar(1, 2, 1.1m, 0.75m), Ask = new Bar(3.8m, 8.4m, 7.3m, 5.3m), LastBidSize = 9, LastAskSize = 5 },
new QuoteBar(){Time = time, Symbol = Symbols.SPY, Bid = new Bar(3, 3, 2.2m, 1.12m), Ask = new Bar(4.5m, 7.2m, 7.1m, 6.1m), LastBidSize = 6, LastAskSize = 3 },
};
}
protected override void AssertConsolidator(IDataConsolidator consolidator)
{
base.AssertConsolidator(consolidator);
using (Py.GIL())
{
var pythonConsolidator = consolidator as TestDataConsolidatorPythonWrapper;
pythonConsolidator.RawIndicator.GetAttr("update_was_called").TryConvert(out bool pythonConsolidatorUpdateWasCalled);
Assert.IsFalse(pythonConsolidatorUpdateWasCalled);
}
}
protected override IDataConsolidator CreateConsolidator()
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"from AlgorithmImports import *\n" +
"class CustomConsolidator(PythonConsolidator):\n" +
" def __init__(self):\n" +
" self.update_was_called = False\n" +
" self.input_type = QuoteBar\n" +
" self.output_type = QuoteBar\n" +
" self.consolidated = None\n" +
" self.working_data = None\n" +
" def update(self, data):\n" +
" self.update_was_called = True\n" +
" def scan(self, time):\n" +
" pass\n" +
" def reset(self):\n" +
" self.update_was_called = False\n");
var customConsolidator = module.GetAttr("CustomConsolidator").Invoke();
return new TestDataConsolidatorPythonWrapper(customConsolidator);
}
}
public class TestDataConsolidatorPythonWrapper : DataConsolidatorPythonWrapper
{
public PyObject RawIndicator { get; set; }
public TestDataConsolidatorPythonWrapper(PyObject consolidator) : base(consolidator)
{
RawIndicator = consolidator;
}
}
}
}
@@ -0,0 +1,302 @@
#
# 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.
#
from clr import AddReference
AddReference("QuantConnect.Indicators")
from QuantConnect.Indicators import *
from datetime import datetime
import decimal as d
import unittest
class IndicatorExtensionsTests(unittest.TestCase):
def test_PipesDataUsingOfFromFirstToSecond(self):
first = SimpleMovingAverage(2)
second = Delay(1)
# this is a configuration step, but returns the reference to the second for method chaining
third = IndicatorExtensions.Of(second, first)
data1 = IndicatorDataPoint(datetime.now(), 1)
data2 = IndicatorDataPoint(datetime.now(), 2)
data3 = IndicatorDataPoint(datetime.now(), 3)
data4 = IndicatorDataPoint(datetime.now(), 4)
# sma has one item
first.Update(data1)
self.assertFalse(first.IsReady)
self.assertEqual(0, second.Current.Value)
# sma is ready, delay will repeat this value
first.Update(data2)
self.assertTrue(first.IsReady)
self.assertFalse(second.IsReady)
self.assertEqual(1.5, second.Current.Value)
# delay is ready, and repeats its first input
first.Update(data3)
self.assertTrue(second.IsReady)
self.assertEqual(1.5, second.Current.Value)
# now getting the delayed data
first.Update(data4)
self.assertEqual(2.5, second.Current.Value)
def test_PipesDataFirstWeightedBySecond(self):
period = 4
value = Identity("Value")
weight = Identity("Weight")
third = IndicatorExtensions.WeightedBy(value, weight, period)
data = range(1, 11)
window = list(reversed(data))[:period]
current = sum([ 2 * x * x for x in window ]) / float(sum(window))
for item in data:
value.Update(datetime.now(), 2 * item)
weight.Update(datetime.now(), item)
self.assertEqual(current, float(third.Current.Value))
def test_NewDataPushesToDerivedIndicators(self):
identity = Identity("identity")
self.sma = SimpleMovingAverage(3)
identity.Updated += self.identity_updated
identity.Update(datetime.now(), 1)
identity.Update(datetime.now(), 2)
self.assertFalse(self.sma.IsReady)
identity.Update(datetime.now(), 3)
self.assertTrue(self.sma.IsReady)
self.assertEqual(2, self.sma.Current.Value)
def identity_updated(self, sender, consolidated):
self.sma.Update(consolidated)
def test_MultiChainSMA(self):
identity = Identity("identity")
delay = Delay(2)
# create the SMA of the delay of the identity
sma = IndicatorExtensions.SMA(IndicatorExtensions.Of(delay, identity), 2)
identity.Update(datetime.now(), 1)
self.assertTrue(identity.IsReady)
self.assertFalse(delay.IsReady)
self.assertFalse(sma.IsReady)
identity.Update(datetime.now(), 2)
self.assertTrue(identity.IsReady)
self.assertFalse(delay.IsReady)
self.assertFalse(sma.IsReady)
identity.Update(datetime.now(), 3)
self.assertTrue(identity.IsReady)
self.assertTrue(delay.IsReady)
self.assertFalse(sma.IsReady)
identity.Update(datetime.now(), 4)
self.assertTrue(identity.IsReady)
self.assertTrue(delay.IsReady)
self.assertTrue(sma.IsReady)
self.assertEqual(1.5, sma.Current.Value)
def test_MultiChainEMA(self):
identity = Identity("identity")
delay = Delay(2)
# create the EMA of chained methods
ema = IndicatorExtensions.EMA(IndicatorExtensions.Of(delay, identity), 2, d.Decimal(1))
identity.Update(datetime.now(), 1)
self.assertTrue(identity.IsReady)
self.assertFalse(delay.IsReady)
self.assertFalse(ema.IsReady)
identity.Update(datetime.now(), 2)
self.assertTrue(identity.IsReady)
self.assertFalse(delay.IsReady)
self.assertFalse(ema.IsReady)
identity.Update(datetime.now(), 3)
self.assertTrue(identity.IsReady)
self.assertTrue(delay.IsReady)
self.assertFalse(ema.IsReady)
identity.Update(datetime.now(), 4)
self.assertTrue(identity.IsReady)
self.assertTrue(delay.IsReady)
self.assertTrue(ema.IsReady)
self.assertEqual(2, ema.Current.Value)
def test_MultiChainMAX(self):
identity = Identity("identity")
delay = Delay(2)
# create the MAX of the delay of the identity
max = IndicatorExtensions.MAX(IndicatorExtensions.Of(delay, identity), 2)
identity.Update(datetime.now(), 1)
self.assertTrue(identity.IsReady)
self.assertFalse(delay.IsReady)
self.assertFalse(max.IsReady)
identity.Update(datetime.now(), 2)
self.assertTrue(identity.IsReady)
self.assertFalse(delay.IsReady)
self.assertFalse(max.IsReady)
identity.Update(datetime.now(), 3)
self.assertTrue(identity.IsReady)
self.assertTrue(delay.IsReady)
self.assertFalse(max.IsReady)
identity.Update(datetime.now(), 4)
self.assertTrue(identity.IsReady)
self.assertTrue(delay.IsReady)
self.assertTrue(max.IsReady)
self.assertEqual(2, max.Current.Value)
def test_MultiChainMIN(self):
identity = Identity("identity")
delay = Delay(2)
# create the MAX of the delay of the identity
min = IndicatorExtensions.MIN(IndicatorExtensions.Of(delay, identity), 2)
identity.Update(datetime.now(), 1)
self.assertTrue(identity.IsReady)
self.assertFalse(delay.IsReady)
self.assertFalse(min.IsReady)
identity.Update(datetime.now(), 2)
self.assertTrue(identity.IsReady)
self.assertFalse(delay.IsReady)
self.assertFalse(min.IsReady)
identity.Update(datetime.now(), 3)
self.assertTrue(identity.IsReady)
self.assertTrue(delay.IsReady)
self.assertFalse(min.IsReady)
identity.Update(datetime.now(), 4)
self.assertTrue(identity.IsReady)
self.assertTrue(delay.IsReady)
self.assertTrue(min.IsReady)
self.assertEqual(1, min.Current.Value)
def test_PlusAddsLeftAndRightAfterBothUpdated(self):
left = Identity("left")
right = Identity("right")
composite = IndicatorExtensions.Plus(left, right)
left.Update(datetime.now(), 1)
right.Update(datetime.now(), 1)
self.assertEqual(2, composite.Current.Value)
left.Update(datetime.today(), 2)
self.assertEqual(2, composite.Current.Value)
left.Update(datetime.today(), 3)
self.assertEqual(2, composite.Current.Value)
right.Update(datetime.today(), 4)
self.assertEqual(7, composite.Current.Value)
def test_MinusSubtractsLeftAndRightAfterBothUpdated(self):
left = Identity("left")
right = Identity("right")
composite = IndicatorExtensions.Minus(left, right)
left.Update(datetime.today(), 1)
right.Update(datetime.today(), 1)
self.assertEqual(0, composite.Current.Value)
left.Update(datetime.today(), 2)
self.assertEqual(0, composite.Current.Value)
left.Update(datetime.today(), 3)
self.assertEqual(0, composite.Current.Value)
right.Update(datetime.today(), 4)
self.assertEqual(-1, composite.Current.Value)
def test_OverDivdesLeftAndRightAfterBothUpdated(self):
left = Identity("left")
right = Identity("right")
composite = IndicatorExtensions.Over(left, right)
left.Update(datetime.today(), 1)
right.Update(datetime.today(), 1)
self.assertEqual(1, composite.Current.Value)
left.Update(datetime.today(), 2)
self.assertEqual(1, composite.Current.Value)
left.Update(datetime.today(), 3)
self.assertEqual(1, composite.Current.Value)
right.Update(datetime.today(), 4)
self.assertEqual(3.0 / 4.0, composite.Current.Value)
def test_OverHandlesDivideByZero(self):
left = Identity("left")
right = Identity("right")
composite = IndicatorExtensions.Over(left, right)
self.updatedEventFired = False
composite.Updated += self.composite_updated
left.Update(datetime.today(), 1)
self.assertFalse(self.updatedEventFired)
right.Update(datetime.today(), 0)
self.assertFalse(self.updatedEventFired)
# submitting another update to right won't cause an update without corresponding update to left
right.Update(datetime.today(), 1)
self.assertFalse(self.updatedEventFired)
left.Update(datetime.today(), 1)
self.assertTrue(self.updatedEventFired)
def composite_updated(self, sender, consolidated):
self.updatedEventFired = True
def test_TimesMultipliesLeftAndRightAfterBothUpdated(self):
left = Identity("left")
right = Identity("right")
composite = IndicatorExtensions.Times(left, right)
left.Update(datetime.today(), 1)
right.Update(datetime.today(), 1)
self.assertEqual(1, composite.Current.Value)
left.Update(datetime.today(), 2)
self.assertEqual(1, composite.Current.Value)
left.Update(datetime.today(), 3)
self.assertEqual(1, composite.Current.Value)
right.Update(datetime.today(), 4)
self.assertEqual(12, composite.Current.Value)
if __name__ == '__main__':
unittest.main()
+76
View File
@@ -0,0 +1,76 @@
/*
* 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 System;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Python;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class MethodOverloadTests
{
private dynamic _algorithm;
/// <summary>
/// Run before every test
/// </summary>
[SetUp]
public void Setup()
{
PythonInitializer.Initialize();
using (Py.GIL())
{
var module = Py.Import("Test_MethodOverload");
_algorithm = module.GetAttr("Test_MethodOverload").Invoke();
// this is required else will get a 'RuntimeBinderException' because fails to match constructor method
dynamic algo = _algorithm.AsManagedObject((Type)_algorithm.GetPythonType().AsManagedObject(typeof(Type)));
_algorithm.subscription_manager.set_data_manager(new DataManagerStub(algo));
_algorithm.initialize();
}
}
[Test]
public void CallPlotTests()
{
using (Py.GIL())
{
// self.Plot('NUMBER', 0.1)
Assert.DoesNotThrow(() => _algorithm.call_plot_number_test());
// self.Plot('STD', self.std), where self.sma = self.SMA('SPY', 20)
Assert.DoesNotThrow(() => _algorithm.call_plot_sma_test());
// self.Plot('SMA', self.sma), where self.std = self.STD('SPY', 20)
Assert.DoesNotThrow(() => _algorithm.call_plot_std_test());
// self.Plot("ERROR", self.Name), where self.Name is IAlgorithm.Name: string
Assert.Throws<PythonException>(() => _algorithm.call_plot_throw_test());
// self.Plot("ERROR", self.Portfolio), where self.Portfolio is IAlgorithm.Portfolio: instance of SecurityPortfolioManager
Assert.That(() => _algorithm.call_plot_throw_managed_test(),
Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<ArgumentException>());
// self.Plot("ERROR", self.a), where self.a is an instance of a python object
Assert.That(() => _algorithm.call_plot_throw_pyobject_test(),
Throws.InstanceOf<ClrBubbledException>().With.InnerException.InstanceOf<ArgumentException>());
}
}
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
* 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 System;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Securities.Equity;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class NamedArgumentsTests
{
[Test]
public void AddEquityTest()
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
using (Py.GIL())
{
// Test function that will used named args in Python -> C#
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"def test(algorithm):\n" +
" aapl = algorithm.AddEquity(ticker='AAPL')\n" +
" return aapl\n"
);
var testFunction = module.GetAttr("test");
var equity = testFunction.Invoke(algorithm.ToPython()).As<Equity>();
Assert.AreEqual("AAPL", equity.Symbol.Value);
}
}
}
}
@@ -0,0 +1,399 @@
/*
* 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.Python
{
[TestFixture]
public partial class PandasConverterTests
{
[Test, TestCaseSource(nameof(TestDataFrameNonExceptionFunctions))]
public void BackwardsCompatibilityDataFrameDataFrameNonExceptionFunctions(string method, string index, bool cache)
{
if(method == ".to_orc()")
{
if (OS.IsWindows)
{
// not supported in windows
return;
}
// orc does not support serializing a non-default index for the index; you can .reset_index() to make the index into column(s)
method = $".reset_index(){method}";
}
if (cache) SymbolCache.Set("SPY", Symbols.SPY);
using (Py.GIL())
{
dynamic test = PyModule.FromString("testModule",
$@"
def Test(df, symbol):
df = df.lastprice.unstack(level=0){method}").GetAttr("Test");
Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
}
}
[Test, TestCaseSource(nameof(TestDataFrameParameterlessFunctions))]
public void BackwardsCompatibilityDataFrameParameterlessFunctions(string method, string index, bool cache)
{
if (cache) SymbolCache.Set("SPY", Symbols.SPY);
using (Py.GIL())
{
dynamic test = PyModule.FromString("testModule",
$@"
def Test(df, symbol):
df = df.lastprice.unstack(level=0){method}
# If not DataFrame, return
if not hasattr(df, 'columns'):
return
if df.iloc[-1][{index}] is 0:
raise Exception('Data is zero')").GetAttr("Test");
Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
}
}
[Test, TestCaseSource(nameof(TestDataFrameOtherParameterFunctions))]
public void BackwardsCompatibilityDataFrameOtherParameterFunctions(string method, string index, bool cache)
{
// Cannot compare non identically indexed dataframes
if (method == ".compare(other)" && _newerPandas)
{
return;
}
if (cache) SymbolCache.Set("SPY", Symbols.SPY);
using (Py.GIL())
{
dynamic test = PyModule.FromString("testModule",
$@"
def Test(df, other, symbol):
df = df{method}
df = df.lastprice.unstack(level=0)
# If not DataFrame, return
if not hasattr(df, 'columns'):
return
if df.iloc[-1][{index}] is 0:
raise Exception('Data is zero')").GetAttr("Test");
Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
}
}
[Test, TestCaseSource(nameof(TestSeriesNonExceptionFunctions))]
public void BackwardsCompatibilitySeriesNonExceptionFunctions(string method, string index, bool cache)
{
if (cache) SymbolCache.Set("SPY", Symbols.SPY);
using (Py.GIL())
{
dynamic test = PyModule.FromString("testModule",
$@"
def Test(df, symbol):
series = df.lastprice
series = series{method}").GetAttr("Test");
Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
}
}
[Test, TestCaseSource(nameof(TestSeriesParameterlessFunctions))]
public void BackwardsCompatibilitySeriesParameterlessFunctions(string method, string index, bool cache)
{
if (cache) SymbolCache.Set("SPY", Symbols.SPY);
using (Py.GIL())
{
dynamic test = PyModule.FromString("testModule",
$@"
def Test(df, symbol):
series = df.lastprice
series = series{method}
# If not Series, return
if not hasattr(series, 'index') or type(series) is tuple:
return
if series.loc[{index}].iloc[-1] is 0:
raise Exception('Data is zero')").GetAttr("Test");
Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), Symbols.SPY));
}
}
[Test, TestCaseSource(nameof(TestSeriesOtherParameterFunctions))]
public void BackwardsCompatibilitySeriesOtherParameterFunctions(string method, string index, bool cache)
{
// Cannot compare non identically indexed dataframes
if (method == ".compare(other)" && _newerPandas)
{
return;
}
if (cache) SymbolCache.Set("SPY", Symbols.SPY);
using (Py.GIL())
{
dynamic test = PyModule.FromString("testModule",
$@"
def Test(df, other, symbol):
series, other = other.lastprice, df.lastprice
series = series{method}
# If not Series, return
if not hasattr(series, 'index') or type(series) is tuple:
return
if series.loc[{index}].iloc[-1] is 0:
raise Exception('Data is zero')").GetAttr("Test");
Assert.DoesNotThrow(() => test(GetTestDataFrame(Symbols.SPY), GetTestDataFrame(Symbols.AAPL), Symbols.SPY));
}
}
private static TestCaseData[] TestDataFrameNonExceptionFunctions
{
get
{
var functions = new[]
{
".agg('mean', axis=0)",
".aggregate('mean', axis=0)",
".clip(100, 200)",
".fillna(value=999)",
".first('2S')",
".isin([100])",
".last('2S')",
".melt()"
};
if (!IsNewerPandas()){
var additionalFunctions = new[]
{
".clip_lower(100)",
".clip_upper(200)",
};
functions.Concat(additionalFunctions);
}
var testCases = functions.SelectMany(x => new[]
{
new TestCaseData(x, "'SPY'", true),
new TestCaseData(x, "symbol", false),
new TestCaseData(x, "str(symbol.ID)", false)
}).ToList();
testCases.AddRange(_parameterlessFunctions["DataFrame"]);
return testCases.ToArray();
}
}
private static TestCaseData[] TestSeriesNonExceptionFunctions
{
get
{
var functions = new[]
{
".add_suffix('lean')",
".add_prefix('lean')",
".agg('mean', axis=0)",
".aggregate('mean', axis=0)",
".clip(100, 200)",
".fillna(value=999)",
".isin([100])",
".searchsorted(200)",
".value_counts()"
};
if (!IsNewerPandas()){
var additionalFunctions = new[]
{
".clip_lower(100)",
".clip_upper(200)",
};
functions.Concat(additionalFunctions);
}
var testCases = functions.SelectMany(x => new[]
{
new TestCaseData(x, "'SPY'", true),
new TestCaseData(x, "symbol", false),
new TestCaseData(x, "str(symbol.ID)", false)
})
.ToList();
testCases.AddRange(_parameterlessFunctions["Series"]);
return testCases.ToArray();
}
}
private static TestCaseData[] TestDataFrameParameterlessFunctions => _parameterlessFunctions["DataFrameParameterless"];
private static TestCaseData[] TestSeriesParameterlessFunctions => _parameterlessFunctions["SeriesParameterless"];
private static TestCaseData[] TestDataFrameOtherParameterFunctions
{
get
{
var functions = new[]
{
"+",
"-",
"/",
"*",
"%",
"**",
"//"
}
.SelectMany(x => new[]
{
new TestCaseData($" {x} other", "'SPY'", true),
new TestCaseData($" {x} other", "symbol", false),
new TestCaseData($" {x} other", "str(symbol.ID)", false)
}).ToList();
functions.AddRange(_parameterlessFunctions["DataFrameOtherParameter"]);
return functions.ToArray();
}
}
private static TestCaseData[] TestSeriesOtherParameterFunctions
{
get
{
var functions = new[]
{
"+",
"-",
"/",
"*",
"%",
"**",
"//"
}
.SelectMany(x => new[]
{
new TestCaseData($" {x} other", "'SPY'", true),
new TestCaseData($" {x} other", "symbol", false),
new TestCaseData($" {x} other", "str(symbol.ID)", false)
}).ToList();
functions.AddRange(_parameterlessFunctions["SeriesOtherParameter"]);
return functions.ToArray();
}
}
private static Dictionary<string, TestCaseData[]> _parameterlessFunctions = GetParameterlessFunctions();
private static Dictionary<string, TestCaseData[]> GetParameterlessFunctions()
{
// Initialize the Python engine and begin allow thread
PythonInitializer.Initialize();
var functionsByType = new Dictionary<string, TestCaseData[]>();
using (Py.GIL())
{
var module = PyModule.FromString("Test",
@"import pandas
from inspect import getmembers, isfunction, signature
skipped = [ 'boxplot', 'hist', 'plot', # <- Graphics
'agg', 'aggregate', 'align', 'bool','combine', 'corrwith', 'dot', 'drop',
'equals', 'ewm', 'fillna', 'filter', 'groupby', 'join', 'mask', 'melt',
'pivot', 'pivot_table', 'reindex_like', 'rename', 'reset_index', 'select_dtypes',
'slice_shift', 'swaplevel', 'to_clipboard', 'to_excel', 'to_feather', 'to_gbq',
'to_hdf', 'to_list', 'tolist', 'to_parquet', 'to_period', 'to_pickle', 'to_sql', 'to_xml',
'to_stata', 'to_timestamp', 'to_xarray', 'tshift', 'update', 'value_counts', 'where']
newPandas = int(pandas.__version__.split('.')[0]) >= 1
def getSimpleExceptionTestFunctions(cls):
functions = [ 'describe', 'mode']
if not newPandas:
functions.append('get_dtype_counts')
functions.append('get_ftype_counts')
for name, member in getmembers(cls):
if isfunction(member) and name.startswith('to') and name not in skipped:
functions.append(name)
return functions
DataFrame = getSimpleExceptionTestFunctions(pandas.DataFrame)
Series = getSimpleExceptionTestFunctions(pandas.Series)
skipped += set(DataFrame + Series)
def getParameterlessFunctions(cls):
functions = list()
for name, member in getmembers(cls):
if isfunction(member) and not name.startswith('_') and name not in skipped:
parameters = signature(member).parameters
count = 0
for parameter in parameters.values():
if parameter.default is parameter.empty:
count += 1
else:
break
if count < 2:
functions.append(name)
return functions
def getOtherParameterFunctions(cls):
functions = list()
for name, member in getmembers(pandas.DataFrame):
if isfunction(member) and not name.startswith('_') and name not in skipped:
parameters = signature(member).parameters
for parameter in parameters.values():
if parameter.name == 'other':
functions.append(name)
break
return functions
DataFrameParameterless = getParameterlessFunctions(pandas.DataFrame)
SeriesParameterless = getParameterlessFunctions(pandas.Series)
DataFrameOtherParameter = getOtherParameterFunctions(pandas.DataFrame)
SeriesOtherParameter = getOtherParameterFunctions(pandas.Series)
");
Func<string, string, TestCaseData[]> converter = (s, p) =>
{
var list = (List<string>)module.GetAttr(s).AsManagedObject(typeof(List<string>));
return list.SelectMany(x => new[]
{
new TestCaseData($".{x}{p}", "'SPY'", true),
new TestCaseData($".{x}{p}", "symbol", false),
new TestCaseData($".{x}{p}", "str(symbol.ID)", false)
}
).ToArray();
};
functionsByType.Add("DataFrame", converter("DataFrame", "()"));
functionsByType.Add("Series", converter("Series", "()"));
functionsByType.Add("DataFrameParameterless", converter("DataFrameParameterless", "()"));
functionsByType.Add("SeriesParameterless", converter("SeriesParameterless", "()"));
functionsByType.Add("DataFrameOtherParameter", converter("DataFrameOtherParameter", "(other)"));
functionsByType.Add("SeriesOtherParameter", converter("SeriesOtherParameter", "(other)"));
}
return functionsByType;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,364 @@
/*
* 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.Data;
using QuantConnect.Python;
using System;
using System.Collections.Generic;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public partial class PandasConverterTests
{
// Over complicating it on purpose to test the expanding of nested classes
private class BarOpen
{
public decimal Open { get; set; }
public BarOpen(decimal open)
{
Open = open;
}
}
private class BarHigh
{
public decimal High { get; set; }
public BarHigh(decimal high)
{
High = high;
}
}
private class BarLow
{
public decimal Low { get; set; }
public BarLow(decimal low)
{
Low = low;
}
}
private class BarClose
{
public decimal Close { get; set; }
public BarClose(decimal close)
{
Close = close;
}
}
private class CustomBar
{
public BarOpen Open { get; set; }
public BarHigh High { get; set; }
public BarLow Low { get; set; }
public BarClose Close { get; set; }
}
private class CustomTradeBar : BaseData
{
public CustomBar Prices { get; set; }
public decimal Volume { get; set; }
public CustomTradeBar(Symbol symbol, decimal open, decimal high, decimal low, decimal close, decimal volume)
{
Symbol = symbol;
Prices = new CustomBar
{
Open = new BarOpen(open),
High = new BarHigh(high),
Low = new BarLow(low),
Close = new BarClose(close),
};
Volume = volume;
}
}
[Test]
public void ExpandsNestedClassesIntoDataFrameColumns()
{
var converter = new PandasConverter();
var data = new List<CustomTradeBar>
{
new CustomTradeBar(Symbols.IBM, 101m, 102m, 100m, 101m, 10m),
new CustomTradeBar(Symbols.IBM, 102m, 103m, 101m, 101m, 9m),
new CustomTradeBar(Symbols.IBM, 99m, 100m, 98m, 99m, 10m),
};
dynamic dataFrame = converter.GetDataFrame(data);
var expectedColumnNames = new List<string>() { "open", "high", "low", "close", "volume" };
AssertDataFrameColumns(dataFrame, data.Count, expectedColumnNames);
}
private class CustomQuoteBar : BaseData
{
public CustomBar Bid { get; set; }
public decimal BidSize { get; set; }
public CustomBar Ask { get; set; }
public decimal AskSize { get; set; }
public CustomQuoteBar(Symbol symbol, decimal bidOpen, decimal bidHigh, decimal bidLow, decimal bidClose, decimal bidSize,
decimal askOpen, decimal askHigh, decimal askLow, decimal askClose, decimal askSize)
{
Symbol = symbol;
Bid = new CustomBar
{
Open = new BarOpen(bidOpen),
High = new BarHigh(bidHigh),
Low = new BarLow(bidLow),
Close = new BarClose(bidClose),
};
BidSize = bidSize;
Ask = new CustomBar
{
Open = new BarOpen(askOpen),
High = new BarHigh(askHigh),
Low = new BarLow(askLow),
Close = new BarClose(askClose),
};
AskSize = askSize;
}
}
[Test]
public void ExpandsNestedClassesIntoDataFrameColumnsWithDuplicateNames()
{
var converter = new PandasConverter();
var data = new List<CustomQuoteBar>
{
new CustomQuoteBar(Symbols.IBM, 101m, 102m, 100m, 101m, 10m, 101m, 102m, 100m, 101m, 10m),
new CustomQuoteBar(Symbols.IBM, 102m, 103m, 101m, 101m, 9m, 102m, 103m, 101m, 101m, 9m),
new CustomQuoteBar(Symbols.IBM, 99m, 100m, 98m, 99m, 10m, 99m, 100m, 98m, 99m, 10m),
};
dynamic dataFrame = converter.GetDataFrame(data);
var expectedColumnNames = new List<string>() {
"bidopen", "bidhigh", "bidlow", "bidclose", "bidsize",
"askopen", "askhigh", "asklow", "askclose", "asksize"
};
AssertDataFrameColumns(dataFrame, data.Count, expectedColumnNames);
}
public class TestInnerData1
{
public decimal DecimalValue1 { get; set; }
public string StringValue1 { get; set; }
}
[PandasNonExpandable]
public class TestInnerData2
{
public decimal DecimalValue2 { get; set; }
public string StringValue2 { get; set; }
}
public class TestData1 : BaseData
{
public TestInnerData1 TestInnerData1 { get; set; }
public TestInnerData2 TestInnerData2 { get; set; }
[PandasNonExpandable]
public TestInnerData1 TestInnerData3 { get; set; }
public TestData1(Symbol symbol, decimal decimalValue1, string stringValue1, decimal decimalValue2, string stringValue2,
decimal decimalValue3, string stringValue3)
{
Symbol = symbol;
TestInnerData1 = new TestInnerData1
{
DecimalValue1 = decimalValue1,
StringValue1 = stringValue1,
};
TestInnerData2 = new TestInnerData2
{
DecimalValue2 = decimalValue2,
StringValue2 = stringValue2,
};
TestInnerData3 = new TestInnerData1
{
DecimalValue1 = decimalValue3,
StringValue1 = stringValue3,
};
}
}
[Test]
public void DoesNotExpandMarkedPropertiesAndClasses()
{
var converter = new PandasConverter();
var data = new List<TestData1>
{
new TestData1(Symbols.IBM, 1m, "Test 1.1", 2m, "Test 1.2", 3m, "Test 1.3"),
new TestData1(Symbols.IBM, 10m, "Test 2.1", 20m, "Test 2.2", 30m, "Test 2.3"),
new TestData1(Symbols.IBM, 100m, "Test 3.1", 200m, "Test 3.2", 300m, "Test 3.3"),
};
dynamic dataFrame = converter.GetDataFrame(data);
var expectedColumnNames = new List<string>() { "decimalvalue1", "stringvalue1", "testinnerdata2", "testinnerdata3" };
AssertDataFrameColumns(dataFrame, data.Count, expectedColumnNames);
using var _ = Py.GIL();
foreach (var value in dataFrame["testinnerdata2"])
{
Assert.DoesNotThrow(() => value.As<TestInnerData2>());
}
foreach (var value in dataFrame["testinnerdata3"])
{
Assert.DoesNotThrow(() => value.As<TestInnerData1>());
}
}
private static void AssertDataFrameColumns(dynamic dataFrame, int dataCount, List<string> expectedColumnNames)
{
using var _ = Py.GIL();
Assert.AreEqual(dataCount, dataFrame.shape[0].As<int>());
Assert.AreEqual(expectedColumnNames.Count, dataFrame.shape[1].As<int>());
var columnNames = new List<string>();
foreach (var pandasColumn in dataFrame.columns.to_list())
{
columnNames.Add(pandasColumn.__str__().As<string>());
}
CollectionAssert.AreEquivalent(expectedColumnNames, columnNames);
}
private class TestInnerData3
{
public decimal TestValue1 { get; set; }
public decimal TestValue2 { get; set; }
[PandasIgnore]
public decimal IgnoredValue { get; set; }
}
private class TestData2 : BaseData
{
public TestInnerData3 InnerData { get; set; }
public string MainValue { get; set; }
public TestData2(Symbol symbol, string mainValue, decimal testValue1, decimal testValue2, decimal ignoredValue)
{
Symbol = symbol;
MainValue = mainValue;
InnerData = new TestInnerData3()
{
TestValue1 = testValue1,
TestValue2 = testValue2,
IgnoredValue = ignoredValue
};
}
}
[Test]
public void OmitsPropertiesMarkedToBeIgnored()
{
var converter = new PandasConverter();
var data = new List<TestData2>
{
new TestData2(Symbols.IBM, "Main value 1", 10m, 200m, 5m),
new TestData2(Symbols.IBM, "Main value 2", 20m, 300m, 10m),
new TestData2(Symbols.IBM, "Main value 3", 30m, 400m, 15m),
};
dynamic dataFrame = converter.GetDataFrame(data);
var expectedColumnNames = new List<string>() { "mainvalue", "testvalue1", "testvalue2" };
AssertDataFrameColumns(dataFrame, data.Count, expectedColumnNames);
}
[PandasIgnoreMembers]
private class TestIgnoreMembersBaseClass : BaseData
{
public decimal Decimal1 { get; set; }
public decimal Decimal2 { get; set; }
}
private class TestIgnoreMembersDerivedClass : TestIgnoreMembersBaseClass
{
public decimal Decimal3 { get; set; }
public decimal Decimal4 { get; set; }
}
[Test]
public void OmitsBaseClassMembers()
{
var converter = new PandasConverter();
var data = new List<TestIgnoreMembersDerivedClass>
{
new TestIgnoreMembersDerivedClass { Decimal1 = 1m, Decimal2 = 2m, Decimal3 = 3m, Decimal4 = 4m },
new TestIgnoreMembersDerivedClass { Decimal1 = 10m, Decimal2 = 20m, Decimal3 = 30m, Decimal4 = 40m },
new TestIgnoreMembersDerivedClass { Decimal1 = 100m, Decimal2 = 200m, Decimal3 = 300m, Decimal4 = 400m },
};
dynamic dataFrame = converter.GetDataFrame(data);
// BaseData and TestIgnoreMembersBaseClass members are ignored
var expectedColumnNames = new List<string>() { "decimal3", "decimal4" };
AssertDataFrameColumns(dataFrame, data.Count, expectedColumnNames);
}
private class TestRenameMembersClass : ISymbolProvider
{
[PandasColumn("TheSymbol")]
public Symbol Symbol { get; set; }
[PandasColumn("Decimal")]
public decimal DecimalValue { get; set; }
[PandasColumn("String")]
public string StringValue { get; set; }
}
[Test]
public void RenamesMemberNames()
{
var converter = new PandasConverter();
var data = new List<TestRenameMembersClass>
{
new TestRenameMembersClass { Symbol = Symbols.IBM, DecimalValue = 1m, StringValue = "Test 1" },
new TestRenameMembersClass { Symbol = Symbols.AAPL, DecimalValue = 10m, StringValue = "Test 2" },
new TestRenameMembersClass { Symbol = Symbols.SPY, DecimalValue = 100m, StringValue = "Test 3" },
};
dynamic dataFrame = converter.GetDataFrame(data);
var expectedColumnNames = new List<string>() { "thesymbol", "decimal", "string" };
AssertDataFrameColumns(dataFrame, data.Count, expectedColumnNames);
}
}
}
+101
View File
@@ -0,0 +1,101 @@
/*
* 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 System;
using NUnit.Framework;
using Python.Runtime;
using static QLNet.NumericHaganPricer;
namespace QuantConnect.Tests.Python
{
[TestFixture]
// TODO: Rename to PandasPythonTests, dedicate class to python tests under ./PandasTests directory
public class PandasIndexingTests
{
private dynamic _module;
private dynamic _pandasIndexingTests;
private dynamic _pandasDataFrameTests;
[SetUp]
public void Setup()
{
using (Py.GIL())
{
_module = Py.Import("PandasIndexingTests");
_pandasIndexingTests = _module.PandasIndexingTests();
_pandasDataFrameTests = _module.PandasDataFrameTests();
}
}
[Test]
public void IndexingDataFrameWithList()
{
using (Py.GIL())
{
Assert.DoesNotThrow((() => _pandasIndexingTests.test_indexing_dataframe_with_list()));
}
}
[Test]
public void ContainsUserMappedTickers()
{
using (Py.GIL())
{
PyObject result = _pandasDataFrameTests.test_contains_user_mapped_ticker();
var test = result.As<bool>();
Assert.IsTrue(test);
}
}
[TestCase("SPY WhatEver")]
[TestCase("Sharpe ratio")]
public void ContainsUserDefinedColumnsWithSpaces(string columnName)
{
using (Py.GIL())
{
PyObject result = _pandasDataFrameTests.test_contains_user_defined_columns_with_spaces(columnName);
var test = result.As<bool>();
Assert.IsTrue(test);
}
}
[Test]
public void ExpectedException()
{
using (Py.GIL())
{
PyObject result = _pandasDataFrameTests.test_expected_exception();
var exception = result.As<string>();
Assert.IsTrue(exception.Contains("No key found for either mapped or original key.", StringComparison.InvariantCulture), exception);
}
}
[Test]
public void ColumnEqualsOnlyMatchingString()
{
using (Py.GIL())
{
PyObject result = _pandasDataFrameTests.test_column_equals_only_matching_string();
var test = result.As<bool>();
Assert.IsTrue(test);
}
}
}
}
@@ -0,0 +1,76 @@
# 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.
from AlgorithmImports import *
from QuantConnect.Tests import *
from QuantConnect.Tests.Python import *
from PandasMapper import PandasColumn
# TODO: Rename to PandasResearchTests and keep this class for QB related tests; rename py module to PandasTests
class PandasIndexingTests():
def __init__(self):
self.qb = QuantBook()
self.qb.SetStartDate(2020, 1, 1)
self.qb.SetEndDate(2020, 1, 4)
self.symbol = self.qb.AddEquity("SPY", Resolution.Daily).Symbol
def test_indexing_dataframe_with_list(self):
symbols = [self.symbol]
self.history = self.qb.History(symbols, 30)
self.history = self.history['close'].unstack(level=0).dropna()
test = self.history[[self.symbol]]
return True
# Test class that sets up two dataframes to test on
class PandasDataFrameTests():
def __init__(self):
self.spy = Symbols.SPY
self.aapl = Symbols.AAPL
# Set our symbol cache
SymbolCache.Set("SPY", self.spy)
SymbolCache.Set("AAPL", self.aapl)
pdConverter = PandasConverter()
# Create our dataframes
self.spydf = pdConverter.GetDataFrame(PythonTestingUtils.GetSlices(self.spy))
def test_contains_user_mapped_ticker(self):
# Create a new DF that has a plain ticker, test that our mapper doesn't break
# searching for it.
df = pd.DataFrame({'spy': [2, 5, 8, 10]})
return 'spy' in df
def test_expected_exception(self):
# Try indexing a ticker that doesn't exist in this frame, but is still in our cache
try:
self.spydf['aapl']
except KeyError as e:
return str(e)
def test_contains_user_defined_columns_with_spaces(self, column_name):
# Adds a column, then try accessing it.
# If the colums has white spaces, it should not fail
df = self.spydf.copy()
df[column_name] = 1
try:
x = df[column_name]
return True
except:
return False
def test_column_equals_only_matching_string(self):
# A column label should only equal a matching string, never None/ints/floats
column = PandasColumn("shares")
return (not (column == None)) and (not (column == 0)) and (not (column == 123)) and (column == "shares")
@@ -0,0 +1,91 @@
# 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.
'''
To test this script directly you will need to import QuantConnect Dlls using clrloader from the appropriate
location, the code below shows how to do this. Otherwise you can run it directly from C# in Lean without it.
To run as a solo script, add the following code to your script
Requires:
clr-loader==0.1.6
pandas
*********** CODE ***********
import os
import sys
# Get to DLL location where we are testing, change as needed
fileDirectory = os.path.dirname(os.path.abspath(__file__))
dlldir = "../../bin/Debug"
dlldir = os.path.join(fileDirectory, dlldir)
# Move us to dll directory and add it to path
os.chdir(dlldir)
sys.path.append(dlldir)
# Tell PythonNet to use .dotnet 6
from pythonnet import set_runtime
import clr_loader
set_runtime(clr_loader.get_coreclr(os.path.join(dlldir, "QuantConnect.Lean.Launcher.runtimeconfig.json")))
'''
from clr import AddReference
AddReference("QuantConnect.Common")
AddReference("QuantConnect.Tests")
from QuantConnect import *
from QuantConnect.Python import PandasConverter
from QuantConnect.Tests import Symbols
from QuantConnect.Tests.Python import PythonTestingUtils
# Import our mapper which wraps core pandas functions (included in build dir)
import PandasMapper
import pandas as pd
# Get some dataframes from Lean to test on
spy = Symbols.SPY
aapl = Symbols.AAPL
SymbolCache.Set("SPY", spy)
SymbolCache.Set("AAPL", aapl)
pdConverter = PandasConverter()
slices = PythonTestingUtils.GetSlices(spy)
spydf = pdConverter.GetDataFrame(slices)
slices = PythonTestingUtils.GetSlices(aapl)
aapldf = pdConverter.GetDataFrame(slices)
def Test_Concat(dataFrame, dataFrame2, indexer):
newDataFrame = pd.concat([dataFrame, dataFrame2])
data = newDataFrame['lastprice'].unstack(level=0).iloc[-1][indexer]
if data is 0:
raise Exception('Data is zero')
def Test_Join(dataFrame, dataFrame2, indexer):
newDataFrame = dataFrame.join(dataFrame2, lsuffix='_')
base = newDataFrame['lastprice_'].unstack(level=0)
data = base.iloc[-1][indexer]
if data is 0:
raise Exception('Data is zero')
Test_Concat(spydf, aapldf, "spy")
Test_Concat(spydf, aapldf, spy)
Test_Concat(spydf, aapldf, str(spy.ID))
Test_Join(spydf, aapldf, "spy")
Test_Join(spydf, aapldf, spy)
Test_Join(spydf, aapldf, str(spy.ID))
+139
View File
@@ -0,0 +1,139 @@
/*
* 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.Algorithm;
using QuantConnect.Data.Market;
using QuantConnect.Python;
using QuantConnect.Securities;
using QuantConnect.Tests.Common.Securities;
using QuantConnect.Tests.Engine.DataFeeds;
using System;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class PortfolioCustomModelTests
{
[Test]
[TestCase(true)]
[TestCase(false)]
public void SetMarginCallModelSuccess(bool isChild)
{
var algorithm = new QCAlgorithm();
var portfolio = algorithm.Portfolio;
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.SetDateTime(new DateTime(2018, 8, 20, 15, 0, 0));
algorithm.Transactions.SetOrderProcessor(new FakeOrderProcessor());
var spy = algorithm.AddEquity("SPY", Resolution.Daily);
spy.SetMarketPrice(new Tick(algorithm.Time, Symbols.SPY, 100m, 100m));
// Test two custom buying power models.
// The first inherits from C# SecurityMarginModel and the other is 100% python
var code = isChild
? CreateCustomMarginCallModelFromSecurityMarginModelCode()
: CreateCustomMarginCallModelCode();
portfolio.SetMarginCallModel(CreateCustomMarginCallModel(code, portfolio));
Assert.IsAssignableFrom<MarginCallModelPythonWrapper>(portfolio.MarginCallModel);
bool issueMarginCallWarning;
var marginCallOrders = portfolio.MarginCallModel.GetMarginCallOrders(out issueMarginCallWarning);
if (isChild)
{
Assert.IsFalse(issueMarginCallWarning);
Assert.AreEqual(0, marginCallOrders.Count);
}
else
{
Assert.IsTrue(issueMarginCallWarning);
Assert.AreEqual(3, marginCallOrders.Count);
}
}
[Test]
public void SetMarginCallModelFails()
{
var algorithm = new QCAlgorithm();
var portfolio = algorithm.Portfolio;
// Renaming GetMarginCall will cause a NotImplementedException exception
var code = CreateCustomMarginCallModelCode();
code = code.Replace("GetMarginCall", "SetMarginCall");
var pyObject = CreateCustomMarginCallModel(code, portfolio);
Assert.Throws<NotImplementedException>(() => portfolio.SetMarginCallModel(CreateCustomMarginCallModel(code, portfolio)));
}
private PyObject CreateCustomMarginCallModel(string code, SecurityPortfolioManager portfolio)
{
using (Py.GIL())
{
var module = PyModule.FromString("CustomMarginCallModel", code);
dynamic CustomMarginCallModel = module.GetAttr("CustomMarginCallModel");
return CustomMarginCallModel(portfolio, null);
}
}
private string CreateCustomMarginCallModelCode() => @"
import os, sys
sys.path.append(os.getcwd())
from AlgorithmImports import *
class CustomMarginCallModel:
def __init__(self, portfolio, defaultOrderProperties):
self.portfolio = portfolio
self.defaultOrderProperties = defaultOrderProperties
def ExecuteMarginCall(self, generatedMarginCallOrders):
return []
def GenerateMarginCallOrder(self, security, netLiquidationValue, totalMargin, maintenanceMarginRequirement):
time = Extensions.ConvertToUtc(security.LocalTime, security.Exchange.TimeZone)
quantity = netLiquidationValue / security.Price
return SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, quantity, 0, 0, time, 'Margin Call', None)
def GetMarginCallOrders(self, issueMarginCallWarning):
issueMarginCallWarning = True
spy = self.portfolio.Securities['SPY']
totalPortfolioValue = self.portfolio.TotalPortfolioValue
totalMarginUsed = self.portfolio.TotalMarginUsed
order = self.GenerateMarginCallOrder(spy, totalPortfolioValue, totalMarginUsed, 0)
return [order, order, order], issueMarginCallWarning";
private string CreateCustomMarginCallModelFromSecurityMarginModelCode() => @"
import os, sys
sys.path.append(os.getcwd())
from AlgorithmImports import *
class CustomMarginCallModel(DefaultMarginCallModel):
def __init__(self, portfolio, defaultOrderProperties):
super().__init__(portfolio, defaultOrderProperties)
self.porfolio = portfolio
self.defaultOrderProperties = defaultOrderProperties
def GenerateMarginCallOrder(self, security, netLiquidationValue, totalMargin, maintenanceMarginRequirement):
time = Extensions.ConvertToUtc(security.LocalTime, security.Exchange.TimeZone)
quantity = netLiquidationValue / security.Price
return SubmitOrderRequest(OrderType.Market, security.Type, security.Symbol, quantity, 0, 0, time, 'Margin Call', None)";
}
}
+72
View File
@@ -0,0 +1,72 @@
/*
* 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 Python.Runtime;
using NUnit.Framework;
using System.Collections.Generic;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class PythonCollectionsTests
{
private static dynamic containsKeyTest;
private static dynamic containsTest;
private static string testModule = @"
def ContainsTest(key, collection):
if key in collection.Keys:
return True
return False
def ContainsKeyTest(key, collection):
return collection.ContainsKey(key)
";
[OneTimeSetUp]
public void Setup()
{
using (Py.GIL())
{
var pyModule = PyModule.FromString("module", testModule);
containsTest = pyModule.GetAttr("ContainsTest");
containsKeyTest = pyModule.GetAttr("ContainsKeyTest");
}
}
[TestCase("AAPL", false)]
[TestCase("SPY", true)]
public void Contains(string key, bool expected)
{
using (Py.GIL())
{
var dic = new Dictionary<string, object> { { "SPY", new object() } };
Assert.AreEqual(expected, (bool)containsTest(key, dic));
}
}
[TestCase("AAPL", false)]
[TestCase("SPY", true)]
public void ContainsKey(string key, bool expected)
{
using (Py.GIL())
{
var dic = new Dictionary<string, object> { { "SPY", new object() } };
Assert.AreEqual(expected, (bool)containsKeyTest(key, dic));
}
}
}
}
+237
View File
@@ -0,0 +1,237 @@
/*
* 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 System;
using NodaTime;
using Python.Runtime;
using NUnit.Framework;
using QuantConnect.Data;
using QuantConnect.Python;
using System.Collections.Generic;
using System.Reflection;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class PythonDataTests
{
[TestCase("value", "symbol")]
[TestCase("Value", "Symbol")]
public void ValueAndSymbol(string value, string symbol)
{
using (Py.GIL())
{
dynamic testModule = PyModule.FromString("testModule",
$@"
from AlgorithmImports import *
class CustomDataTest(PythonData):
def Reader(self, config, line, date, isLiveMode):
result = CustomDataTest()
result.{symbol} = config.Symbol
result.{value} = 10
result.time = datetime.strptime(""2022-05-05"", ""%Y-%m-%d"")
result.end_time = datetime.strptime(""2022-05-15"", ""%Y-%m-%d"")
return result");
var data = GetDataFromModule(testModule);
Assert.AreEqual(Symbols.SPY, data.Symbol);
Assert.AreEqual(10, data.Value);
Assert.AreEqual(Symbols.SPY, data.symbol);
Assert.AreEqual(10, data.value);
}
}
[TestCase("EndTime", "Time")]
[TestCase("endtime", "Time")]
[TestCase("end_time", "Time")]
[TestCase("EndTime", "time")]
[TestCase("endtime", "time")]
[TestCase("end_time", "time")]
public void TimeAndEndTimeCanBeSet(string endtime, string time)
{
using (Py.GIL())
{
dynamic testModule = PyModule.FromString("testModule",
$@"
from AlgorithmImports import *
class CustomDataTest(PythonData):
def Reader(self, config, line, date, isLiveMode):
result = CustomDataTest()
result.Symbol = config.Symbol
result.Value = 10
result.{time} = datetime.strptime(""2022-05-05"", ""%Y-%m-%d"")
result.{endtime} = datetime.strptime(""2022-05-15"", ""%Y-%m-%d"")
return result");
var data = GetDataFromModule(testModule);
Assert.AreEqual(new DateTime(2022, 5, 5), data.Time);
Assert.AreEqual(new DateTime(2022, 5, 15), data.EndTime);
}
}
[TestCase("EndTime")]
[TestCase("endtime")]
[TestCase("end_time")]
public void OnlyEndTimeCanBeSet(string endtime)
{
using (Py.GIL())
{
dynamic testModule = PyModule.FromString("testModule",
$@"
from AlgorithmImports import *
class CustomDataTest(PythonData):
def Reader(self, config, line, date, isLiveMode):
result = CustomDataTest()
result.Symbol = config.Symbol
result.Value = 10
result.{endtime} = datetime.strptime(""2022-05-05"", ""%Y-%m-%d"")
return result");
var data = GetDataFromModule(testModule);
Assert.AreEqual(new DateTime(2022, 5, 5), data.Time);
Assert.AreEqual(new DateTime(2022, 5, 5), data.EndTime);
}
}
[TestCase("Time")]
[TestCase("time")]
public void OnlyTimeCanBeSet(string time)
{
using (Py.GIL())
{
dynamic testModule = PyModule.FromString("testModule",
$@"
from AlgorithmImports import *
class CustomDataTest(PythonData):
def Reader(self, config, line, date, isLiveMode):
result = CustomDataTest()
result.Symbol = config.Symbol
result.Value = 10
result.{time} = datetime.strptime(""2022-05-05"", ""%Y-%m-%d"")
return result");
var data = GetDataFromModule(testModule);
Assert.AreEqual(new DateTime(2022, 5, 5), data.Time);
Assert.AreEqual(new DateTime(2022, 5, 5), data.EndTime);
}
}
public class TestPythonData : PythonData
{
private static void Throw()
{
throw new Exception("TestPythonData.Throw()");
}
public override bool RequiresMapping()
{
Throw();
return true;
}
public override bool IsSparseData()
{
Throw();
return true;
}
public override Resolution DefaultResolution()
{
Throw();
return Resolution.Daily;
}
public override List<Resolution> SupportedResolutions()
{
Throw();
return new List<Resolution> { Resolution.Daily };
}
public override BaseData Reader(SubscriptionDataConfig config, string line, DateTime date, bool isLiveMode)
{
Throw();
return new TestPythonData();
}
public override SubscriptionDataSource GetSource(SubscriptionDataConfig config, DateTime date, bool isLiveMode)
{
Throw();
return new SubscriptionDataSource("test", SubscriptionTransportMedium.LocalFile);
}
}
[TestCase("RequiresMapping")]
[TestCase("IsSparseData")]
[TestCase("DefaultResolution")]
[TestCase("SupportedResolutions")]
[TestCase("Reader")]
[TestCase("GetSource")]
public void CallsCSharpMethodsIfNotDefinedInPython(string methodName)
{
using (Py.GIL())
{
dynamic testModule = PyModule.FromString("testModule",
$@"
from AlgorithmImports import *
from QuantConnect.Tests.Python import *
class CustomDataClass(PythonDataTests.TestPythonData):
pass");
var customDataClass = testModule.GetAttr("CustomDataClass");
var type = Extensions.CreateType(customDataClass);
var data = new PythonData(customDataClass());
var args = Array.Empty<object>();
var methodArgsType = Array.Empty<Type>();
if (methodName.Equals("Reader", StringComparison.OrdinalIgnoreCase))
{
var config = new SubscriptionDataConfig(type, Symbols.SPY, Resolution.Daily, DateTimeZone.Utc,
DateTimeZone.Utc, false, false, false, isCustom: true);
args = new object[] { config, "line", DateTime.MinValue, false };
methodArgsType = new[] { typeof(SubscriptionDataConfig), typeof(string), typeof(DateTime), typeof(bool) };
}
else if (methodName.Equals("GetSource", StringComparison.OrdinalIgnoreCase))
{
var config = new SubscriptionDataConfig(type, Symbols.SPY, Resolution.Daily, DateTimeZone.Utc,
DateTimeZone.Utc, false, false, false, isCustom: true);
args = new object[] { config, DateTime.MinValue, false };
methodArgsType = new[] { typeof(SubscriptionDataConfig), typeof(DateTime), typeof(bool) };
}
var exception = Assert.Throws<TargetInvocationException>(() => typeof(PythonData).GetMethod(methodName, methodArgsType).Invoke(data, args));
Assert.AreEqual($"TestPythonData.Throw()", exception.InnerException.Message);
}
}
private static BaseData GetDataFromModule(dynamic testModule)
{
var type = Extensions.CreateType(testModule.GetAttr("CustomDataTest"));
var customDataTest = new PythonData(testModule.GetAttr("CustomDataTest")());
var config = new SubscriptionDataConfig(type, Symbols.SPY, Resolution.Daily, DateTimeZone.Utc,
DateTimeZone.Utc, false, false, false, isCustom: true);
return customDataTest.Reader(config, "something", DateTime.UtcNow, false);
}
}
}
+109
View File
@@ -0,0 +1,109 @@
/*
* 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 System;
using Python.Runtime;
using NUnit.Framework;
using System.Threading;
using QuantConnect.Orders;
using QuantConnect.Logging;
using QuantConnect.Securities;
using QuantConnect.Orders.Fees;
using QuantConnect.Tests.Common.Securities;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class PythonMemoryLeakTests
{
private Security _security;
private static DateTime orderDateTime;
[SetUp]
public void SetUp()
{
_security = SecurityTests.GetSecurity();
orderDateTime = new DateTime(2017, 2, 2, 13, 0, 0);
var reference = orderDateTime;
var referenceUtc = reference.ConvertToUtc(TimeZones.NewYork);
var timeKeeper = new TimeKeeper(referenceUtc);
_security.SetLocalTimeKeeper(timeKeeper.GetLocalTimeKeeper(TimeZones.NewYork));
}
[Test]
public void DoesNotLeak()
{
using (Py.GIL())
{
var module = PyModule.FromString(Guid.NewGuid().ToString(),
@"
from AlgorithmImports import *
def jose(security):
security.SetFeeModel(MakerTakerModel())
class MakerTakerModel(FeeModel):
def __init__(self, maker = -.0016, taker = .003):
self.maker = maker
self.taker = taker
def GetOrderFee(self, parameters: OrderFeeParameters) -> OrderFee:
qty = parameters.Order.Quantity
ord_type = parameters.Order.Type
# fee_in_usd = .0008
# make_ps = -.0016 #Rebate
# take_ps = .003
if ord_type in [OrderType.Market, OrderType.StopMarket]:
fee_usd = self.taker * qty
else:
fee_usd = self.maker * qty
return OrderFee(CashAmount(fee_usd, 'USD'))");
module.GetAttr("jose").Invoke(_security.ToPython());
var parameters = new OrderFeeParameters(_security, new MarketOrder(_security.Symbol, 1, orderDateTime));
// warmup
var result = _security.FeeModel.GetOrderFee(parameters);
Assert.IsNotNull(result);
// let the system stabilize
Thread.Sleep(1000);
var start = GC.GetTotalMemory(true);
for (var i = 0; i < 50000; i++)
{
result = _security.FeeModel.GetOrderFee(parameters);
Assert.IsNotNull(result);
if (i % 10000 == 0)
{
Log.Debug($"Memory: {GC.GetTotalMemory(true)}");
}
}
Thread.Sleep(1000);
var end = GC.GetTotalMemory(true);
var message = $"Start: {start}. End {end}. Variation {((end - start) / (decimal)start * 100).RoundToSignificantDigits(2)}%";
Log.Debug(message);
// 5% noise, leak was >10%
Assert.LessOrEqual(end, start * 1.05, message);
}
}
}
}
+154
View File
@@ -0,0 +1,154 @@
/*
* 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 System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Algorithm;
using QuantConnect.Data.Market;
using QuantConnect.Data.UniverseSelection;
using QuantConnect.Securities;
using QuantConnect.Statistics;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class PythonOptionTests
{
[Test]
public void PythonFilterFunctionReturnsList()
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
var spyOption = algorithm.AddOption("SPY");
using (Py.GIL())
{
//Filter function that returns a list of symbols
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"def filter(universe):\n" +
" universe = universe.WeeklysOnly().Expiration(0, 10)\n" +
" return [symbol for symbol in universe\n"+
" if symbol.ID.OptionRight != OptionRight.Put\n" +
" and universe.Underlying.Price - symbol.ID.StrikePrice < 10]\n"
);
var filterFunction = module.GetAttr("filter");
Assert.DoesNotThrow(() => spyOption.SetFilter(filterFunction));
}
}
[Test]
public void PythonFilterFunctionReturnsUniverse()
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
var spyOption = algorithm.AddOption("SPY");
using (Py.GIL())
{
//Filter function that returns a OptionFilterUniverse
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"def filter(universe):\n" +
" universe = universe.WeeklysOnly().Expiration(0, 5)\n" +
" return universe"
);
var filterFunction = module.GetAttr("filter");
Assert.DoesNotThrow(() => spyOption.SetFilter(filterFunction));
}
}
[Test]
public void PythonFilterFunctionReturnsNone()
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
var spyOption = algorithm.AddOption("SPY");
using (Py.GIL())
{
//Filter function that modifies the universe in place and returns None:
//the return value is only necessary for chaining
var module = PyModule.FromString(Guid.NewGuid().ToString(),
"def filter(universe):\n" +
" universe.strikes(-20, 20).expiration(0, 10)\n"
);
var filterFunction = module.GetAttr("filter");
spyOption.SetFilter(filterFunction);
}
var underlying = new Tick { Value = 10m, Time = new DateTime(2016, 12, 29) };
var symbols = new[]
{
// within the 0-10 days expiration window
Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, OptionRight.Call, 10, new DateTime(2017, 01, 04)),
Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, OptionRight.Put, 10, new DateTime(2017, 01, 06)),
// beyond the 0-10 days expiration window
Symbol.CreateOption("SPY", Market.USA, OptionStyle.American, OptionRight.Call, 10, new DateTime(2017, 01, 20)),
};
var data = symbols.Select(x => new OptionUniverse() { Symbol = x }).ToList();
var filtered = spyOption.ContractFilter.Filter(new OptionFilterUniverse(spyOption, data, underlying)).ToList();
Assert.AreEqual(2, filtered.Count);
Assert.AreEqual(symbols[0], filtered[0].Symbol);
Assert.AreEqual(symbols[1], filtered[1].Symbol);
}
[Test]
public void FilterReturnsUniverseRegression()
{
var parameter = new RegressionTests.AlgorithmStatisticsTestParameters("FilterUniverseRegressionAlgorithm",
new Dictionary<string, string> {
{PerformanceMetrics.TotalOrders, "2"},
{"Average Win", "0%"},
{"Average Loss", "-0.02%"},
{"Compounding Annual Return", "-1.521%"},
{"Drawdown", "0.000%"},
{"Expectancy", "-1"},
{"End Equity", "99979"},
{"Net Profit", "-0.021%"},
{"Sharpe Ratio", "0"},
{"Probabilistic Sharpe Ratio", "0%"},
{"Loss Rate", "100%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0"},
{"Beta", "0"},
{"Annual Standard Deviation", "0"},
{"Annual Variance", "0"},
{"Information Ratio", "0"},
{"Tracking Error", "0"},
{"Treynor Ratio", "0"},
{"Total Fees", "$1.00"},
{"OrderListHash", "22f0bc8a92f13dfa5d16c507824e2b68"}
},
Language.Python,
AlgorithmStatus.Completed);
AlgorithmRunner.RunLocalBacktest(parameter.Algorithm,
parameter.Statistics,
parameter.Language,
parameter.ExpectedFinalStatus);
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,39 @@
/*
* 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 System.Collections.Generic;
using NUnit.Framework;
using QuantConnect.Statistics;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class PythonRegressionAlgorithmsTests
{
[Test]
public void SelectUniverseSymbolsFromIDRegressionAlgorithm()
{
var parameters = new RegressionTests.AlgorithmStatisticsTestParameters("SelectUniverseSymbolsFromIDRegressionAlgorithm",
new Dictionary<string, string> { {PerformanceMetrics.TotalOrders, "0"} },
Language.Python,
AlgorithmStatus.Completed);
AlgorithmRunner.RunLocalBacktest(parameters.Algorithm,
parameters.Statistics,
parameters.Language,
parameters.ExpectedFinalStatus);
}
}
}
+61
View File
@@ -0,0 +1,61 @@
/*
* 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 QuantConnect.Statistics;
using System.Collections.Generic;
namespace QuantConnect.Tests.Common
{
[TestFixture]
public class PythonSliceGetByTypeTests
{
[Test]
public void RunPythonSliceGetByTypeRegressionAlgorithm()
{
var parameter = new RegressionTests.AlgorithmStatisticsTestParameters("SliceGetByTypeRegressionAlgorithm",
new Dictionary<string, string> {
{PerformanceMetrics.TotalOrders, "1"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "284.284%"},
{"Drawdown", "2.200%"},
{"Expectancy", "0"},
{"Net Profit", "1.736%"},
{"Sharpe Ratio", "8.86"},
{"Probabilistic Sharpe Ratio", "67.459%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "-0.004"},
{"Beta", "0.997"},
{"Annual Standard Deviation", "0.222"},
{"Annual Variance", "0.049"},
{"Information Ratio", "-14.547"},
{"Tracking Error", "0.001"},
{"Treynor Ratio", "1.972"},
{"Total Fees", "$3.45"},
{"OrderListHash", "275925e122dc6f40501d1e3f35339e26"}
},
Language.Python,
AlgorithmStatus.Completed);
AlgorithmRunner.RunLocalBacktest(parameter.Algorithm,
parameter.Statistics,
parameter.Language,
parameter.ExpectedFinalStatus);
}
}
}
+53
View File
@@ -0,0 +1,53 @@
/*
* 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 System;
using System.Collections.Generic;
using System.Linq;
using QuantConnect.Data;
using QuantConnect.Data.Market;
namespace QuantConnect.Tests.Python
{
public static class PythonTestingUtils
{
public static dynamic GetSlices(Symbol symbol)
{
var slices = Enumerable
.Range(0, 100)
.Select(
i =>
{
var time = new DateTime(2013, 10, 7).AddMilliseconds(14400000 + i * 10000);
return new Slice(
time,
new List<BaseData> {
new Tick
{
Time = time,
Symbol = symbol,
Value = 167 + i / 10,
Quantity = 1 + i * 10,
Exchange = "T"
}
}, time
);
}
);
return slices;
}
}
}
+52
View File
@@ -0,0 +1,52 @@
/*
* 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 Python.Runtime;
using NUnit.Framework;
using QuantConnect.Python;
using System.Threading.Tasks;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class PythonThreadingTests
{
[Test]
public void ImportsCanBeExecutedFromDifferentThreads()
{
PythonInitializer.Initialize();
Task.Factory.StartNew(() =>
{
using (Py.GIL())
{
var module = Py.Import("Test_MethodOverload");
module.GetAttr("Test_MethodOverload").Invoke();
}
}).Wait();
PythonInitializer.Initialize();
Task.Factory.StartNew(() =>
{
using (Py.GIL())
{
var module = Py.Import("Test_AlgorithmPythonWrapper");
module.GetAttr("Test_AlgorithmPythonWrapper").Invoke();
}
}).Wait();
}
}
}
@@ -0,0 +1,70 @@
/*
* 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 System;
using System.IO;
using Python.Runtime;
using NUnit.Framework;
using QuantConnect.Python;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class PythonVirtualEnvironmentTests
{
[TestCase(null)]
[TestCase("/something")]
[TestCase("non existing env")]
public void InvalidVirtualEnvironment(string venv)
{
Assert.IsFalse(PythonInitializer.ActivatePythonVirtualEnvironment(venv));
}
[Test]
public void VirtualEnvironment()
{
if (Directory.Exists("testenv"))
{
Directory.Delete("testenv", true);
}
using (Py.GIL())
{
PythonEngine.Exec("import venv;venv.create(\"testenv\", system_site_packages=True)");
}
Assert.IsTrue(PythonInitializer.ActivatePythonVirtualEnvironment("testenv"));
}
[Test, Explicit("Requires a virtual env setup")]
public void AssertVirtualEnvironment()
{
Assert.IsTrue(PythonInitializer.ActivatePythonVirtualEnvironment("/lean-testenv"));
using (Py.GIL())
{
var code = @"import lean
def assertLeanVersion():
return lean.__version__";
var module = PyModule.FromString(Guid.NewGuid().ToString(), code);
dynamic assertVersion = module.GetAttr("assertLeanVersion");
Assert.AreEqual("1.0.221", (string)assertVersion());
}
}
}
}
+391
View File
@@ -0,0 +1,391 @@
/*
* 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 System;
using NUnit.Framework;
using Python.Runtime;
using QuantConnect.Python;
using System.Collections.Generic;
using QuantConnect.Statistics;
using System.Reflection;
namespace QuantConnect.Tests.Python
{
public static class PythonWrapperTests
{
[TestFixture]
public class ValidateImplementationOf
{
[TestCase(nameof(MissingMethodOne), "ModelMissingMethodOne", "MethodOne")]
[TestCase(nameof(MissingProperty), "ModelMissingProperty", "PropertyOne")]
public void ThrowsOnMissingMember(string moduleName, string className, string missingMemberName)
{
using (Py.GIL())
{
var moduleStr = GetFieldValue(moduleName);
var module = PyModule.FromString(nameof(ValidateImplementationOf), moduleStr);
var model = module.GetAttr(className).Invoke();
Assert.That(() => model.ValidateImplementationOf<IModel>(), Throws
.Exception.InstanceOf<NotImplementedException>().With.Message.Contains(missingMemberName));
}
}
[TestCase(nameof(FullyImplemented), "FullyImplementedModel")]
[TestCase(nameof(FullyImplementedSnakeCase), "FullyImplementedSnakeCaseModel")]
[TestCase(nameof(FullyImplementedWithPropertyAsField), "FullyImplementedModelWithPropertyAsField")]
[TestCase(nameof(DerivedFromCsharp), "DerivedFromCSharpModel")]
public void DoesNotThrowWhenInterfaceFullyImplemented(string moduleName, string className)
{
using (Py.GIL())
{
var moduleStr = GetFieldValue(moduleName);
var module = PyModule.FromString(nameof(ValidateImplementationOf), moduleStr);
var model = module.GetAttr(className).Invoke();
Assert.That(() => model.ValidateImplementationOf<IModel>(), Throws.Nothing);
}
}
[Test]
public void SettlementModelPythonWrapperWorks()
{
var results = AlgorithmRunner.RunLocalBacktest("CustomSettlementModelRegressionAlgorithm",
new Dictionary<string, string>()
{
{PerformanceMetrics.TotalOrders, "0"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "119.460%"},
{"Drawdown", "0%"},
{"Expectancy", "0"},
{"Net Profit", "1.010%"},
{"Sharpe Ratio", "-5.989"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "1.011%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "-0.411"},
{"Beta", "-0.033"},
{"Annual Standard Deviation", "0.079"},
{"Annual Variance", "0.006"},
{"Information Ratio", "-10.086"},
{"Tracking Error", "0.243"},
{"Treynor Ratio", "14.619"},
{"Total Fees", "$0.00"},
{"Estimated Strategy Capacity", "$0"},
{"Lowest Capacity Asset", ""},
{"Portfolio Turnover", "0%"},
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
},
Language.Python,
AlgorithmStatus.Completed,
algorithmLocation: "../../../Algorithm.Python/CustomSettlementModelRegressionAlgorithm.py"
);
}
[Test]
public void BenchmarkModelPythonWrapperWorks()
{
var results = AlgorithmRunner.RunLocalBacktest("CustomBenchmarkRegressionAlgorithm",
new Dictionary<string, string>()
{
{PerformanceMetrics.TotalOrders, "0"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "0%"},
{"Drawdown", "0%"},
{"Expectancy", "0"},
{"Net Profit", "0%"},
{"Sharpe Ratio", "0"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "0%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "0"},
{"Beta", "0"},
{"Annual Standard Deviation", "0"},
{"Annual Variance", "0"},
{"Information Ratio", "-6.654540153820717E+27"},
{"Tracking Error", "11.906"},
{"Treynor Ratio", "0"},
{"Total Fees", "$0.00"},
{"Estimated Strategy Capacity", "$0"},
{"Lowest Capacity Asset", ""},
{"Portfolio Turnover", "0%"},
{"OrderListHash", "d41d8cd98f00b204e9800998ecf8427e"}
},
Language.Python,
AlgorithmStatus.Completed,
algorithmLocation: "../../../Algorithm.Python/CustomBenchmarkRegressionAlgorithm.py"
);
}
[Test]
public void PEP8StyleAlgorithmsImplementationsWork()
{
AlgorithmRunner.RunLocalBacktest("PEP8StyleBasicAlgorithm",
new Dictionary<string, string>()
{
{"Total Orders", "1"},
{"Average Win", "0%"},
{"Average Loss", "0%"},
{"Compounding Annual Return", "271.453%"},
{"Drawdown", "2.200%"},
{"Expectancy", "0"},
{"Start Equity", "100000"},
{"End Equity", "101691.92"},
{"Net Profit", "1.692%"},
{"Sharpe Ratio", "8.854"},
{"Sortino Ratio", "0"},
{"Probabilistic Sharpe Ratio", "67.459%"},
{"Loss Rate", "0%"},
{"Win Rate", "0%"},
{"Profit-Loss Ratio", "0"},
{"Alpha", "-0.005"},
{"Beta", "0.996"},
{"Annual Standard Deviation", "0.222"},
{"Annual Variance", "0.049"},
{"Information Ratio", "-14.565"},
{"Tracking Error", "0.001"},
{"Treynor Ratio", "1.97"},
{"Total Fees", "$3.44"},
{"Estimated Strategy Capacity", "$56000000.00"},
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
{"Portfolio Turnover", "19.93%"},
{"OrderListHash", "3da9fa60bf95b9ed148b95e02e0cfc9e"}
},
Language.Python,
AlgorithmStatus.Completed,
algorithmLocation: "../../../Algorithm.Python/PEP8StyleBasicAlgorithm.py"
);
}
[Test]
public void PEP8StyleCustomModelsWork()
{
AlgorithmRunner.RunLocalBacktest("CustomModelsPEP8Algorithm",
new Dictionary<string, string>()
{
{"Total Orders", "63"},
{"Average Win", "0.10%"},
{"Average Loss", "-0.06%"},
{"Compounding Annual Return", "-7.101%"},
{"Drawdown", "2.400%"},
{"Expectancy", "-0.181"},
{"Start Equity", "100000"},
{"End Equity", "99383.07"},
{"Net Profit", "-0.617%"},
{"Sharpe Ratio", "-1.441"},
{"Sortino Ratio", "-1.977"},
{"Probabilistic Sharpe Ratio", "20.329%"},
{"Loss Rate", "69%"},
{"Win Rate", "31%"},
{"Profit-Loss Ratio", "1.64"},
{"Alpha", "-0.101"},
{"Beta", "0.121"},
{"Annual Standard Deviation", "0.04"},
{"Annual Variance", "0.002"},
{"Information Ratio", "-4.109"},
{"Tracking Error", "0.102"},
{"Treynor Ratio", "-0.475"},
{"Total Fees", "$62.23"},
{"Estimated Strategy Capacity", "$52000000.00"},
{"Lowest Capacity Asset", "SPY R735QTJ8XC9X"},
{"Portfolio Turnover", "197.93%"},
{"OrderListHash", "fe01fe4923e8856fe3376ece636b4e23"}
},
Language.Python,
AlgorithmStatus.Completed,
algorithmLocation: "../../../Algorithm.Python/CustomModelsPEP8Algorithm.py"
);
}
private static string GetFieldValue(string name)
{
return typeof(ValidateImplementationOf).GetField(name, BindingFlags.Static | BindingFlags.NonPublic).GetValue(null) as string;
}
private const string FullyImplemented =
@"
from clr import AddReference
AddReference('QuantConnect.Tests')
from QuantConnect.Tests.Python import *
class FullyImplementedModel:
def MethodOne():
pass
def MethodTwo():
pass
@property
def PropertyOne(self):
return 'value'
";
private const string FullyImplementedSnakeCase =
@"
from clr import AddReference
AddReference('QuantConnect.Tests')
from QuantConnect.Tests.Python import *
class FullyImplementedSnakeCaseModel:
def method_one():
pass
def method_two():
pass
@property
def property_one(self):
pass
";
private const string FullyImplementedWithPropertyAsField =
@"
from clr import AddReference
AddReference('QuantConnect.Tests')
from QuantConnect.Tests.Python import *
class FullyImplementedModelWithPropertyAsField:
def method_one():
pass
def method_two():
pass
def __init__(self):
self.property_one = 'value'
";
private const string DerivedFromCsharp =
@"
from clr import AddReference
AddReference('QuantConnect.Tests')
from QuantConnect.Tests.Python import *
class DerivedFromCSharpModel(PythonWrapperTests.ValidateImplementationOf.Model):
def MethodOne():
pass
@property
def PropertyOne(self):
return 'value'
";
private const string MissingMethodOne =
@"
from clr import AddReference
AddReference('QuantConnect.Tests')
from QuantConnect.Tests.Python import *
class ModelMissingMethodOne:
def MethodTwo():
pass
@property
def PropertyOne(self):
return 'value'
";
private const string MissingProperty =
@"
from clr import AddReference
AddReference('QuantConnect.Tests')
from QuantConnect.Tests.Python import *
class ModelMissingProperty:
def MethodOne():
pass
def MethodTwo():
pass
";
interface IModel
{
string PropertyOne { get; set; }
void MethodOne();
void MethodTwo();
}
public class Model : IModel
{
public string PropertyOne { get; set; }
public void MethodOne()
{
}
public void MethodTwo()
{
}
}
}
[TestFixture]
public class InvokeTests
{
[Test]
public void InvokesCSharpMethod()
{
using (Py.GIL())
{
var module = PyModule.FromString(nameof(InvokeTests), InvokeModule);
var model = module.GetAttr("PythonInvokeTestsModel").Invoke();
Assert.That(model.InvokeMethod<int>("AddThreeNumbers", 1, 2, 3), Is.EqualTo(6));
}
}
[Test]
public void InvokesPythonMethod()
{
using (Py.GIL())
{
var module = PyModule.FromString(nameof(InvokeTests), InvokeModule);
var model = module.GetAttr("PythonInvokeTestsModel").Invoke();
Assert.That(model.InvokeMethod<int>("AddTwoNumbers", 1, 2), Is.EqualTo(3));
}
}
private const string InvokeModule =
@"
from clr import AddReference
AddReference('QuantConnect.Tests')
from QuantConnect.Tests.Python import *
class PythonInvokeTestsModel(PythonWrapperTests.InvokeTests.InvokeTestsModel):
def add_two_numbers(self, a, b):
return a + b
";
public class InvokeTestsModel
{
public int AddTwoNumbers(int a, int b)
{
throw new NotImplementedException();
}
public int AddThreeNumbers(int a, int b, int c)
{
return a + b + c;
}
}
}
}
}
+158
View File
@@ -0,0 +1,158 @@
/*
* 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.Algorithm;
using QuantConnect.Data;
using QuantConnect.Data.Market;
using QuantConnect.Python;
using QuantConnect.Securities;
using QuantConnect.Securities.Equity;
using QuantConnect.Tests.Common.Securities;
using System;
using QuantConnect.Tests.Engine.DataFeeds;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class SecurityCustomModelTests
{
[Test]
[TestCase(true)]
[TestCase(false)]
public void SetBuyingPowerModelSuccess(bool isChild)
{
var algorithm = new QCAlgorithm();
algorithm.SubscriptionManager.SetDataManager(new DataManagerStub(algorithm));
algorithm.SetDateTime(new DateTime(2018, 8, 20, 15, 0, 0));
algorithm.Transactions.SetOrderProcessor(new FakeOrderProcessor());
var spy = algorithm.AddEquity("SPY", Resolution.Daily);
spy.SetMarketPrice(new Tick(algorithm.Time, Symbols.SPY, 100m, 100m));
// Test two custom buying power models.
// The first inherits from C# SecurityMarginModel and the other is 100% python
var code = isChild
? CreateCustomBuyingPowerModelFromSecurityMarginModelCode()
: CreateCustomBuyingPowerModelCode();
spy.SetBuyingPowerModel(CreateCustomBuyingPowerModel(code));
Assert.IsAssignableFrom<BuyingPowerModelPythonWrapper>(spy.MarginModel);
Assert.AreEqual(1, spy.MarginModel.GetLeverage(spy));
spy.SetLeverage(2);
Assert.AreEqual(2, spy.MarginModel.GetLeverage(spy));
var quantity = algorithm.CalculateOrderQuantity(spy.Symbol, 1m);
Assert.AreEqual(isChild ? 100 : 200, quantity);
}
[Test]
public void SetBuyingPowerModelFails()
{
var spy = GetSecurity<Equity>(Symbols.SPY, Resolution.Daily);
// Renaming GetMaximumOrderQuantityForTargetDeltaBuyingPower will cause a NotImplementedException exception
var code = CreateCustomBuyingPowerModelCode();
code = code.Replace("GetMaximumOrderQuantityForDeltaBuyingPower", "AnotherName");
var pyObject = CreateCustomBuyingPowerModel(code);
Assert.Throws<NotImplementedException>(() => spy.SetBuyingPowerModel(pyObject));
}
private PyObject CreateCustomBuyingPowerModel(string code)
{
using (Py.GIL())
{
var module = PyModule.FromString("CustomBuyingPowerModel", code);
return module.GetAttr("CustomBuyingPowerModel").Invoke();
}
}
private string CreateCustomBuyingPowerModelCode() => @"
import os, sys
sys.path.append(os.getcwd())
from AlgorithmImports import *
class CustomBuyingPowerModel:
def __init__(self):
self.margin = 1.0
def GetBuyingPower(self, context):
return BuyingPower(context.Portfolio.MarginRemaining)
def GetMaximumOrderQuantityForDeltaBuyingPower(self, context):
return GetMaximumOrderQuantityResult(200)
def GetLeverage(self, security):
return 1.0 / self.margin
def GetMaximumOrderQuantityForTargetBuyingPower(self, context):
return GetMaximumOrderQuantityResult(200)
def GetReservedBuyingPowerForPosition(self, context):
return ReservedBuyingPowerForPosition(context.Security.Holdings.AbsoluteHoldingsCost * self.margin)
def HasSufficientBuyingPowerForOrder(self, context):
return HasSufficientBuyingPowerForOrderResult(True)
def GetMaintenanceMargin(self, context):
return None
def GetInitialMarginRequirement(self, context):
return None
def GetInitialMarginRequiredForOrder(self, context):
return None
def SetLeverage(self, security, leverage):
self.margin = 1.0 / float(leverage)";
private string CreateCustomBuyingPowerModelFromSecurityMarginModelCode() => @"
import os, sys
sys.path.append(os.getcwd())
from AlgorithmImports import *
class CustomBuyingPowerModel(SecurityMarginModel):
def GetMaximumOrderQuantityForTargetBuyingPower(self, context):
return GetMaximumOrderQuantityResult(100)";
private Security GetSecurity<T>(Symbol symbol, Resolution resolution)
{
var subscriptionDataConfig = new SubscriptionDataConfig(
typeof(T),
symbol,
resolution,
TimeZones.Utc,
TimeZones.Utc,
true,
true,
false);
return new Security(
SecurityExchangeHours.AlwaysOpen(TimeZones.Utc),
subscriptionDataConfig,
new Cash(Currencies.USD, 0, 1m),
SymbolProperties.GetDefault(Currencies.USD),
ErrorCurrencyConverter.Instance,
RegisteredSecurityDataTypesProvider.Null,
new SecurityCache()
);
}
}
}
@@ -0,0 +1,59 @@
/*
* 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.AlgorithmFactory.Python.Wrappers;
using System;
using System.Collections.Generic;
using System.IO;
using QuantConnect.Orders;
using Moq;
using static QuantConnect.Tests.Engine.PerformanceBenchmarkAlgorithms;
using QuantConnect.Python;
using QuantConnect.Securities;
namespace QuantConnect.Tests.Python
{
[TestFixture]
public class SettlementModelPythonWrapperTests
{
[Test]
public void GetsDefaultUnsettledCashFromNone()
{
using (Py.GIL())
{
var testModule = PyModule.FromString("testModule",
@"
class CustomSettlementModel:
def ApplyFunds(self, parameters):
pass
def Scan(self, parameters):
pass
def GetUnsettledCash(self):
return None
");
var settlementModel = new SettlementModelPythonWrapper(testModule.GetAttr("CustomSettlementModel").Invoke());
var result = settlementModel.GetUnsettledCash();
Assert.AreEqual(default(CashAmount), result);
}
}
}
}