Skip to content

Commit

Permalink
Add TradeStation & Alpaca Brokerage Support (#469)
Browse files Browse the repository at this point in the history
* feat: TradeStation in Readme

* feat: DQH of TradeStation

* Implement auth0 api client

* feat: OAth implementation

* feat: rename configuration of TS in Readme

* feat: get auth0 configurations

* feat: increase number of modules json file

* refactor: README

* fix: use isinstance instead of type in condition
fix: Readme skip oauth type

* feat: auto open browser when call authorize

* Add Alpaca to readme

* Pin setuptools

---------

Co-authored-by: Ronit Jain <[email protected]>
Co-authored-by: Martin Molinero <[email protected]>
  • Loading branch information
3 people authored Jul 19, 2024
1 parent e377aa3 commit 13b5996
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 9 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

- name: Install dependencies
run: |
pip install wheel
pip install setuptools==69.5.1 wheel
pip install -r requirements.txt
- name: Run tests
Expand Down
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,9 +320,9 @@ Usage: lean cloud live deploy [OPTIONS] PROJECT
--notify-insights.
Options:
--brokerage [Paper Trading|Interactive Brokers|Tradier|Oanda|Bitfinex|Coinbase Advanced Trade|Binance|Zerodha|Samco|Terminal Link|Trading Technologies|Kraken|TDAmeritrade|Bybit]
--brokerage [Paper Trading|Interactive Brokers|Tradier|Oanda|Bitfinex|Coinbase Advanced Trade|Binance|Zerodha|Samco|Terminal Link|Trading Technologies|Kraken|TDAmeritrade|Bybit|TradeStation|Alpaca]
The brokerage to use
--data-provider-live [QuantConnect|Interactive Brokers|Tradier|Oanda|Bitfinex|Coinbase Advanced Trade|Binance|Zerodha|Samco|Terminal Link|Trading Technologies|Kraken|TDAmeritrade|Polygon|IEX|CoinApi|Bybit]
--data-provider-live [QuantConnect|Interactive Brokers|Tradier|Oanda|Bitfinex|Coinbase Advanced Trade|Binance|Zerodha|Samco|Terminal Link|Trading Technologies|Kraken|TDAmeritrade|Polygon|IEX|CoinApi|Bybit|TradeStation|Alpaca]
The live data provider to use
--ib-user-name TEXT Your Interactive Brokers username
--ib-account TEXT Your Interactive Brokers account id
Expand Down Expand Up @@ -406,13 +406,21 @@ Options:
--bybit-api-secret TEXT Your Bybit API secret
--bybit-vip-level [VIP0|VIP1|VIP2|VIP3|VIP4|VIP5|SupremeVIP|Pro1|Pro2|Pro3|Pro4|Pro5]
Your Bybit VIP Level
--trade-station-environment [live|paper]
Whether Live or Paper environment should be used
--trade-station-account-type [Cash|Margin|Futures|DVP]
Specifies the type of account on TradeStation
--alpaca-environment [live|paper]
Whether Live or Paper environment should be used
--polygon-api-key TEXT Your Polygon.io API Key
--iex-cloud-api-key TEXT Your iexcloud.io API token publishable key
--iex-price-plan [Launch|Grow|Enterprise]
Your IEX Cloud Price plan
--coinapi-api-key TEXT Your coinapi.io Api Key
--coinapi-product [Free|Startup|Streamer|Professional|Enterprise]
CoinApi pricing plan (https://www.coinapi.io/market-data-api/pricing)
--alpaca-api-key TEXT Your Alpaca Api Key
--alpaca-api-secret TEXT Your Alpaca Api Secret
--node TEXT The name or id of the live node to run on
--auto-restart BOOLEAN Whether automatic algorithm restarting must be enabled
--notify-order-events BOOLEAN Whether notifications must be sent for order events
Expand Down Expand Up @@ -1225,9 +1233,9 @@ Options:
--environment TEXT The environment to use
--output DIRECTORY Directory to store results in (defaults to PROJECT/live/TIMESTAMP)
-d, --detach Run the live deployment in a detached Docker container and return immediately
--brokerage [Paper Trading|Interactive Brokers|Tradier|Oanda|Bitfinex|Coinbase Advanced Trade|Binance|Zerodha|Samco|Terminal Link|Trading Technologies|Kraken|TDAmeritrade|Bybit]
--brokerage [Paper Trading|Interactive Brokers|Tradier|Oanda|Bitfinex|Coinbase Advanced Trade|Binance|Zerodha|Samco|Terminal Link|Trading Technologies|Kraken|TDAmeritrade|Bybit|TradeStation|Alpaca]
The brokerage to use
--data-provider-live [Interactive Brokers|Tradier|Oanda|Bitfinex|Coinbase Advanced Trade|Binance|Zerodha|Samco|Terminal Link|Trading Technologies|Kraken|TDAmeritrade|IQFeed|Polygon|IEX|CoinApi|ThetaData|Custom data only|Bybit]
--data-provider-live [Interactive Brokers|Tradier|Oanda|Bitfinex|Coinbase Advanced Trade|Binance|Zerodha|Samco|Terminal Link|Trading Technologies|Kraken|TDAmeritrade|IQFeed|Polygon|IEX|CoinApi|ThetaData|Custom data only|Bybit|TradeStation|Alpaca]
The live data provider to use
--data-provider-historical [Interactive Brokers|Oanda|Bitfinex|Coinbase Advanced Trade|Binance|Kraken|IQFeed|Polygon|FactSet|IEX|AlphaVantage|CoinApi|ThetaData|QuantConnect|Local|Bybit]
Update the Lean configuration file to retrieve data from the given historical provider
Expand Down Expand Up @@ -1328,6 +1336,12 @@ Options:
Your Bybit VIP Level
--bybit-use-testnet [live|paper]
Whether the testnet should be used
--trade-station-environment [live|paper]
Whether Live or Paper environment should be used
--trade-station-account-type [Cash|Margin|Futures|DVP]
Specifies the type of account on TradeStation
--alpaca-environment [live|paper]
Whether Live or Paper environment should be used
--ib-enable-delayed-streaming-data BOOLEAN
Whether delayed data may be used when your algorithm subscribes to a security you
don't have a market data subscription for (Optional).
Expand All @@ -1347,6 +1361,8 @@ Options:
--thetadata-rest-url TEXT The ThetaData host address (Optional).
--thetadata-subscription-plan [Free|Value|Standard|Pro]
Your ThetaData subscription price plan
--alpaca-api-key TEXT Your Alpaca Api Key
--alpaca-api-secret TEXT Your Alpaca Api Secret
--factset-auth-config-file FILE
The path to the FactSet authentication configuration file
--alpha-vantage-api-key TEXT Your Alpha Vantage Api Key
Expand Down
2 changes: 2 additions & 0 deletions lean/components/api/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from typing import Any, Dict

from lean.components.api.auth0_client import Auth0Client
from lean.components.api.account_client import AccountClient
from lean.components.api.backtest_client import BacktestClient
from lean.components.api.compile_client import CompileClient
Expand Down Expand Up @@ -52,6 +53,7 @@ def __init__(self, logger: Logger, http_client: HTTPClient, user_id: str, api_to
self.set_user_token(user_id, api_token)

# Create the clients containing the methods to send requests to the various API endpoints
self.auth0 = Auth0Client(self)
self.accounts = AccountClient(self)
self.backtests = BacktestClient(self)
self.compiles = CompileClient(self)
Expand Down
61 changes: 61 additions & 0 deletions lean/components/api/auth0_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean CLI v1.0. Copyright 2021 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 lean.components.api.api_client import *
from lean.components.util.logger import Logger
from lean.constants import API_BASE_URL
from lean.models.api import QCAuth0Authorization
from lean.models.errors import RequestFailedError


class Auth0Client:
"""The Auth0Client class contains methods to interact with live/auth0/* API endpoints."""

def __init__(self, api_client: 'APIClient') -> None:
"""Creates a new Auth0Client instance.
:param api_client: the APIClient instance to use when making requests
"""
self._api = api_client

def read(self, brokerage_id: str) -> QCAuth0Authorization:
"""Reads the authorization data for a brokerage.
:param brokerage_id: the id of the brokerage to read the authorization data for
:return: the authorization data for the specified brokerage
"""
try:
payload = {
"brokerage": brokerage_id
}

data = self._api.post("live/auth0/read", payload)
return QCAuth0Authorization(**data)
except RequestFailedError as e:
return QCAuth0Authorization(authorization=None)

@staticmethod
def authorize(brokerage_id: str, logger: Logger) -> None:
"""Starts the authorization process for a brokerage.
:param brokerage_id: the id of the brokerage to start the authorization process for
:param logger: the logger instance to use
"""
from webbrowser import open

full_url = f"{API_BASE_URL}live/auth0/authorize?brokerage={brokerage_id}"
logger.info(f"Please open the following URL in your browser to authorize the LEAN CLI.")
logger.info(full_url)
open(full_url)


45 changes: 45 additions & 0 deletions lean/components/util/auth0_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals.
# Lean CLI v1.0. Copyright 2021 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 lean.models.api import QCAuth0Authorization
from lean.components.api.api_client import Auth0Client
from lean.components.util.logger import Logger


def get_authorization(auth0_client: Auth0Client, brokerage_id: str, logger: Logger) -> QCAuth0Authorization:
"""Gets the authorization data for a brokerage, authorizing if necessary.
:param auth0_client: An instance of Auth0Client, containing methods to interact with live/auth0/* API endpoints.
:param brokerage_id: The ID of the brokerage to get the authorization data for.
:param logger: An instance of Logger, handling all output printing.
:return: The authorization data for the specified brokerage.
"""
from time import time, sleep

data = auth0_client.read(brokerage_id)
if data.authorization is not None:
return data

start_time = time()
auth0_client.authorize(brokerage_id, logger)

# keep checking for new data every 5 seconds for 7 minutes
while time() - start_time < 420:
logger.info("Will sleep 5 seconds and retry fetching authorization...")
sleep(5)
data = auth0_client.read(brokerage_id)
if data.authorization is None:
continue
return data

raise Exception("Authorization failed")
2 changes: 1 addition & 1 deletion lean/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from time import time

json_modules = {}
file_name = "modules-1.13.json"
file_name = "modules-1.14.json"
directory = Path(__file__).parent
file_path = directory.parent / file_name

Expand Down
2 changes: 2 additions & 0 deletions lean/models/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
# The models in this module are all parts of responses from the QuantConnect API
# The keys of properties are not changed, so they don't obey the rest of the project's naming conventions

class QCAuth0Authorization(WrappedBaseModel):
authorization: Optional[Dict[str, str]]

class ProjectEncryptionKey(WrappedBaseModel):
id: str
Expand Down
31 changes: 31 additions & 0 deletions lean/models/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ def factory(config_json_object) -> 'Configuration':
return UserInputConfiguration.factory(config_json_object)
elif config_json_object["type"] == "filter-env":
return BrokerageEnvConfiguration.factory(config_json_object)
elif config_json_object["type"] == "oauth-token":
return AuthConfiguration.factory(config_json_object)
else:
raise ValueError(
f'Undefined input method type {config_json_object["type"]}')
Expand Down Expand Up @@ -388,6 +390,35 @@ def ask_user_for_input(self, default_value, logger: Logger, hide_input: bool = F
raise ValueError(f"Undefined input method type {self._input_method}")


class AuthConfiguration(InternalInputUserInput):

def __init__(self, config_json_object):
super().__init__(config_json_object)

def factory(config_json_object) -> 'AuthConfiguration':
"""Creates an instance of the child classes.
:param config_json_object: the json object dict with configuration info
:return: An instance of AuthConfiguration.
"""
if config_json_object["type"] == "oauth-token":
return AuthConfiguration(config_json_object)
else:
raise ValueError(
f'Undefined input method type {config_json_object["type"]}')

def ask_user_for_input(self, default_value, logger: Logger, hide_input: bool = False):
"""Prompts user to provide input while validating the type of input
against the expected type
:param default_value: The default to prompt to the user.
:param logger: The instance of logger class.
:param hide_input: Whether to hide the input (not used for this type of input, which is never hidden).
:return: The value provided by the user.
"""
raise ValueError(f'user input not allowed with {self.__class__.__name__}')


class FilterEnvConfiguration(BrokerageEnvConfiguration):
"""This class adds extra filters to user filters."""

Expand Down
16 changes: 13 additions & 3 deletions lean/models/json_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
from click import get_current_context
from click.core import ParameterSource

from lean.components.util.auth0_helper import get_authorization
from lean.components.util.logger import Logger
from lean.constants import MODULE_TYPE, MODULE_PLATFORM, MODULE_CLI_PLATFORM
from lean.container import container
from lean.models.configuration import BrokerageEnvConfiguration, Configuration, InternalInputUserInput, \
PathParameterUserInput
PathParameterUserInput, AuthConfiguration
from copy import copy
from abc import ABC

Expand Down Expand Up @@ -132,13 +133,17 @@ def get_settings(self) -> Dict[str, str]:
for configuration in self._lean_configs:
if not self._check_if_config_passes_filters(configuration, all_for_platform_type=False):
continue
settings[configuration._id] = str(configuration._value).replace("\\", "/")
if isinstance(configuration, AuthConfiguration) and isinstance(configuration._value, dict):
for key, value in configuration._value.items():
settings[key] = str(value)
else:
settings[configuration._id] = str(configuration._value).replace("\\", "/")

return settings

def get_all_input_configs(self, filters: List[Type[Configuration]] = []) -> List[Configuration]:
return [copy(config) for config in self._lean_configs if config._is_required_from_user
if type(config) not in filters
if not isinstance(config, tuple(filters))
and self._check_if_config_passes_filters(config, all_for_platform_type=True)]

def convert_lean_key_to_variable(self, lean_key: str) -> str:
Expand Down Expand Up @@ -195,6 +200,11 @@ def config_build(self,
_logged_messages.add(log_message)
if type(configuration) is InternalInputUserInput:
continue
elif isinstance(configuration, AuthConfiguration):
auth_authorizations = get_authorization(container.api_client.auth0, self._display_name.lower(), logger)
logger.debug(f'auth: {auth_authorizations}')
configuration._value = auth_authorizations.authorization
continue

property_name = self.convert_lean_key_to_variable(configuration._id)
# Only ask for user input if the config wasn't given as an option
Expand Down

0 comments on commit 13b5996

Please sign in to comment.