diff --git a/Blankly/auth/Alpaca/__init__.py b/Blankly/auth/Alpaca/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Blankly/auth/Alpaca/auth.py b/Blankly/auth/Alpaca/auth.py new file mode 100644 index 00000000..5b868e7e --- /dev/null +++ b/Blankly/auth/Alpaca/auth.py @@ -0,0 +1,26 @@ +from Blankly.auth.abc_auth import auth_interface + +import warnings +class alpaca_auth(auth_interface): + def __init__(self, keys_file, portfolio_name): + super().__init__(keys_file, portfolio_name, 'alpaca') + self.API_KEY = None + self.API_SECRET = None + self.validate_credentials() + + def validate_credentials(self): + """ + Validate that exchange specific credentials are present + """ + try: + self.API_KEY = self.raw_cred.pop('API_KEY') + self.API_SECRET = self.raw_cred.pop('API_SECRET') + except KeyError as e: + print(f"One of 'API_KEY' or 'API_SECRET' not defined for Exchange: {self.__exchange} Portfolio: {self.__portfolio_name}") + raise KeyError(e) + + if len(self.raw_cred) > 0: + warnings.warn("Additional configs for Exchange: {self.__exchange} Portfolio: {self.__portfolio_name} being ignored") + + + diff --git a/Blankly/auth/Binance/__init__.py b/Blankly/auth/Binance/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Blankly/auth/Binance/auth.py b/Blankly/auth/Binance/auth.py new file mode 100644 index 00000000..a24ec4ba --- /dev/null +++ b/Blankly/auth/Binance/auth.py @@ -0,0 +1,26 @@ +from Blankly.auth.abc_auth import auth_interface +import warnings + +class binance_auth(auth_interface): + def __init__(self, keys_file, portfolio_name): + super.__init__(keys_file, portfolio_name, 'binance') + self.API_KEY = None + self.API_SECRET = None + self.validate_credentials() + + def validate_credentials(self): + """ + Validate that exchange specific credentials are present + """ + try: + self.API_KEY = self.raw_cred.pop('API_KEY') + self.API_SECRET = self.raw_cred.pop('API_SECRET') + except KeyError as e: + print(f"One of 'API_KEY' or 'API_SECRET' not defined for Exchange: {self.__exchange} Portfolio: {self.__portfolio_name}") + raise KeyError(e) + + if len(self.raw_cred) > 0: + warnings.warn(f"Additional configs for Exchange: {self.__exchange} Portfolio: {self.__portfolio_name} being ignored") + + + diff --git a/Blankly/auth/Coinbase/__init__.py b/Blankly/auth/Coinbase/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Blankly/auth/Coinbase/auth.py b/Blankly/auth/Coinbase/auth.py new file mode 100644 index 00000000..c71a5414 --- /dev/null +++ b/Blankly/auth/Coinbase/auth.py @@ -0,0 +1,29 @@ +from Blankly.auth.abc_auth import auth_interface +import warnings + +class coinbase_auth(auth_interface): + def __init__(self, keys_file, portfolio_name): + super.__init__(keys_file, portfolio_name, 'coinbase') + self.API_KEY = None + self.API_SECRET = None + self.API_PASS = None + + self.validate_credentials() + + def validate_credentials(self): + """ + Validate that exchange specific credentials are present + """ + try: + self.API_KEY = self.raw_cred.pop('API_KEY') + self.API_SECRET = self.raw_cred.pop('API_SECRET') + self.API_PASS = self.raw_cred.pop('API_PASS') + except KeyError as e: + print(f"One of 'API_KEY' or 'API_SECRET' or 'API_PASS' not defined for Exchange: {self.__exchange} Portfolio: {self.__portfolio_name}") + raise KeyError(e) + + if len(self.raw_cred) > 0: + warnings.warn(f"Additional configs for Exchange: {self.__exchange} Portfolio: {self.__portfolio_name} being ignored") + + + diff --git a/Blankly/auth/__init__.py b/Blankly/auth/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Blankly/auth/abc_auth.py b/Blankly/auth/abc_auth.py new file mode 100644 index 00000000..b48357f3 --- /dev/null +++ b/Blankly/auth/abc_auth.py @@ -0,0 +1,52 @@ +""" + Logic to provide consistency across exchanges + Copyright (C) 2021 Emerson Dove + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . +""" + +import abc +from Blankly.auth.utils import load_json + +class auth_interface(abc.ABC): + def __init__(self, keys_file, portfolio_name, exchange): + """ + Create a currency interface + Args: + keys_file (str): filepath to keys.json + portfolio_name (str): name of portfolio + """ + assert keys_file + assert portfolio_name + assert exchange + self.portfolio_name = portfolio_name + self.exchange = exchange + self.raw_cred = self.load_credentials(keys_file, portfolio_name, exchange) + + def load_credentials(self, keys_file, portfolio_name, exchange): + """ + Load credentials from keys json file + """ + auth_object = load_json(keys_file) + exchange_keys = auth_object[exchange] + credentials = exchange_keys[portfolio_name] + + return credentials + + @abc.abstractmethod + def validate_credentials(self): + """ + Validate that exchange specific credentials are present + """ + pass diff --git a/Blankly/auth/auth.py b/Blankly/auth/auth.py new file mode 100644 index 00000000..e69de29b diff --git a/Blankly/auth/auth_factory.py b/Blankly/auth/auth_factory.py new file mode 100644 index 00000000..ab08008e --- /dev/null +++ b/Blankly/auth/auth_factory.py @@ -0,0 +1,16 @@ +from Blankly.auth.Alpaca.auth import alpaca_auth +from Blankly.auth.Binance.auth import binance_auth +from Blankly.auth.Coinbase.auth import coinbase_auth + + +class AuthFactory: + @staticmethod + def create_auth(self, keys_file, exchange_name, portfolio_name): + if exchange_name == 'alpaca': + return alpaca_auth(keys_file, portfolio_name) + elif exchange_name == 'binance': + return binance_auth(keys_file, portfolio_name) + elif exchange_name == 'coinbase_pro': + return coinbase_auth(keys_file, portfolio_name) + else: + raise KeyError("Exchange not supported") diff --git a/Blankly/auth/utils.py b/Blankly/auth/utils.py new file mode 100644 index 00000000..02bce780 --- /dev/null +++ b/Blankly/auth/utils.py @@ -0,0 +1,22 @@ +import json +import warnings + +def load_json(keys_file): + try: + f = open(keys_file) + return json.load(f) + except FileNotFoundError: + raise FileNotFoundError("Make sure a Keys.json file is placed in the same folder as the project working " + "directory!") + +def default_first_portfolio(keys_file, exchange_name): + auth_object = load_json(keys_file) + exchange_keys = auth_object[exchange_name] + first_key = list(exchange_keys.keys())[0] + warning_string = "No portfolio name to load specified, defaulting to the first in the file: " \ + "(" + first_key + "). This is fine if there is only one portfolio in use." + warnings.warn(warning_string) + # Read the first in the portfolio + portfolio = exchange_keys[first_key] + name = first_key + return name, portfolio \ No newline at end of file diff --git a/Blankly/exchanges/Alpaca/Alpaca.py b/Blankly/exchanges/Alpaca/Alpaca.py index e69de29b..dc349111 100644 --- a/Blankly/exchanges/Alpaca/Alpaca.py +++ b/Blankly/exchanges/Alpaca/Alpaca.py @@ -0,0 +1,32 @@ +""" + Coinbase Pro exchange definitions and setup + Copyright (C) 2021 Emerson Dove + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . +""" + +from Blankly.exchanges.exchange import Exchange +from Blankly.auth.auth_factory import AuthFactory +from Blankly.auth.utils import default_first_portfolio +from Blankly.interface.currency_factory import InterfaceFactory + +class Alpaca(Exchange): + def __init__(self, portfolio_name=None, auth_path="Keys.json", preferences_path=None): + if not portfolio_name: + portfolio_name = default_first_portfolio(auth_path, 'alpaca') + Exchange.__init__(self, 'alpaca', portfolio_name, preferences_path) + alpaca_auth = AuthFactory(auth_path, 'alpaca', portfolio_name) + self.interface = InterfaceFactory('alpaca', alpaca_auth, self.preferences) + + diff --git a/Blankly/exchanges/Alpaca/Alpaca_API.py b/Blankly/exchanges/Alpaca/Alpaca_API.py index 2b20d1e1..a798d044 100644 --- a/Blankly/exchanges/Alpaca/Alpaca_API.py +++ b/Blankly/exchanges/Alpaca/Alpaca_API.py @@ -1,3 +1,5 @@ +from Blankly.auth.Alpaca.auth import alpaca_auth + import alpaca_trade_api as tradeapi import os @@ -6,13 +8,13 @@ class API: - def __init__(self, API_KEY, API_SECRET, paper_trading = False): - if (paper_trading): + def __init__(self, auth: alpaca_auth, paper_trading=True): + if paper_trading: self.__api_url = APCA_API_PAPER_URL else: self.__api_url = APCA_API_LIVE_URL - self.alp_client = tradeapi.REST(API_KEY, API_SECRET, self.__api_url, 'v2') + self.alp_client = tradeapi.REST(auth.API_KEY, auth.API_SECRET, self.__api_url, 'v2') api = os.getenv("ALPACA_PUBLIC") @@ -20,4 +22,4 @@ def __init__(self, API_KEY, API_SECRET, paper_trading = False): if __name__ == "__main__": client = API(api, secret, True) - print(client.alp_client.list_assets()) \ No newline at end of file + print(client.alp_client.list_assets()) diff --git a/Blankly/exchanges/Alpaca/alpaca_api_interface.py b/Blankly/exchanges/Alpaca/alpaca_api_interface.py new file mode 100644 index 00000000..5263c033 --- /dev/null +++ b/Blankly/exchanges/Alpaca/alpaca_api_interface.py @@ -0,0 +1,159 @@ +import warnings + +from Blankly.utils import utils as utils +from Blankly.exchanges.Alpaca.Alpaca_API import API +from Blankly.interface.currency_Interface import CurrencyInterface +import alpaca_trade_api as tradeapi + +from Blankly.utils.purchases.limit_order import LimitOrder +from Blankly.utils.purchases.market_order import MarketOrder + +class AlpacaInterface(CurrencyInterface): + def __init__(self, authenticated_API: API): + super().__init__('alpaca', authenticated_API) + assert isinstance(self.calls, tradeapi.REST) + + def init_exchange(self): + assert isinstance(self.calls, tradeapi.REST) + account_info = self.calls.get_account()._raw + try: + if account_info['account_blocked']: + warnings.warn('Your alpaca account is indicated as blocked for trading....') + except KeyError: + raise LookupError("Alpaca API call failed") + + self.__exchange_properties = { + "maker_fee_rate": 0, + "taker_fee_rate": 0 + } + + def get_products(self): + ''' + [ + { + "id": "904837e3-3b76-47ec-b432-046db621571b", + "class": "us_equity", + "exchange": "NASDAQ", + "symbol": "AAPL", + "status": "active", + "tradable": true, + "marginable": true, + "shortable": true, + "easy_to_borrow": true, + "fractionable": true + }, + ... + ] + ''' + needed = self.needed['get_products'] + assets = self.calls.list_assets(status=None, asset_class=None)._raw + + for asset in assets: + asset['currency_id'] = asset.pop('id') + asset['base_currency'] = asset.pop('symbol') + asset['quote_currency'] = 'usd' + asset['base_min_size'] = -1 + asset['base_max_size'] = -1 + asset['base_increment'] = -1 + + for i in range(len(assets)): + assets[i] = utils.isolate_specific(needed, assets[i]) + + return assets + + def get_account(self, currency=None, override_paper_trading=False): + assert isinstance(self.calls, tradeapi.REST) + needed = self.needed['get_account'] + + account_dict = self.calls.get_account()._raw + account_dict['currency'] = account_dict.pop('cash') + account_dict['hold'] = -1 + + positions = self.calls.list_positions() + for position in positions: + position['currency'] = position.pop('symbol') + position['available'] = position.pop('qty') + position['hold'] = -1 + + positions.append(account_dict) + + for i in range(len(positions)): + positions[i] = utils.isolate_specific(needed, positions[i]) + + return positions + + def market_order(self, product_id, side, funds) -> MarketOrder: + assert isinstance(self.calls, tradeapi.REST) + needed = self.needed['market_order'] + + order = { + 'funds': funds, + 'side': side, + 'product_id': product_id, + 'type': 'market' + } + response = self.calls.submit_order(product_id, side=side, type='market', time_int_force='day', notional=funds)._raw + response = utils.isolate_specific(needed, response) + return MarketOrder(order, response, self) + + def limit_order(self, product_id, side, price, size) -> LimitOrder: + pass + + def cancel_order(self, currency_id, order_id) -> dict: + assert isinstance(self.calls, tradeapi.REST) + self.calls.cancel_order(order_id) + + #TODO: handle the different response codes + return {'order_id': order_id} + + # TODO: this doesnt exactly fit + def get_open_orders(self, product_id=None): + assert isinstance(self.calls, tradeapi.REST) + needed = self.needed['get_open_orders'] + orders = self.calls.list_orders()._raw + renames = [ + ["asset_id", "product_id"], + ["filled_at", "price"], + ["qty", "size"], + ["notional", "funds"] + ] + for order in orders: + order = utils.rename_to(renames, order) + order = utils.isolate_specific(needed, order) + return orders + + def get_order(self, currency_id, order_id) -> dict: + assert isinstance(self.calls, tradeapi.REST) + needed = self.needed['get_order'] + order = self.calls.get_order(order_id)._raw + renames = [ + ["asset_id", "product_id"], + ["filled_at", "price"], + ["qty", "size"], + ["notional", "funds"] + ] + order = utils.rename_to(renames, order) + order = utils.isolate_specific(needed, order) + return order + + def get_fees(self): + assert isinstance(self.calls, tradeapi.REST) + return { + 'maker_fee_rate': 0, + 'taker_fee_rate': 0 + } + + def get_product_history(self, product_id, epoch_start, epoch_stop, granularity): + assert isinstance(self.calls, tradeapi.REST) + + pass + + # TODO: tbh not sure how this one works + def get_market_limits(self, product_id): + assert isinstance(self.calls, tradeapi.REST) + pass + + def get_price(self, currency_pair) -> float: + assert isinstance(self.calls, tradeapi.REST) + response = self.calls.get_last_trade() + return float(response['p']) \ No newline at end of file diff --git a/Blankly/interface/currency_Interface.py b/Blankly/interface/currency_Interface.py index f017558c..6e0b1e3c 100644 --- a/Blankly/interface/currency_Interface.py +++ b/Blankly/interface/currency_Interface.py @@ -21,7 +21,7 @@ from Blankly.interface.abc_currency_interface import ICurrencyInterface import abc - +# TODO: need to add a cancel all orders function class CurrencyInterface(ICurrencyInterface, abc.ABC): def __init__(self, exchange_name, authenticated_API): self.exchange_name = exchange_name diff --git a/Blankly/interface/currency_factory.py b/Blankly/interface/currency_factory.py index dc0d3dc6..59936efe 100644 --- a/Blankly/interface/currency_factory.py +++ b/Blankly/interface/currency_factory.py @@ -1,31 +1,36 @@ +from Blankly.exchanges.Alpaca.alpaca_api_interface import AlpacaInterface from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_API import API as Coinbase_Pro_API from binance.client import Client - +from Blankly.exchanges.Alpaca.Alpaca_API import API as Alpaca_API from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_Interface import CoinbaseProInterface from Blankly.exchanges.Binance.Binance_Interface import BinanceInterface - +from Blankly.auth.auth_factory import AuthFactory class InterfaceFactory: - def create_interface(self, name, preferences, auth): - if name == 'coinbase_pro': + + @staticmethod + def create_interface(exchange_name: str, preferences, auth): + if exchange_name == 'coinbase_pro': if preferences["settings"]["use_sandbox"]: calls = Coinbase_Pro_API(auth[0], auth[1], auth[2], - API_URL="https://api-public.sandbox.pro.coinbase.com/") + API_URL="https://api-public.sandbox.pro.coinbase.com/") else: # Create the authenticated object calls = Coinbase_Pro_API(auth[0], auth[1], auth[2]) - return CoinbaseProInterface(name, calls) + return CoinbaseProInterface(exchange_name, calls) - elif name == 'binance': + elif exchange_name == 'binance': if preferences["settings"]["use_sandbox"] or preferences["settings"]["paper_trade"]: calls = Client(api_key=auth[0], api_secret=auth[1], - tld=preferences["settings"]["binance_tld"], - testnet=True) + tld=preferences["settings"]["binance_tld"], + testnet=True) else: calls = Client(api_key=auth[0], api_secret=auth[1], - tld=preferences["settings"]["binance_tld"]) - return BinanceInterface(name, calls) + tld=preferences["settings"]["binance_tld"]) + return BinanceInterface(exchange_name, calls) - elif name == 'alpaca': - pass + elif exchange_name == 'alpaca': + # TODO: Fix the hardcoded true + calls = Alpaca_API(auth, True) + return AlpacaInterface(calls) \ No newline at end of file diff --git a/Examples/Keys_Example.json b/Examples/Keys_Example.json index a7bee6bd..676bf119 100644 --- a/Examples/Keys_Example.json +++ b/Examples/Keys_Example.json @@ -11,5 +11,11 @@ "API_KEY": "**************************************************************", "API_SECRET": "*************************************************************" } + }, + "alpaca": { + "another cool portfolio": { + "API_KEY": "**************************************************************", + "API_SECRET": "*************************************************************" + } } } \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..db75e7a7 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ +# pytest.ini +[pytest] +testpaths = + tests \ No newline at end of file diff --git a/setup.py b/setup.py index b12b097f..1269dec3 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,9 @@ 'websocket-client', 'pandas', 'python-binance', - 'tulipy' + 'tulipy', + 'alpaca-trade-api', + 'pytest' ], classifiers=[ # Possible: "3 - Alpha", "4 - Beta" or "5 - Production/Stable" diff --git a/tests/exchanges/alpaca/alpaca_api_interface_test.py b/tests/exchanges/alpaca/alpaca_api_interface_test.py new file mode 100644 index 00000000..e55ac1d9 --- /dev/null +++ b/tests/exchanges/alpaca/alpaca_api_interface_test.py @@ -0,0 +1,7 @@ +from Blankly.auth.Alpaca.auth import alpaca_auth +from Blankly.interface.currency_factory import InterfaceFactory + +def test_alpaca_interface(): + auth_obj = alpaca_auth("/Users/arunannamalai/Documents/Blankly/Examples/keys.json", "paper account") + alpaca_interface = InterfaceFactory.create_interface("alpaca", None, auth_obj) +