chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
@@ -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)";
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user