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)
+