예제 #1
0
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"
예제 #2
0
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"
예제 #3
0
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()
예제 #4
0
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"
예제 #5
0
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"
예제 #6
0
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")
예제 #7
0
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)
예제 #8
0
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"]
예제 #9
0
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")
예제 #10
0
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")
예제 #11
0
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 ")
예제 #12
0
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"
예제 #13
0
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)
예제 #14
0
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)
예제 #15
0
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 ")
예제 #16
0
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))
예제 #17
0
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))