def test_post_makes_post_request_to_given_endpoint(requests_mock: RequestsMock) -> None: requests_mock.add(requests_mock.POST, API_BASE_URL + "endpoint", '{ "success": true }') api = APIClient(mock.Mock(), "123", "456") api.post("endpoint") assert len(requests_mock.calls) == 1 assert requests_mock.calls[0].request.url == API_BASE_URL + "endpoint"
def test_get_attaches_parameters_to_url(requests_mock: RequestsMock) -> None: requests_mock.add(requests_mock.GET, API_BASE_URL + "endpoint", '{ "success": true }') api = APIClient(mock.Mock(), "123", "456") api.get("endpoint", {"key1": "value1", "key2": "value2"}) assert len(requests_mock.calls) == 1 assert requests_mock.calls[0].request.url == API_BASE_URL + "endpoint?key1=value1&key2=value2"
def test_is_authenticated_returns_false_when_authenticated_request_fails(requests_mock: RequestsMock) -> None: requests_mock.assert_all_requests_are_fired = False requests_mock.add(requests_mock.GET, re.compile(".*"), body='{ "success": false }') requests_mock.add(requests_mock.POST, re.compile(".*"), body='{ "success": false }') api = APIClient(mock.Mock(), "123", "456") assert not api.is_authenticated()
def test_post_sets_body_of_request_as_form_data(requests_mock: RequestsMock) -> None: requests_mock.add(requests_mock.POST, API_BASE_URL + "endpoint", '{ "success": true }') api = APIClient(mock.Mock(), "123", "456") api.post("endpoint", {"key1": "value1", "key2": "value2"}, data_as_json=False) assert len(requests_mock.calls) == 1 assert requests_mock.calls[0].request.url == API_BASE_URL + "endpoint" assert requests_mock.calls[0].request.body == "key1=value1&key2=value2"
def test_post_sets_body_of_request_as_json(requests_mock: RequestsMock) -> None: requests_mock.add(requests_mock.POST, API_BASE_URL + "endpoint", '{ "success": true }') api = APIClient(mock.Mock(), "123", "456") api.post("endpoint", {"key1": "value1", "key2": "value2"}) assert len(requests_mock.calls) == 1 assert requests_mock.calls[0].request.url == API_BASE_URL + "endpoint" body = json.loads(requests_mock.calls[0].request.body) assert body["key1"] == "value1" assert body["key2"] == "value2"
def test_api_client_raises_authentication_error_on_http_500(method: str, requests_mock: RequestsMock) -> None: requests_mock.add(method.upper(), API_BASE_URL + "endpoint", status=500) api = APIClient(mock.Mock(), "123", "456") with pytest.raises(AuthenticationError): getattr(api, method)("endpoint")
def create_api_client() -> APIClient: user_id = USER_ID or os.getenv("QC_USER_ID", "") api_token = API_TOKEN or os.getenv("QC_API_TOKEN", "") if user_id == "" or api_token == "": pytest.skip("API credentials not specified") return APIClient(mock.Mock(), user_id, api_token)
def test_api_client_returns_data_when_success_is_true(method: str, requests_mock: RequestsMock) -> None: requests_mock.add(method.upper(), API_BASE_URL + "endpoint", '{ "success": true }') api = APIClient(mock.Mock(), "123", "456") response = getattr(api, method)("endpoint") assert "success" in response assert response["success"]
def test_api_client_raises_request_failed_error_on_failing_response_non_http_500( method: str, requests_mock: RequestsMock) -> None: requests_mock.add(method.upper(), API_BASE_URL + "endpoint", status=404) api = APIClient(mock.Mock(), HTTPClient(mock.Mock()), "123", "456") with pytest.raises(RequestFailedError): getattr(api, method)("endpoint")
def test_api_client_raises_authentication_error_on_error_complaining_about_hash( method: str, requests_mock: RequestsMock) -> None: requests_mock.add( method.upper(), API_BASE_URL + "endpoint", '{ "success": false, "errors": ["Hash doesn\'t match."] }') api = APIClient(mock.Mock(), HTTPClient(mock.Mock()), "123", "456") with pytest.raises(AuthenticationError): getattr(api, method)("endpoint")
def test_api_client_sets_user_agent(method: str, requests_mock: RequestsMock) -> None: requests_mock.add(method.upper(), API_BASE_URL + "endpoint", '{ "success": true }') api = APIClient(mock.Mock(), "123", "456") getattr(api, method)("endpoint") assert len(requests_mock.calls) == 1 headers = requests_mock.calls[0].request.headers assert headers["User-Agent"].startswith("Lean CLI ")
def test_api_client_raises_request_failed_error_when_response_contains_internal_error( method: str, requests_mock: RequestsMock) -> None: requests_mock.add(method.upper(), API_BASE_URL + "endpoint", '{ "success": false, "Message": "Internal Error 21" }') api = APIClient(mock.Mock(), HTTPClient(mock.Mock()), "123", "456") with pytest.raises(RequestFailedError) as error: getattr(api, method)("endpoint") assert str(error.value) == "Internal Error 21"
def test_api_client_retries_request_when_response_is_http_5xx_error(method: str, status_code: int, expected_error: Any, requests_mock: RequestsMock) -> None: requests_mock.add(method.upper(), API_BASE_URL + "endpoint", status=status_code) api = APIClient(mock.Mock(), "123", "456") with pytest.raises(expected_error): getattr(api, method)("endpoint") requests_mock.assert_call_count(API_BASE_URL + "endpoint", 2)
def create_api_client() -> APIClient: if os.environ.get("QC_API", "") == "local": user_id = "123" api_token = "abc" else: user_id = USER_ID or os.environ.get("QC_USER_ID", "") api_token = API_TOKEN or os.environ.get("QC_API_TOKEN", "") if user_id == "" or api_token == "": pytest.skip("API credentials not specified") return APIClient(mock.Mock(), HTTPClient(mock.Mock()), user_id, api_token)
def test_api_client_makes_authenticated_requests(method: str, requests_mock: RequestsMock) -> None: requests_mock.add(method.upper(), API_BASE_URL + "endpoint", '{ "success": true }') api = APIClient(mock.Mock(), "123", "456") getattr(api, method)("endpoint") assert len(requests_mock.calls) == 1 headers = requests_mock.calls[0].request.headers assert "Timestamp" in headers assert "Authorization" in headers assert headers["Authorization"].startswith("Basic ")
def test_cli() -> None: """Tests the CLI by actually calling it like a real user would do. Unlike "normal" tests, this file only contains a single test method which steps through all commands. This is done on purpose to make the test as close to what real users do as possible. """ user_id = USER_ID or os.environ.get("QC_USER_ID", "") api_token = API_TOKEN or os.environ.get("QC_API_TOKEN", "") if user_id == "" or api_token == "": pytest.skip("API credentials not specified") credentials_path = Path("~/.lean").expanduser() / "credentials" # Create an empty directory to perform tests in test_dir = Path(tempfile.mkdtemp()) # We use project names suffixed by a timestamp to prevent conflicts when we synchronize with the cloud timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") python_project_name = f"Python Project {timestamp}" csharp_project_name = f"CSharp Project {timestamp}" # Log in run_command(["lean", "login"], input=[user_id, api_token]) assert credentials_path.exists() assert json.loads(credentials_path.read_text(encoding="utf-8")) == { "user-id": user_id, "api-token": api_token } # Check that we are logged in run_command(["lean", "whoami"]) # Download sample data and LEAN configuration file run_command(["lean", "init"], cwd=test_dir, input=["python"]) assert (test_dir / "data").is_dir() assert (test_dir / "lean.json").is_file() # Generate random data # This is the first command that uses the LEAN Docker image, so we increase the timeout to have time to pull it generate_output = run_command([ "lean", "data", "generate", "--start", "20150101", "--symbol-count", "1", "--resolution", "Daily" ], cwd=test_dir, timeout=600) matches = re.findall( r"Begin data generation of 1 randomly generated Equity assets\.\.\.\r?\n\s*Symbol\[1]: ([A-Z]+)", generate_output) assert len(matches) == 1 assert (test_dir / "data" / "equity" / "usa" / "daily" / f"{matches[0].lower()}.zip").is_file() # Configure global settings run_command(["lean", "config", "set", "default-language", "csharp"]) run_command(["lean", "config", "get", "default-language"], expected_output="csharp") run_command(["lean", "config", "unset", "default-language"]) run_command(["lean", "config", "get", "default-language"], expected_return_code=1) run_command(["lean", "config", "set", "default-language", "python"]) run_command(["lean", "config", "get", "default-language"], expected_output="python") list_output = run_command(["lean", "config", "list"]) assert len(re.findall(r"default-language[ ]+[^ ] python", list_output)) == 1 # Create Python project run_command([ "lean", "create-project", "--language", "python", python_project_name ], cwd=test_dir) python_project_dir = test_dir / python_project_name assert (python_project_dir / "main.py").is_file() assert (python_project_dir / "research.ipynb").is_file() assert (python_project_dir / "config.json").is_file() assert (python_project_dir / ".vscode" / "launch.json").is_file() assert (python_project_dir / ".vscode" / "settings.json").is_file() assert (python_project_dir / ".idea" / f"{python_project_name}.iml").is_file() assert (python_project_dir / ".idea" / "misc.xml").is_file() assert (python_project_dir / ".idea" / "modules.xml").is_file() assert (python_project_dir / ".idea" / "workspace.xml").is_file() # Create C# project run_command([ "lean", "create-project", "--language", "csharp", csharp_project_name ], cwd=test_dir) csharp_project_dir = test_dir / csharp_project_name assert (csharp_project_dir / "Main.cs").is_file() assert (csharp_project_dir / "research.ipynb").is_file() assert (csharp_project_dir / "config.json").is_file() assert (csharp_project_dir / f"{csharp_project_name}.csproj").is_file() assert (csharp_project_dir / ".vscode" / "launch.json").is_file() # Add custom Python library run_command(["lean", "library", "add", python_project_name, "altair"], cwd=test_dir) assert (python_project_dir / "requirements.txt").is_file() assert f"altair==" in (python_project_dir / "requirements.txt").read_text(encoding="utf-8") # Cannot add custom Python library incompatible with Python 3.6 run_command(["lean", "library", "add", python_project_name, "PyS3DE"], cwd=test_dir, expected_return_code=1) # Cannot add custom Python library without version when it's not on PyPI run_command( ["lean", "library", "add", python_project_name, str(uuid.uuid4())], cwd=test_dir, expected_return_code=1) # Cannot add custom Python library with version when version is invalid run_command([ "lean", "library", "add", python_project_name, "matplotlib", "--version", "0.0.0.0.0.1" ], cwd=test_dir, expected_return_code=1) # Cannot add custom Python library with version when version is incompatible with Python 3.6 run_command([ "lean", "library", "add", python_project_name, "matplotlib", "--version", "3.4.2" ], cwd=test_dir, expected_return_code=1) # Add custom C# library run_command( ["lean", "library", "add", csharp_project_name, "Microsoft.ML"], cwd=test_dir) csproj_file = csharp_project_dir / f"{csharp_project_name}.csproj" assert 'Include="Microsoft.ML"' in csproj_file.read_text(encoding="utf-8") # Cannot add custom C# library without version when it's not on NuGet run_command( ["lean", "library", "add", csharp_project_name, str(uuid.uuid4())], cwd=test_dir, expected_return_code=1) # Copy over algorithms containing a SPY buy-and-hold strategy with custom libraries fixtures_dir = Path(__file__).parent / "fixtures" shutil.copy(fixtures_dir / "local" / "main.py", python_project_dir / "main.py") shutil.copy(fixtures_dir / "local" / "Main.cs", csharp_project_dir / "Main.cs") # Backtest Python project locally run_command(["lean", "backtest", python_project_name], cwd=test_dir, expected_output="Total Trades 1") python_backtest_dirs = list((python_project_dir / "backtests").iterdir()) assert len(python_backtest_dirs) == 1 # Backtest C# project locally run_command(["lean", "backtest", csharp_project_name], cwd=test_dir, expected_output="Total Trades 1") csharp_backtest_dirs = list((csharp_project_dir / "backtests").iterdir()) assert len(csharp_backtest_dirs) == 1 # Remove custom Python library run_command(["lean", "library", "remove", python_project_name, "altair"], cwd=test_dir) assert f"altair==" not in (python_project_dir / "requirements.txt").read_text(encoding="utf-8") # Remove custom C# library run_command( ["lean", "library", "remove", csharp_project_name, "Microsoft.ML"], cwd=test_dir) assert 'Include="Microsoft.ML"' not in csproj_file.read_text( encoding="utf-8") # Custom Python library is removed, so Python backtest should now fail run_command(["lean", "backtest", python_project_name], cwd=test_dir, expected_return_code=1) # Custom C# library is removed, so C# backtest should now fail run_command(["lean", "backtest", csharp_project_name], cwd=test_dir, expected_return_code=1) # Generate reports python_results_file = next(f for f in python_backtest_dirs[0].iterdir() if f.name.endswith(".json") and not f.name.endswith("-order-events.json")) run_command([ "lean", "report", "--backtest-results", str(python_results_file), "--report-destination", "python.html" ], cwd=test_dir) csharp_results_file = next(f for f in csharp_backtest_dirs[0].iterdir() if f.name.endswith(".json") and not f.name.endswith("-order-events.json")) run_command([ "lean", "report", "--backtest-results", str(csharp_results_file), "--report-destination", "csharp.html" ], cwd=test_dir) assert (test_dir / "python.html").is_file() assert (test_dir / "csharp.html").is_file() # Copy over algorithms containing a SPY buy-and-hold strategy without custom libraries shutil.copy(fixtures_dir / "cloud" / "main.py", python_project_dir / "main.py") shutil.copy(fixtures_dir / "cloud" / "Main.cs", csharp_project_dir / "Main.cs") # Push projects to the cloud run_command(["lean", "cloud", "push", "--project", python_project_name], cwd=test_dir) run_command(["lean", "cloud", "push", "--project", csharp_project_name], cwd=test_dir) # Remove some files and see if we can successfully pull them from the cloud (python_project_dir / "main.py").unlink() (csharp_project_dir / "Main.cs").unlink() # Pull projects from the cloud run_command(["lean", "cloud", "pull", "--project", python_project_name], cwd=test_dir) run_command(["lean", "cloud", "pull", "--project", csharp_project_name], cwd=test_dir) # Ensure deleted files have been pulled (python_project_dir / "main.py").is_file() (csharp_project_dir / "Main.cs").is_file() # Run Python backtest in the cloud run_command(["lean", "cloud", "backtest", python_project_name], cwd=test_dir) # Run C# backtest in the cloud run_command(["lean", "cloud", "backtest", csharp_project_name], cwd=test_dir) # Get cloud project status run_command(["lean", "cloud", "status", python_project_name], cwd=test_dir) run_command(["lean", "cloud", "status", csharp_project_name], cwd=test_dir) # Log out run_command(["lean", "logout"]) assert not credentials_path.exists() # Delete the test directory that we used shutil.rmtree(test_dir, ignore_errors=True) # Delete the cloud projects that we used api_client = APIClient(Logger(), HTTPClient(Logger()), user_id, api_token) cloud_projects = api_client.projects.get_all() api_client.projects.delete( next(p.projectId for p in cloud_projects if p.name == python_project_name)) api_client.projects.delete( next(p.projectId for p in cloud_projects if p.name == csharp_project_name))
def test_cli() -> None: """Tests the CLI by actually calling it like a real user would do. Unlike "normal" tests, this file only contains a single test method which steps through all commands. This is done on purpose to make the test as close to what real users do as possible. """ user_id = USER_ID or os.getenv("QC_USER_ID", "") api_token = API_TOKEN or os.getenv("QC_API_TOKEN", "") if user_id == "" or api_token == "": pytest.skip("API credentials not specified") global_config_path = Path("~/.lean").expanduser() credentials_path = global_config_path / "credentials" # Create an empty directory to perform tests in test_dir = Path(tempfile.mkdtemp()) # We use project names suffixed by a timestamp to prevent conflicts when we synchronize with the cloud timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") python_project_name = f"Python Project {timestamp}" csharp_project_name = f"CSharp Project {timestamp}" # Unset all global configuration shutil.rmtree(global_config_path, ignore_errors=True) # Log in run_command(["lean", "login"], input=[user_id, api_token]) assert credentials_path.exists() assert json.loads(credentials_path.read_text(encoding="utf-8")) == { "user-id": user_id, "api-token": api_token } # Download sample data and LEAN configuration file run_command(["lean", "init"], cwd=test_dir, input=["python"]) assert (test_dir / "data").is_dir() assert (test_dir / "lean.json").is_file() # Generate random data # This is the first command that uses the LEAN Docker image, so we increase the timeout to have time to pull it generate_output = run_command([ "lean", "data", "generate", "--start", "20150101", "--symbol-count", "1", "--resolution", "Daily" ], cwd=test_dir, timeout=600) matches = re.findall( r"Begin data generation of 1 randomly generated Equity assets\.\.\.\nSymbol\[1]: ([A-Z]+)", generate_output) assert len(matches) == 1 assert (test_dir / "data" / "equity" / "usa" / "daily" / f"{matches[0].lower()}.zip").is_file() # Configure global settings run_command(["lean", "config", "set", "default-language", "csharp"]) run_command(["lean", "config", "get", "default-language"], expected_output="csharp") run_command(["lean", "config", "unset", "default-language"]) run_command(["lean", "config", "get", "default-language"], expected_return_code=1) run_command(["lean", "config", "set", "default-language", "python"]) run_command(["lean", "config", "get", "default-language"], expected_output="python") list_output = run_command(["lean", "config", "list"]) assert len(re.findall(r"default-language[ ]+[^ ] python", list_output)) == 1 # Create Python project run_command([ "lean", "create-project", "--language", "python", python_project_name ], cwd=test_dir) python_project_dir = test_dir / python_project_name assert (python_project_dir / "main.py").is_file() assert (python_project_dir / "research.ipynb").is_file() assert (python_project_dir / "config.json").is_file() assert (python_project_dir / ".vscode" / "launch.json").is_file() assert (python_project_dir / ".vscode" / "settings.json").is_file() assert (python_project_dir / ".idea" / f"{python_project_name}.iml").is_file() assert (python_project_dir / ".idea" / "misc.xml").is_file() assert (python_project_dir / ".idea" / "modules.xml").is_file() assert (python_project_dir / ".idea" / "workspace.xml").is_file() # Create C# project run_command([ "lean", "create-project", "--language", "csharp", csharp_project_name ], cwd=test_dir) csharp_project_dir = test_dir / csharp_project_name assert (csharp_project_dir / "Main.cs").is_file() assert (csharp_project_dir / "research.ipynb").is_file() assert (csharp_project_dir / "config.json").is_file() assert (csharp_project_dir / f"{csharp_project_name}.csproj").is_file() assert (csharp_project_dir / ".vscode" / "launch.json").is_file() # Copy over algorithms containing a SPY buy-and-hold strategy fixtures_dir = Path(__file__).parent / "fixtures" shutil.copy(fixtures_dir / "main.py", python_project_dir / "main.py") shutil.copy(fixtures_dir / "Main.cs", csharp_project_dir / "Main.cs") # Backtest Python project locally run_command(["lean", "backtest", python_project_name], cwd=test_dir, expected_output="Total Trades 1") python_backtest_dirs = list((python_project_dir / "backtests").iterdir()) assert len(python_backtest_dirs) == 1 # Backtest C# project locally run_command(["lean", "backtest", csharp_project_name], cwd=test_dir, expected_output="Total Trades 1") csharp_backtest_dirs = list((csharp_project_dir / "backtests").iterdir()) assert len(csharp_backtest_dirs) == 1 # Generate report run_command([ "lean", "report", "--backtest-data-source-file", f"{python_project_name}/backtests/{python_backtest_dirs[0].name}/main.json" ], cwd=test_dir) assert (test_dir / "report.html").is_file() # Push projects to the cloud run_command(["lean", "cloud", "push", "--project", python_project_name], cwd=test_dir) run_command(["lean", "cloud", "push", "--project", csharp_project_name], cwd=test_dir) # Remove some files and see if we can successfully pull them from the cloud (python_project_dir / "main.py").unlink() (csharp_project_dir / "Main.cs").unlink() # Pull projects from the cloud run_command(["lean", "cloud", "pull", "--project", python_project_name], cwd=test_dir) run_command(["lean", "cloud", "pull", "--project", csharp_project_name], cwd=test_dir) # Ensure deleted files have been pulled (python_project_dir / "main.py").is_file() (csharp_project_dir / "Main.cs").is_file() # Run Python backtest in the cloud run_command(["lean", "cloud", "backtest", python_project_name], cwd=test_dir) # Run C# backtest in the cloud run_command(["lean", "cloud", "backtest", csharp_project_name], cwd=test_dir) # Log out run_command(["lean", "logout"]) assert not credentials_path.exists() # Delete the test directory that we used shutil.rmtree(test_dir, ignore_errors=True) # Delete the cloud projects that we used api_client = APIClient(Logger(), user_id, api_token) cloud_projects = api_client.projects.get_all() api_client.projects.delete( next(p.projectId for p in cloud_projects if p.name == python_project_name)) api_client.projects.delete( next(p.projectId for p in cloud_projects if p.name == csharp_project_name))