From 5fa1389af77dc2520e00188ea8544f4216f37e6a Mon Sep 17 00:00:00 2001 From: davidlatte Date: Tue, 28 Nov 2023 14:04:00 -0800 Subject: [PATCH] tradier: new broker and data_source skeleton code added along with initial unit tests. --- lumibot/brokers/__init__.py | 1 + lumibot/brokers/tradier.py | 69 ++++++++++++++++++++++++++++ lumibot/data_sources/__init__.py | 7 +++ lumibot/data_sources/tradier_data.py | 35 ++++++++++++++ tests/test_tradier.py | 15 ++++++ 5 files changed, 127 insertions(+) create mode 100644 lumibot/brokers/tradier.py create mode 100644 lumibot/data_sources/tradier_data.py create mode 100644 tests/test_tradier.py diff --git a/lumibot/brokers/__init__.py b/lumibot/brokers/__init__.py index e5417f1b1..adc37656f 100644 --- a/lumibot/brokers/__init__.py +++ b/lumibot/brokers/__init__.py @@ -2,3 +2,4 @@ from .broker import Broker from .ccxt import Ccxt from .interactive_brokers import InteractiveBrokers +from .tradier import Tradier diff --git a/lumibot/brokers/tradier.py b/lumibot/brokers/tradier.py new file mode 100644 index 000000000..3ef10ad53 --- /dev/null +++ b/lumibot/brokers/tradier.py @@ -0,0 +1,69 @@ +from lumibot.brokers import Broker +from lumibot.data_sources import TRADIER_LIVE_API_URL, TRADIER_PAPER_API_URL, TradierAPIError, TradierData +from lumibot.entities import Asset, Order + + +class Tradier(Broker): + """ + Broker that connects to Tradier API to place orders and retrieve data + """ + def __init__(self, account_id=None, api_token=None, paper=True, config=None, max_workers=20, connect_stream=True, + data_source=None): + + if data_source is None: + data_source = TradierData(account_id=account_id, api_key=api_token, paper=paper, max_workers=max_workers) + + super().__init__(name='Tradier', data_source=data_source, config=config, max_workers=max_workers, + connect_stream=connect_stream) + + self._tradier_api_key = api_token + self._tradier_account_id = account_id + self._tradier_paper = paper + self._tradier_base_url = TRADIER_PAPER_API_URL if self._tradier_paper else TRADIER_LIVE_API_URL + + try: + self.validate_credentials() + except TradierAPIError as e: + raise TradierAPIError("Invalid Tradier Credentials") from e + + def validate_credentials(self): + pass + + def cancel_order(self, order: Order): + pass + + def _submit_order(self, order: Order): + pass + + def _get_balances_at_broker(self, quote_asset: Asset) -> float: + pass + + def get_historical_account_value(self): + pass + + def _get_stream_object(self): + pass + + def _register_stream_events(self): + pass + + def _run_stream(self): + pass + + def _parse_broker_position(self, broker_position, strategy, orders=None): + pass + + def _pull_broker_position(self, asset: Asset): + pass + + def _pull_broker_positions(self, strategy=None): + pass + + def _parse_broker_order(self, response, strategy_name, strategy_object=None): + pass + + def _pull_broker_order(self, identifier): + pass + + def _pull_broker_open_orders(self): + pass diff --git a/lumibot/data_sources/__init__.py b/lumibot/data_sources/__init__.py index 5ca35ac4d..350c7b27f 100644 --- a/lumibot/data_sources/__init__.py +++ b/lumibot/data_sources/__init__.py @@ -6,5 +6,12 @@ from .exceptions import NoDataFound, UnavailabeTimestep from .interactive_brokers_data import InteractiveBrokersData from .pandas_data import PandasData +from .tradier_data import ( + TRADIER_LIVE_API_URL, + TRADIER_PAPER_API_URL, + TRADIER_STREAM_API_URL, + TradierAPIError, + TradierData, +) from .tradovate_data import TradovateData from .yahoo_data import YahooData diff --git a/lumibot/data_sources/tradier_data.py b/lumibot/data_sources/tradier_data.py new file mode 100644 index 000000000..9abe9b01c --- /dev/null +++ b/lumibot/data_sources/tradier_data.py @@ -0,0 +1,35 @@ +from .data_source import DataSource + +TRADIER_LIVE_API_URL = "https://api.tradier.com/v1/" +TRADIER_PAPER_API_URL = "https://sandbox.tradier.com/v1/" +TRADIER_STREAM_API_URL = "https://stream.tradier.com/v1/" # Only valid Live, no Paper support + + +class TradierAPIError(Exception): + pass + + +class TradierData(DataSource): + MIN_TIMESTEP = "minute" + SOURCE = "Tradier" + + def __init__(self, account_id, api_key, paper=True, max_workers=20): + super().__init__(api_key=api_key) + self._account_id = account_id + self._paper = paper + self._base_url = TRADIER_PAPER_API_URL if self._paper else TRADIER_LIVE_API_URL + self.max_workers = min(max_workers, 50) + + def _pull_source_symbol_bars(self, asset, length, timestep=MIN_TIMESTEP, timeshift=None, quote=None, exchange=None, + include_after_hours=True): + pass + + def _pull_source_bars(self, assets, length, timestep=MIN_TIMESTEP, timeshift=None, quote=None, + include_after_hours=True): + pass + + def _parse_source_symbol_bars(self, response, asset, quote=None, length=None): + pass + + def get_last_price(self, asset, quote=None, exchange=None): + pass diff --git a/tests/test_tradier.py b/tests/test_tradier.py new file mode 100644 index 000000000..fb5654e73 --- /dev/null +++ b/tests/test_tradier.py @@ -0,0 +1,15 @@ +from lumibot.brokers import Tradier +from lumibot.data_sources import TradierData + + +class TestTradierData: + def test_basics(self): + tdata = TradierData(account_id="1234", api_key="a1b2c3", paper=True) + assert tdata._account_id == "1234" + + +class TestTradierBroker: + def test_basics(self): + broker = Tradier(account_id="1234", api_token="a1b2c3", paper=True) + assert broker.name == "Tradier" + assert broker._tradier_account_id == "1234"