Repository: blankly-finance/blankly (language: python)

Issue:
Backtest metrics
Adding backtest metrics for better backtesting analysis.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 600d2d33..5829a62b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,1 +1,141 @@
-.vscode
\ No newline at end of file
+.vscode
+
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cover/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+#   For a library or package, you might want to ignore these files since the code is
+#   intended to run in multiple environments; otherwise, check them in:
+# .python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+keys.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
\ No newline at end of file
diff --git a/Blankly/ProfitManager.py b/Blankly/ProfitManager.py
deleted file mode 100644
index fd6db916..00000000
--- a/Blankly/ProfitManager.py
+++ /dev/null
@@ -1,89 +0,0 @@
-"""
-    Deprecated Risk management or purchase management for recent buys.
-    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 <https://www.gnu.org/licenses/>.
-"""
-
-import Constants
-
-"""
--------Deprecated-------
-"""
-
-
-class ProfitManager:
-    def __init__(self, currencyType, ticker, exchanges=None, show=False):
-        if exchanges is None:
-            exchanges = []
-        self.__currencyType = currencyType
-        self.__exchanges = exchanges
-        self.__ticker = ticker
-        ticker.append_callback(self)
-        self.__show = show
-
-    def addExchange(self, exchange):
-        self.__exchanges.append(exchange)
-
-    def getExchange(self, index):
-        return self.__exchanges[index]
-
-    """ This will fire when the attached ticker receives a new price update """
-
-    def priceEvent(self, tick):
-        price = float(tick["price"])
-        for i in range(len(self.__exchanges)):
-            if self.__exchanges[i].getIfSold() is True:
-                # If this one is sold, just forget about it
-                # TODO move this remove to the sell areas. This isn't done yet because it isn't nailed down
-                del self.__exchanges[i]
-                continue
-            profitable = self.__exchanges[i].is_profitable()
-            # This makes sure there is some degree of profitability
-            profitableSellPrice = self.__exchanges[i].is_profitable_buy()
-            # If the current price is higher than the exchange's minimum to be considered sellable
-            if self.__show:
-                print(str(price) + " needs to get past: " + str(
-                    profitableSellPrice + (profitableSellPrice * Constants.SELL_MIN)))
-                print("Minimum to surpass fees: " + str(profitableSellPrice))
-
-            if price > profitableSellPrice + (profitableSellPrice * Constants.SELL_MIN):
-                # Set allow it to be up for selling
-                self.__exchanges[i].setIfPastSellMin(True)
-                print("Exchange " + str(self.__exchanges[i].get_purchase_id()) + " is now past sell min")
-
-            # Now check if the price has dropped from the sell min to almost loosing profit
-            """ 
-            This way of making sure it has to go past a minimum allows it to keep rising past 
-            to emergency sell it has to be less than profitable, then less than the minimum sellable price minus half 
-            of that difference. This puts a buffer in that ensures slight price deviations won't mean it gets sold 
-            """
-            if self.__exchanges[i].getIfPastSellMin() and (profitableSellPrice < price < (
-                    (profitableSellPrice + (profitableSellPrice * Constants.SELL_MIN)) - (
-                    (profitableSellPrice + (profitableSellPrice * Constants.SELL_MIN)) - profitableSellPrice) / 2)):
-                self.__exchanges[i].sellSelf()
-                print("Emergency selling self: " + str(self.__exchanges[i].get_purchase_id()))
-                print("Now have: " + str(LocalAccount.account["USD"]) + " USD")
-                print("and: " + str(LocalAccount.account["BTC-USD"]) + " BTC")
-            elif (profitableSellPrice < price < ((profitableSellPrice + (profitableSellPrice * Constants.SELL_MIN)) - ((profitableSellPrice + (profitableSellPrice * Constants.SELL_MIN)) - profitableSellPrice) / 2)) and (
-            not self.__exchanges[i].getIfPastSellMin()):
-                # Sell even if it hasn't gone very high and the price is looking down
-                if self.__utils.get_price_derivative(self.__ticker, Constants.EMERGENCY_SELL_SAMPLE) < 0:
-                    print("Emergency selling self because market is trending downwards: " + str(
-                        self.__exchanges[i].get_purchase_id()))
-                    self.__exchanges[i].sellSelf()
-
-            elif not profitable:
-                if self.__show:
-                    print("Exchange " + str(self.__exchanges[i].get_purchase_id()) + " is not profitable")
diff --git a/Blankly/TradingBot.py b/Blankly/TradingBot.py
deleted file mode 100644
index bc58c461..00000000
--- a/Blankly/TradingBot.py
+++ /dev/null
@@ -1,170 +0,0 @@
-"""
-    Main script for testing some internal library functions
-    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 <https://www.gnu.org/licenses/>.
-"""
-
-import utils
-import time
-from Server import TradeInterface as TI
-
-
-""" Define secret keys """
-API_KEY = Keys.API_KEY
-API_SECRET = Keys.API_SECRET
-API_PASS = Keys.API_PASS
-
-
-""" Define used currencies """
-currencies = ["BTC", "XLM"]
-market = "USD"
-
-""" Define known best buy times """
-buys = ["2:00"]
-sells = ["21:00"]
-
-""" Define libraries & Connections """
-utils = utils.Utils()
-Server = TI()
-
-# exchange = Coinbase_Pro("bill", {}, [API_KEY, API_SECRET, API_PASS])
-
-
-# print("State:")
-# exchange.append_model("COMP")
-# print(exchange.get_portfolio_state())
-# print("State with everything:")
-# print("Started Model")
-# exchange.start_models()
-Server.init()
-Server.add_exchange("API Portfolio", "coinbase_pro", [API_KEY, API_SECRET, API_PASS])
-# calls = API(API_KEY, API_SECRET, API_PASS)
-print(Server.get_portfolio_state("API Portfolio"))
-Server.assign_model("API Portfolio", "Bill's Model", "COMP")
-# print(calls.getPortfolio())
-# print(Server.get_exchange_state("API Portfolio"))
-Server.run_model("API Portfolio")
-while True:
-    print(Server.get_portfolio_state("API Portfolio"))
-    time.sleep(1)
-
-sys.exit()
-
-
-
-
-
-bitcoinTicker = Tickers("BTC-USD", show=False)
-time.sleep(2)
-
-"""you always quit as soon as the going gets tough"""
-
-print(call.get_portfolio())
-print(call.getAccountInfo('GRT-USD'))
-sys.exit()
-
-
-ids = 0
-def nextId():
-    global ids
-    ids += 1
-    return {
-        "id": ids
-    }
-
-
-manager = ProfitManager("BTC", bitcoinTicker)
-
-
-# fit = utils.fitParabola(bitcoinTicker, 10000)
-manager.addExchange(Exchange.Exchange(utils.generateMarketOrder(.001, "buy", "BTC-USD"), bitcoinTicker))
-
-# Main thread becomes I/O thread
-while True:
-    command = input("View LocalAccount (v) or sell exchange 0 (s):")
-    if command == "v":
-        print("USD: " + str(LocalAccount.account["USD"]))
-        print("BTC: " + str(LocalAccount.account["BTC-USD"]))
-    elif command == "s":
-        manager.getExchange(0).sellSelf()
-        print("Selling all: ")
-        print("USD: " + str(LocalAccount.account["USD"]))
-        print("BTC: " + str(LocalAccount.account["BTC-USD"]))
-    else:
-        print("Command not found")
-
-sys.exit()
-print("Fee:")
-exchange.get_fee(show=True)
-print("Value:")
-print(exchange.get_value_at_time())
-print("Profitable")
-exchange.is_profitable_buy(show=True)
-sys.exit()
-
-# print(utils.getPriceDerivative(bitcoinTicker, 1000))
-#
-# while True:
-#     time.sleep(1)
-#
-# order = utils.generateLimitOrder(Constants.MINIMUM_TO_SET_LIMIT, 15000, "buy", "BTC-USD")
-# response, exchange = call.placeOrder(order, bitcoinTicker, show=True)
-# print("Placed order")
-# time.sleep(5)
-# orders = call.getOpenOrders(show=True)
-# print("Open Orders")
-# print(orders)
-# print("Canceling order")
-# exchange.cancelOrder()
-# time.sleep(3)
-# print(exchange.confirmCanceled())
-#
-# while True:
-#     time.sleep(1)
-
-
-# order = utils.generateLimitOrder(Constants.MINIMUM_TO_SET_LIMIT, 15000, "buy", "BTC-USD")
-# response, exchange = call.placeOrder(order, bitcoinTicker, show=True)
-
-response = {
-    'id': 3
-}
-call.getAccountInfo("BTC", "balance")
-sys.exit()
-exchange = Exchange.Exchange("buy", 46, bitcoinTicker, response, call)
-while True:
-    print(exchange.is_profitable(show=True))
-    time.sleep(1)
-    utils.get_price_derivative(bitcoinTicker, 10)
-
-""" Main trade loop """
-scheduler = sched.scheduler(time.time, time.sleep)
-
-def tradeFunction(id):
-    print("started at " + str(time.time()))
-    counter = 0
-    while counter < 5:
-        print("Trading " + id + ": " + str(counter))
-        time.sleep(1)
-        counter += 1
-    print("finished at " + str(time.time()))
-
-
-schedule.every().minute.at(":00").do(tradeFunction, "no1")
-schedule.every().minute.at(":02").do(tradeFunction, "no1")
-
-while True:
-    schedule.run_pending()
-    time.sleep(1)
\ No newline at end of file
diff --git a/Blankly/__init__.py b/Blankly/__init__.py
deleted file mode 100644
index a6b84e1e..00000000
--- a/Blankly/__init__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""
-    __init__ file to give the module access to the libraries.
-    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 <https://www.gnu.org/licenses/>.
-"""
-
-from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro import Coinbase_Pro as Coinbase_Pro
-from Blankly.exchanges.Binance.Binance import Binance as Binance
-from Blankly.exchanges.Alpaca.Alpaca import Alpaca as Alpaca
-from Blankly.exchanges.Paper_Trade.Paper_Trade import PaperTrade
-from Blankly.blankly_bot import BlanklyBot
-
-from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_API import API as Direct_Coinbase_Pro_API
-from Blankly.exchanges.ticker_manager import TickerManager as TickerManager
-from Blankly.exchanges.orderbook_manager import OrderbookManger as OrderbookManager
-from Blankly.exchanges.general_stream_manager import GeneralManager as GeneralManager
-from Blankly.interface.abc_currency_interface import ICurrencyInterface as Interface
-import Blankly.utils.utils as utils
-from Blankly.utils.scheduler import Scheduler
-import Blankly.indicators as indicators
-from Blankly.utils import time_builder
-
-from Blankly.strategy.strategy_base import Strategy
diff --git a/Blankly/auth/Alpaca/auth.py b/Blankly/auth/Alpaca/auth.py
deleted file mode 100644
index 5b868e7e..00000000
--- a/Blankly/auth/Alpaca/auth.py
+++ /dev/null
@@ -1,26 +0,0 @@
-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/auth.py b/Blankly/auth/Binance/auth.py
deleted file mode 100644
index a24ec4ba..00000000
--- a/Blankly/auth/Binance/auth.py
+++ /dev/null
@@ -1,26 +0,0 @@
-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/auth.py b/Blankly/auth/Coinbase/auth.py
deleted file mode 100644
index c71a5414..00000000
--- a/Blankly/auth/Coinbase/auth.py
+++ /dev/null
@@ -1,29 +0,0 @@
-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/abc_auth.py b/Blankly/auth/abc_auth.py
deleted file mode 100644
index b48357f3..00000000
--- a/Blankly/auth/abc_auth.py
+++ /dev/null
@@ -1,52 +0,0 @@
-"""
-    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 <https://www.gnu.org/licenses/>.
-"""
-
-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_factory.py b/Blankly/auth/auth_factory.py
deleted file mode 100644
index ab08008e..00000000
--- a/Blankly/auth/auth_factory.py
+++ /dev/null
@@ -1,16 +0,0 @@
-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
deleted file mode 100644
index 02bce780..00000000
--- a/Blankly/auth/utils.py
+++ /dev/null
@@ -1,22 +0,0 @@
-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_API.py b/Blankly/exchanges/Alpaca/Alpaca_API.py
deleted file mode 100644
index b60b6e22..00000000
--- a/Blankly/exchanges/Alpaca/Alpaca_API.py
+++ /dev/null
@@ -1,32 +0,0 @@
-from Blankly.auth.Alpaca.auth import alpaca_auth
-
-import alpaca_trade_api
-import os
-
-APCA_API_LIVE_URL = "https://api.alpaca.markets"
-APCA_API_PAPER_URL = "https://paper-api.alpaca.markets"
-
-def create_alpaca_client(auth: alpaca_auth, paper_trading=True):
-    if paper_trading:
-        api_url = APCA_API_PAPER_URL
-    else:
-        api_url = APCA_API_LIVE_URL
-
-    return alpaca_trade_api.REST(auth.API_KEY, auth.API_SECRET, api_url, 'v2', raw_data=True)
-
-class API:
-    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 = alpaca_trade_api.REST(auth.API_KEY, auth.API_SECRET, self.__api_url, 'v2')
-
-
-api = os.getenv("ALPACA_PUBLIC")
-secret = os.getenv("ALPACA_PRIVATE")
-
-if __name__ == "__main__":
-    client = API(api, secret, True)
-    print(client.alp_client.list_assets())
diff --git a/Blankly/exchanges/Alpaca/alpaca_api_interface.py b/Blankly/exchanges/Alpaca/alpaca_api_interface.py
deleted file mode 100644
index dde2523b..00000000
--- a/Blankly/exchanges/Alpaca/alpaca_api_interface.py
+++ /dev/null
@@ -1,159 +0,0 @@
-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
-
-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, preferences_path: str):
-        super().__init__('alpaca', authenticated_API, preferences_path)
-        assert isinstance(self.calls, alpaca_trade_api.REST)
-
-    def init_exchange(self):
-        assert isinstance(self.calls, alpaca_trade_api.REST)
-        account_info = self.calls.get_account()
-        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) -> dict:
-        '''
-        [
-            {
-              "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)
-
-        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, alpaca_trade_api.REST)
-        needed = self.needed['get_account']
-
-        account_dict = self.calls.get_account()
-        account_dict['available'] = 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, alpaca_trade_api.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)
-        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, alpaca_trade_api.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, alpaca_trade_api.REST)
-        needed = self.needed['get_open_orders']
-        orders = self.calls.list_orders()
-        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, alpaca_trade_api.REST)
-        needed = self.needed['get_order']
-        order = self.calls.get_order(order_id)
-        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, alpaca_trade_api.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, alpaca_trade_api.REST)
-
-        pass
-
-    # TODO: tbh not sure how this one works
-    def get_market_limits(self, product_id):
-        assert isinstance(self.calls, alpaca_trade_api.REST)
-        pass
-
-    def get_price(self, currency_pair) -> float:
-        assert isinstance(self.calls, alpaca_trade_api.REST)
-        response = self.calls.get_last_trade()
-        return float(response['p'])
\ No newline at end of file
diff --git a/Blankly/exchanges/Binance/Binance.py b/Blankly/exchanges/Binance/Binance.py
deleted file mode 100644
index 81955623..00000000
--- a/Blankly/exchanges/Binance/Binance.py
+++ /dev/null
@@ -1,67 +0,0 @@
-"""
-    Binance 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 <https://www.gnu.org/licenses/>.
-"""
-
-
-from Blankly.exchanges.exchange import Exchange
-import Blankly.auth_constructor
-import Blankly.utils.utils as utils
-
-from binance.client import Client
-
-
-class Binance(Exchange):
-    def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
-        # Load the auth from the keys file
-        auth, defined_name = Blankly.auth_constructor.load_auth_binance(keys_path, portfolio_name)
-
-        Exchange.__init__(self, "binance", defined_name, settings_path)
-
-        preferences = utils.load_user_preferences()
-        if preferences["settings"]["use_sandbox"] or preferences["settings"]["paper_trade"]:
-            self.__calls = Client(api_key=auth[0], api_secret=auth[1],
-                                  tld=self.get_preferences()["settings"]["binance_tld"],
-                                  testnet=True)
-        else:
-            self.__calls = Client(api_key=auth[0], api_secret=auth[1],
-                                  tld=self.get_preferences()["settings"]["binance_tld"])
-
-        Blankly.auth_constructor.write_auth_cache("binance", defined_name, self.__calls)
-
-        self.construct_interface(self.__calls)
-
-    """
-    Builds information about the currency on this exchange by making particular API calls
-    """
-    def get_currency_state(self, currency):
-        """
-        Portfolio state is the internal properties for the exchange block.
-        """
-        # TODO Populate this with useful information
-        currency = Blankly.utils.get_base_currency(currency)
-        account = self.Interface.get_account(currency=currency)
-        return account
-
-    def get_exchange_state(self):
-        """
-        Exchange state is the external properties for the exchange block
-        """
-        # TODO Populate this with useful information
-        return self.Interface.get_fees()
-
-    def get_direct_calls(self) -> Client:
-        return self.__calls
diff --git a/Blankly/exchanges/Coinbase_Pro/orderbook_cbpro.py b/Blankly/exchanges/Coinbase_Pro/orderbook_cbpro.py
deleted file mode 100644
index 5673b136..00000000
--- a/Blankly/exchanges/Coinbase_Pro/orderbook_cbpro.py
+++ /dev/null
@@ -1,251 +0,0 @@
-"""
-    Orderbook websocket feed.
-    Copyright (c) 2017 Daniel Paquin, modified by Emerson Dove
-
-    Permission is hereby granted, free of charge, to any person obtaining a copy
-    of this software and associated documentation files (the "Software"), to deal
-    in the Software without restriction, including without limitation the rights
-    to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-    copies of the Software, and to permit persons to whom the Software is
-    furnished to do so, subject to the following conditions:
-
-    The above copyright notice and this permission notice shall be included in all
-    copies or substantial portions of the Software.
-"""
-
-from sortedcontainers import SortedDict
-from decimal import Decimal
-import pickle
-
-from cbpro.public_client import PublicClient
-from cbpro.websocket_client import WebsocketClient
-
-
-class OrderBook(WebsocketClient):
-    def __init__(self, product_id='BTC-USD', log_to=None):
-        super(OrderBook, self).__init__(products=product_id)
-        self._asks = SortedDict()
-        self._bids = SortedDict()
-        self._client = PublicClient()
-        self._sequence = -1
-        self._log_to = log_to
-        if self._log_to:
-            assert hasattr(self._log_to, 'write')
-        self._current_ticker = None
-
-    @property
-    def product_id(self):
-        """ Currently OrderBook only supports a single product even though it is stored as a list of products. """
-        return self.products[0]
-
-    def on_open(self):
-        self._sequence = -1
-        print("-- Subscribed to OrderBook! --\n")
-
-    def on_close(self):
-        print("\n-- OrderBook Socket Closed! --")
-
-    def reset_book(self):
-        self._asks = SortedDict()
-        self._bids = SortedDict()
-        res = self._client.get_product_order_book(product_id=self.product_id, level=3)
-        for bid in res['bids']:
-            self.add({
-                'id': bid[2],
-                'side': 'buy',
-                'price': Decimal(bid[0]),
-                'size': Decimal(bid[1])
-            })
-        for ask in res['asks']:
-            self.add({
-                'id': ask[2],
-                'side': 'sell',
-                'price': Decimal(ask[0]),
-                'size': Decimal(ask[1])
-            })
-        self._sequence = res['sequence']
-
-    def on_message(self, message):
-        if self._log_to:
-            pickle.dump(message, self._log_to)
-
-        sequence = message.get('sequence', -1)
-        if self._sequence == -1:
-            self.reset_book()
-            return
-        if sequence <= self._sequence:
-            # ignore older messages (e.g. before order book initialization from getProductOrderBook)
-            return
-        elif sequence > self._sequence + 1:
-            self.on_sequence_gap(self._sequence, sequence)
-            return
-
-        msg_type = message['type']
-        if msg_type == 'open':
-            self.add(message)
-        elif msg_type == 'done' and 'price' in message:
-            self.remove(message)
-        elif msg_type == 'match':
-            self.match(message)
-            self._current_ticker = message
-        elif msg_type == 'change':
-            self.change(message)
-
-        self._sequence = sequence
-
-    def on_sequence_gap(self, gap_start, gap_end):
-        self.reset_book()
-        print('Error: messages missing ({} - {}). Re-initializing  book at sequence.'.format(
-            gap_start, gap_end, self._sequence))
-
-    def add(self, order):
-        order = {
-            'id': order.get('order_id') or order['id'],
-            'side': order['side'],
-            'price': Decimal(order['price']),
-            'size': Decimal(order.get('size') or order['remaining_size'])
-        }
-        if order['side'] == 'buy':
-            bids = self.get_bids(order['price'])
-            if bids is None:
-                bids = [order]
-            else:
-                bids.append(order)
-            self.set_bids(order['price'], bids)
-        else:
-            asks = self.get_asks(order['price'])
-            if asks is None:
-                asks = [order]
-            else:
-                asks.append(order)
-            self.set_asks(order['price'], asks)
-
-    def remove(self, order):
-        price = Decimal(order['price'])
-        if order['side'] == 'buy':
-            bids = self.get_bids(price)
-            if bids is not None:
-                bids = [o for o in bids if o['id'] != order['order_id']]
-                if len(bids) > 0:
-                    self.set_bids(price, bids)
-                else:
-                    self.remove_bids(price)
-        else:
-            asks = self.get_asks(price)
-            if asks is not None:
-                asks = [o for o in asks if o['id'] != order['order_id']]
-                if len(asks) > 0:
-                    self.set_asks(price, asks)
-                else:
-                    self.remove_asks(price)
-
-    def match(self, order):
-        size = Decimal(order['size'])
-        price = Decimal(order['price'])
-
-        if order['side'] == 'buy':
-            bids = self.get_bids(price)
-            if not bids:
-                return
-            assert bids[0]['id'] == order['maker_order_id']
-            if bids[0]['size'] == size:
-                self.set_bids(price, bids[1:])
-            else:
-                bids[0]['size'] -= size
-                self.set_bids(price, bids)
-        else:
-            asks = self.get_asks(price)
-            if not asks:
-                return
-            assert asks[0]['id'] == order['maker_order_id']
-            if asks[0]['size'] == size:
-                self.set_asks(price, asks[1:])
-            else:
-                asks[0]['size'] -= size
-                self.set_asks(price, asks)
-
-    def change(self, order):
-        try:
-            new_size = Decimal(order['new_size'])
-        except KeyError:
-            return
-
-        try:
-            price = Decimal(order['price'])
-        except KeyError:
-            return
-
-        if order['side'] == 'buy':
-            bids = self.get_bids(price)
-            if bids is None or not any(o['id'] == order['order_id'] for o in bids):
-                return
-            index = [b['id'] for b in bids].index(order['order_id'])
-            bids[index]['size'] = new_size
-            self.set_bids(price, bids)
-        else:
-            asks = self.get_asks(price)
-            if asks is None or not any(o['id'] == order['order_id'] for o in asks):
-                return
-            index = [a['id'] for a in asks].index(order['order_id'])
-            asks[index]['size'] = new_size
-            self.set_asks(price, asks)
-
-        tree = self._asks if order['side'] == 'sell' else self._bids
-        node = tree.get(price)
-
-        if node is None or not any(o['id'] == order['order_id'] for o in node):
-            return
-
-    def get_current_ticker(self):
-        return self._current_ticker
-
-    def get_current_book(self):
-        result = {
-            'sequence': self._sequence,
-            'asks': [],
-            'bids': [],
-        }
-        for ask in self._asks:
-            try:
-                # There can be a race condition here, where a price point is removed
-                # between these two ops
-                this_ask = self._asks[ask]
-            except KeyError:
-                continue
-            for order in this_ask:
-                result['asks'].append([order['price'], order['size'], order['id']])
-        for bid in self._bids:
-            try:
-                # There can be a race condition here, where a price point is removed
-                # between these two ops
-                this_bid = self._bids[bid]
-            except KeyError:
-                continue
-
-            for order in this_bid:
-                result['bids'].append([order['price'], order['size'], order['id']])
-        return result
-
-    def get_ask(self):
-        return self._asks.peekitem(0)[0]
-
-    def get_asks(self, price):
-        return self._asks.get(price)
-
-    def remove_asks(self, price):
-        del self._asks[price]
-
-    def set_asks(self, price, asks):
-        self._asks[price] = asks
-
-    def get_bid(self):
-        return self._bids.peekitem(-1)[0]
-
-    def get_bids(self, price):
-        return self._bids.get(price)
-
-    def remove_bids(self, price):
-        del self._bids[price]
-
-    def set_bids(self, price, bids):
-        self._bids[price] = bids
diff --git a/Blankly/exchanges/Coinbase_Pro/orderbook_websocket.py b/Blankly/exchanges/Coinbase_Pro/orderbook_websocket.py
deleted file mode 100644
index a5ae51df..00000000
--- a/Blankly/exchanges/Coinbase_Pro/orderbook_websocket.py
+++ /dev/null
@@ -1,192 +0,0 @@
-"""
-    Coinbase Pro orderbook websocket feed.
-    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 <https://www.gnu.org/licenses/>.
-"""
-import threading
-import json
-import ssl
-import time
-import traceback
-import Blankly
-import collections
-from Blankly.exchanges.IExchange_Websocket import IExchangeWebsocket
-
-from websocket import create_connection
-
-"""
---- DEPRECATED FILE. All orderbook management now goes through the Coinbase Pro Websocket file. -----
-"""
-
-
-def create_websocket_connection(id, url):
-    ws = create_connection(url, sslopt={"cert_reqs": ssl.CERT_NONE})
-    request = """{
-    "type": "subscribe",
-    "product_ids": [
-        \"""" + id + """\"
-    ],
-    "channels": [
-        "level2"
-    ]
-    }"""
-    ws.send(request)
-    return ws
-
-
-class OrderBook(IExchangeWebsocket):
-    def __init__(self, currency_id, WEBSOCKET_URL="wss://ws-feed.pro.coinbase.com"):
-        """
-        Create and initialize the orderbook connection
-        Args:
-            currency_id: Currency to listen on such as "BTC-USD"
-            WEBSOCKET_URL: Default websocket URL feed.
-        """
-        self.__id = currency_id
-
-        self.URL = WEBSOCKET_URL
-        self.ws = None
-        self.__response = None
-        self.__most_recent_tick = None
-        self.__most_recent_time = None
-        self.__callbacks = []
-        self.__snapshot_callbacks = []
-
-        # Load preferences and create the buffers
-        self.__preferences = Blankly.utils.load_user_preferences()
-        buffer_size = self.__preferences["settings"]["websocket_buffer_size"]
-        self.__orderbook_feed = collections.deque(maxlen=buffer_size)
-        self.__time_feed = collections.deque(maxlen=buffer_size)
-
-        # Create the snapshot load
-        self.__snapshot = None
-
-        # Start the websocket
-        self.start_websocket()
-
-    # This could be made static with some changes, would make the code in the loop cleaner
-    def start_websocket(self):
-        """
-        Restart websocket if it was asked to stop.
-        """
-        if self.ws is None:
-            self.ws = create_websocket_connection(self.__id, self.URL)
-            self.__response = self.ws.recv()
-            thread = threading.Thread(target=self.read_websocket)
-            thread.start()
-        else:
-            if self.ws.connected:
-                print("Already running...")
-                pass
-            else:
-                # Use recursion to restart, continue appending to time feed and orderbook feed
-                self.ws = None
-                self.start_websocket()
-
-    def read_websocket(self):
-        # This is unique because coinbase first sends the entire orderbook to use
-        received_string = json.loads(self.ws.recv())
-        if received_string['type'] == 'snapshot':
-            self.__snapshot = received_string
-        try:
-            for i in self.__snapshot_callbacks:
-                i(received_string)
-        except:
-            traceback.print_exc()
-
-        # TODO port this to "WebSocketApp" found in the websockets documentation
-        while self.ws.connected:
-            # In case the user closes while its reading from the websocket
-            persist_connected = self.ws.connected
-            try:
-                received_string = self.ws.recv()
-                received = json.loads(received_string)
-                self.__most_recent_time = Blankly.utils.epoch_from_ISO8601(received['time'])
-                received['time'] = self.__most_recent_time
-                self.__orderbook_feed.append(received)
-                self.__most_recent_tick = received
-                self.__time_feed.append(self.__most_recent_time)
-
-                # Manage price events and fire for each manager attached
-                try:
-                    for i in range(len(self.__callbacks)):
-                        self.__callbacks[i](received)
-                except Exception:
-                    traceback.print_exc()
-
-            except Exception as e:
-                if persist_connected:
-                    traceback.print_exc()
-                    continue
-                else:
-                    print(traceback.format_exc())
-                    print("Error reading orderbook for " + self.__id + ": attempting to re-initialize")
-                    # Give a delay so this doesn't eat up from the main thread if it takes many tries to initialize
-                    time.sleep(2)
-                    self.ws.close()
-                    self.ws = create_websocket_connection(self.__id, self.URL)
-                    # Update response
-                    self.__response = self.ws.recv()
-                    # This allows us to put initialization at the top of the function
-                    self.read_websocket()
-                    break
-
-    """ Required in manager """
-    def is_websocket_open(self):
-        return self.ws.connected
-
-    def get_currency_id(self):
-        return self.__id
-
-    """ Required in manager """
-    def append_callback(self, obj):
-        self.__callbacks.append(obj)
-
-    def append_snapshot_callback(self, obj):
-        self.__snapshot_callbacks.append(obj)
-
-    """ Define a variable each time so there is no array manipulation """
-    """ Required in manager """
-    def get_most_recent_tick(self):
-        return self.__most_recent_tick
-
-    """ Required in manager """
-    def get_most_recent_time(self):
-        return self.__most_recent_time
-
-    """ Required in manager """
-    def get_time_feed(self):
-        return list(self.__time_feed)
-
-    """ Parallel with time feed """
-    """ Required in manager """
-    def get_feed(self):
-        return list(self.__orderbook_feed)
-
-    """ Required in manager """
-    def get_response(self):
-        return self.__response
-
-    """ Required in manager """
-    def close_websocket(self):
-        if self.ws.connected:
-            self.ws.close()
-        else:
-            print("Websocket for " + self.__id + " is already closed")
-
-    """ Required in manager """
-    def restart_ticker(self):
-        self.start_websocket()
-
diff --git a/Blankly/exchanges/orderbook_manager.py b/Blankly/exchanges/orderbook_manager.py
deleted file mode 100644
index 81a052c7..00000000
--- a/Blankly/exchanges/orderbook_manager.py
+++ /dev/null
@@ -1,229 +0,0 @@
-"""
-    Class to manage the orderbook, adding, removing and updating - as well as provide user interaction
-    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 <https://www.gnu.org/licenses/>.
-"""
-import Blankly.utils.utils
-import Blankly.auth_constructor
-import requests
-
-from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_Websocket import Tickers as Coinbase_Pro_Orderbook
-from Blankly.exchanges.Binance.Binance_Websocket import Tickers as Binance_Orderbook
-
-from Blankly.exchanges.websocket_manager import WebsocketManager
-
-
-def binance_snapshot(symbol, limit):
-    # TODO this should be using the REST set that Arun puts in
-    params = {
-        "symbol": symbol,
-        "limit": limit
-    }
-    response = requests.get("https://api.binance.com/api/v3/depth", params=params).json()
-    buys_response = response['bids']
-    sells_response = response['asks']
-    buys = {}
-    sells = {}
-    for i in buys_response:
-        buys[float(i[0])] = float(i[1])
-    for i in sells_response:
-        sells[float(i[0])] = float(i[1])
-
-    return buys, sells
-
-
-class OrderbookManger(WebsocketManager):
-    def __init__(self, default_exchange, default_currency):
-        """
-        Create a new orderbook manager
-        Args:
-            default_exchange: Add an exchange name for the manager to favor
-            default_currency: Add a default currency for the manager to favor
-        """
-        self.__default_exchange = default_exchange
-        self.__default_currency = default_currency
-        self.__orderbooks = {}
-        self.__orderbooks[default_exchange] = {}
-        self.__websockets = {}
-        self.__websockets[default_exchange] = {}
-        self.__websockets_callbacks = {}
-        self.__websockets_callbacks[default_exchange] = {}
-
-        # Create the abstraction for adding many managers
-        super().__init__(self.__websockets, default_currency, default_exchange)
-
-    def create_orderbook(self, callback, currency_id=None, override_exchange=None):
-        """
-        Create an orderbook for a given exchange
-        Args:
-            callback: Callback object for the function. Should be something like self.price_event
-            currency_id: Override the default currency id
-            override_exchange: Override the default exchange
-        """
-        exchange_name = self.__default_exchange
-        # Ensure the ticker dict has this overridden exchange
-        if override_exchange is not None:
-            if override_exchange not in self.__websockets.keys():
-                self.__websockets[override_exchange] = {}
-                self.__websockets_callbacks[override_exchange] = {}
-            # Write this value so it can be used later
-            exchange_name = override_exchange
-
-        if exchange_name == "coinbase_pro":
-            if currency_id is None:
-                currency_id = self.__default_currency
-            websocket = Coinbase_Pro_Orderbook(currency_id, "level2", pre_event_callback=self.coinbase_snapshot_update)
-            # This is where the sorting magic happens
-            websocket.append_callback(self.coinbase_update)
-
-            # Store this object
-            self.__websockets['coinbase_pro'][currency_id] = websocket
-            self.__websockets_callbacks['coinbase_pro'][currency_id] = callback
-            self.__orderbooks['coinbase_pro'][currency_id] = {
-                "buy": {},
-                "sell": {}
-            }
-            return websocket
-        elif exchange_name == "binance":
-            if currency_id is None:
-                currency_id = self.__default_currency
-
-            # Lower the keys to subscribe
-            specific_currency_id = Blankly.utils.to_exchange_coin_id(currency_id, "binance").lower()
-            websocket = Binance_Orderbook(specific_currency_id, "depth")
-            websocket.append_callback(self.binance_update)
-
-            # Binance returns the keys in all UPPER so the books should be created based on response
-            specific_currency_id = specific_currency_id.upper()
-            self.__websockets['binance'][specific_currency_id] = websocket
-            self.__websockets_callbacks['binance'][specific_currency_id] = callback
-
-            buys, sells = binance_snapshot(specific_currency_id, 1000)
-            self.__orderbooks['binance'][specific_currency_id] = {
-                "buy": buys,
-                "sell": sells
-            }
-
-        else:
-            print(exchange_name + " ticker not supported, skipping creation")
-
-    def coinbase_snapshot_update(self, update):
-        print("Orderbook snapshot acquired for: " + update['product_id'])
-        # Clear whatever book we had
-        book = {
-            "buy": {},
-            "sell": {}
-        }
-        # Get all bids
-        buys = update['bids']
-        # Convert these to float and write to our order dictionaries
-        for i in range(len(buys)):
-            buy = buys[i]
-            book['buy'][float(buy[0])] = float(buy[1])
-
-        sells = update['asks']
-        for i in range(len(sells)):
-            sell = sells[i]
-            book['sell'][float(sell[0])] = float(sell[1])
-
-        self.__orderbooks['coinbase_pro'][update['product_id']] = book
-
-    def coinbase_update(self, update):
-        # Side is first in tuple
-        side = update['changes'][0][0]
-        # Price is second
-        price = float(update['changes'][0][1])
-        book = self.__orderbooks['coinbase_pro'][update['product_id']][side]
-
-        # Quantity at that point is third
-        qty = float(update['changes'][0][2])
-        if qty == 0:
-            try:
-                book.pop(price)
-            except KeyError:
-                pass
-        else:
-            book[price] = float(update['changes'][0][2])
-        self.__orderbooks['coinbase_pro'][update['product_id']][side] = book
-        self.__websockets_callbacks['coinbase_pro'][update['product_id']](book)
-
-    def binance_update(self, update):
-        # TODO this needs a snapshot to work correctly, which needs arun's rest code
-        # Get symbol first
-        symbol = update['s']
-
-        # Get symbol for orderbook
-        book_buys = self.__orderbooks['binance'][symbol]['buy']
-        book_sells = self.__orderbooks['binance'][symbol]['sell']
-        # Buys are b
-        new_buys = update['b']
-        for i in new_buys:
-            # Price is i[0], quantity is i[1]
-            if float(i[1]) == 0:
-                try:
-                    book_buys.pop(float(i[0]))
-                except KeyError:
-                    pass
-            else:
-                book_buys[float(i[0])] = float(i[1])
-        # Asks are sells
-        new_sells = update['a']
-        for i in new_sells:
-            # Price is i[0], quantity is i[1]
-            if float(i[1]) == 0:
-                try:
-                    book_sells.pop(float(i[0]))
-                except KeyError:
-                    pass
-            else:
-                book_sells[float(i[0])] = float(i[1])
-        self.__orderbooks['binance'][symbol]['buy'] = book_buys
-        self.__orderbooks['binance'][symbol]['sell'] = book_sells
-        # Pass in this new updated orderbook
-        self.__websockets_callbacks['binance'][symbol](self.__orderbooks['binance'][symbol])
-
-    def append_orderbook_callback(self, callback_object, override_currency_id=None, override_exchange=None):
-        """
-        These are appended calls to a sorted orderbook. Functions added to this will be fired every time the orderbook
-        changes.
-        Args:
-            callback_object: Reference for the callback function. The price_event(self, tick)
-                function would be passed in as just self.price_event -- no parenthesis or arguments, just the reference
-            override_currency_id: Ticker id, such as "BTC-USD" or exchange equivalents.
-            override_exchange: Forces the manager to use a different supported exchange.
-        """
-        if override_currency_id is None:
-            override_currency_id = self.__default_currency
-
-        if override_exchange is None:
-            override_exchange = self.__default_exchange
-
-        self.__websockets_callbacks[override_exchange][override_currency_id] = callback_object
-
-    def get_most_recent_orderbook(self, override_currency_id=None, override_exchange=None):
-        """
-        Get the most recent orderbook under a currency and exchange.
-
-        Args:
-            override_currency_id: Ticker id, such as "BTC-USD" or exchange equivalents.
-            override_exchange: Forces the manager to use a different supported exchange.
-        """
-        if override_currency_id is None:
-            override_currency_id = self.__default_currency
-
-        if override_exchange is None:
-            override_exchange = self.__default_exchange
-
-        return self.__orderbooks[override_exchange][override_currency_id]
diff --git a/Blankly/exchanges/ticker_manager.py b/Blankly/exchanges/ticker_manager.py
deleted file mode 100644
index 467176ab..00000000
--- a/Blankly/exchanges/ticker_manager.py
+++ /dev/null
@@ -1,97 +0,0 @@
-"""
-    Script for managing the variety of tickers on different 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 <https://www.gnu.org/licenses/>.
-"""
-import Blankly.utils.utils
-
-from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_Websocket import Tickers as Coinbase_Pro_Ticker
-from Blankly.exchanges.Binance.Binance_Websocket import Tickers as Binance_Ticker
-
-from Blankly.exchanges.websocket_manager import WebsocketManager
-
-
-class TickerManager(WebsocketManager):
-    def __init__(self, default_exchange, default_currency):
-        """
-        Create a new manager.
-        Args:
-            default_exchange: Add an exchange name for the manager to favor
-            default_currency: Add a default currency for the manager to favor
-        """
-        self.__default_exchange = default_exchange
-        if default_exchange == "binance":
-            default_currency = Blankly.utils.to_exchange_coin_id(default_currency, "binance").lower()
-        self.__default_currency = default_currency
-
-        self.__tickers = {}
-        self.__tickers[default_exchange] = {}
-
-        # Create abstraction for writing many different managers
-        super().__init__(self.__tickers, default_currency, default_exchange)
-
-    """ 
-    Manager Functions 
-    """
-    def create_ticker(self, callback, log=None, currency_id=None, override_exchange=None):
-        """
-        Create a ticker on a given exchange.
-        Args:
-            callback: Callback object for the function. Should be something like self.price_event
-            log: Fill this with a path to log the price updates.
-            currency_id: The currency to create a ticker for.
-            override_exchange: Override the default exchange.
-        Returns:
-            Direct ticker object
-        """
-        preferences = Blankly.utils.load_user_preferences()
-        sandbox_mode = preferences['settings']['use_sandbox_websockets']
-
-        exchange_name = self.__default_exchange
-        # Ensure the ticker dict has this overridden exchange
-        if override_exchange is not None:
-            if override_exchange not in self.__tickers.keys():
-                self.__tickers[override_exchange] = {}
-            # Write this value so it can be used later
-            exchange_name = override_exchange
-
-        if exchange_name == "coinbase_pro":
-            if currency_id is None:
-                currency_id = self.__default_currency
-
-            if sandbox_mode:
-                ticker = Coinbase_Pro_Ticker(currency_id, "ticker", log=log,
-                                             WEBSOCKET_URL="wss://ws-feed-public.sandbox.pro.coinbase.com")
-            else:
-                ticker = Coinbase_Pro_Ticker(currency_id, "ticker", log=log)
-
-            ticker.append_callback(callback)
-            # Store this object
-            self.__tickers['coinbase_pro'][currency_id] = ticker
-            return ticker
-        elif exchange_name == "binance":
-            if currency_id is None:
-                currency_id = self.__default_currency
-
-            currency_id = Blankly.utils.to_exchange_coin_id(currency_id, "binance").lower()
-            if sandbox_mode:
-                ticker = Binance_Ticker(currency_id, "trade", log=log, WEBSOCKET_URL="wss://testnet.binance.vision/ws")
-            else:
-                ticker = Binance_Ticker(currency_id, "trade", log=log)
-            ticker.append_callback(callback)
-            self.__tickers['binance'][currency_id] = ticker
-            return ticker
-        else:
-            print(exchange_name + " ticker not supported, skipping creation")
diff --git a/Blankly/indicators/__init__.py b/Blankly/indicators/__init__.py
deleted file mode 100644
index 1dd55584..00000000
--- a/Blankly/indicators/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from Blankly.indicators.indicators import *
-from Blankly.indicators.utils import *
-from Blankly.indicators.moving_averages import *
-from Blankly.indicators.oscillators import *
-from Blankly.indicators.statistics import *
diff --git a/Blankly/indicators/indicators.py b/Blankly/indicators/indicators.py
deleted file mode 100644
index b8a92248..00000000
--- a/Blankly/indicators/indicators.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from Blankly.indicators.utils import convert_to_numpy
-from Blankly.indicators.utils import check_series
-import numpy as np
-import pandas as pd
-import tulipy as ti
-
-
-def bbands(data, period=14, stddev=2):
-    data = convert_to_numpy(data)    
-    return ti.bbands(data, period, stddev)
-
-
-def wad(data, period=50, use_series=False):
-    if check_series(data):
-        use_series = True
-    data = convert_to_numpy(data)
-    wad = ti.wad(data, period)
-    return pd.Series(wad) if use_series else wad
-
-
-def wilders(data, period=50, use_series=False):
-    if check_series(data):
-        use_series = True
-    data = convert_to_numpy(data)
-    wilders = ti.wilders(data, period)
-    return pd.Series(wilders) if use_series else wilders
-
-
-def willr(data, period=50, use_series=False):
-    if check_series(data):
-        use_series = True
-    data = convert_to_numpy(data)
-    willr = ti.willr(data, period)
-    return pd.Series(willr) if use_series else willr
-
-
-def true_range(high_data, low_data, close_data, period=50, use_series=False):
-    if check_series(high_data) or check_series(low_data) or check_series(close_data):
-        use_series = True
-    high_data = convert_to_numpy(high_data)
-    low_data = convert_to_numpy(low_data)
-    close_data = convert_to_numpy(close_data)
-    tr = ti.tr(high_data, low_data, close_data, period=period)
-    return pd.Series(tr) if use_series else tr
-
-
-def average_true_range(high_data, low_data, close_data, period=50, use_series=False):
-    if check_series(high_data) or check_series(low_data) or check_series(close_data):
-        use_series = True
-    high_data = convert_to_numpy(high_data)
-    low_data = convert_to_numpy(low_data)
-    close_data = convert_to_numpy(close_data)
-    atr = ti.atr(high_data, low_data, close_data, period=period)
-    return pd.Series(atr) if use_series else atr
-
diff --git a/Blankly/indicators/utils.py b/Blankly/indicators/utils.py
deleted file mode 100644
index df66907e..00000000
--- a/Blankly/indicators/utils.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from typing import Any
-import pandas as pd
-import numpy as np
-
-
-def to_historical_returns(data: Any):
-    return pd.Series(data).diff().tolist()
-
-
-def convert_to_numpy(data: Any): 
-    if type(data) == list:
-        return np.asarray(data)
-    elif type(data) == pd.Series:
-        return data.to_numpy()
-    return data
-
-
-def check_series(data: Any):
-    return isinstance(data, pd.Series)
diff --git a/Blankly/interface/README.md b/Blankly/interface/README.md
deleted file mode 100644
index 0f221f05..00000000
--- a/Blankly/interface/README.md
+++ /dev/null
@@ -1,39 +0,0 @@
-## Decisions for return types and values of the Interface class
-
-### get_calls()
-- Not relevant 
-
-### get_products()
-List for each product:
-```json
-[
-  "currency_id": "BTC-USD",
-  "base_currency": "BTC",
-  "quote_currency": "USD",
-  "base_min_size": "0.01",
-  "base_max_size": "10000.00"
-]
-```
-
-### get_account(account)
-```json
-[
-    {
-        "id": "71452118-efc7-4cc4-8780-a5e22d4baa53",
-        "currency": "BTC",
-        "balance": "0.0000000000000000",
-        "available": "0.0000000000000000",
-        "hold": "0.0000000000000000"
-    },
-    {
-        ...
-    }
-]
-```
-This may be better if users are allowed to search through for a currency because that would remove the need for an ID
-
-### Orders
-------- skipped placement
-
-
-### Cancel order
\ No newline at end of file
diff --git a/Blankly/interface/abc_currency_interface.py b/Blankly/interface/abc_currency_interface.py
deleted file mode 100644
index e11cbc12..00000000
--- a/Blankly/interface/abc_currency_interface.py
+++ /dev/null
@@ -1,187 +0,0 @@
-"""
-    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 <https://www.gnu.org/licenses/>.
-"""
-
-
-import abc
-from Blankly.utils.purchases.limit_order import LimitOrder
-from Blankly.utils.purchases.market_order import MarketOrder
-import pandas
-
-
-class ICurrencyInterface(abc.ABC):
-    @abc.abstractmethod
-    def __init__(self, exchange_name, authenticated_API):
-        """
-        Create a currency interface
-        Args:
-            exchange_name (str): Define exchange name ex: 'binance' or 'coinbase_pro'
-            authenticated_API (obj): Authenticated direct calls object
-        """
-        pass
-
-    @abc.abstractmethod
-    def get_calls(self):
-        """
-        Get the direct & authenticated exchange object
-
-        Returns:
-             The exchange's direct calls object. A Blankly Bot class should have immediate access to this by
-             default
-        """
-        pass
-
-    @abc.abstractmethod
-    def get_exchange_type(self):
-        """
-        Get the type of exchange ex: "coinbase_pro" or "binance"
-
-        Returns:
-             The exchange's direct calls object. A Blankly Bot class should have immediate access to this by
-             default
-
-        TODO add return example
-        """
-        pass
-
-    @abc.abstractmethod
-    def get_products(self):
-        """
-        Get all trading pairs on the exchange & some information about the exchange limits.
-
-        TODO add return example
-        """
-        pass
-
-    @abc.abstractmethod
-    def get_account(self, currency=None):
-        """
-        Get all currencies in an account, or sort by currency/account_id
-        Args:
-            currency (Optional): Filter by particular currency
-
-            These arguments are mutually exclusive
-
-        TODO add return example
-        """
-        pass
-
-    @abc.abstractmethod
-    def market_order(self, product_id, side, funds) -> MarketOrder:
-        """
-        Used for buying or selling market orders
-        Args:
-            product_id: currency to buy
-            side: buy/sell
-            funds: desired amount of quote currency to use
-        """
-        pass
-
-    @abc.abstractmethod
-    def limit_order(self, product_id, side, price, size) -> LimitOrder:
-        """
-        Used for buying or selling limit orders
-        Args:
-            product_id: currency to buy
-            side: buy/sell
-            price: price to set limit order
-            size: amount of currency (like BTC) for the limit to be valued
-        """
-        pass
-
-    @abc.abstractmethod
-    def cancel_order(self, currency_id, order_id) -> dict:
-        """
-        Cancel an order on a particular currency id & order id
-
-        Args:
-            currency_id: This is the currency id that the order is under
-            order_id: The unique ID of the order.
-
-        TODO add return example
-        """
-
-    @abc.abstractmethod
-    def get_open_orders(self, product_id=None):
-        """
-        List open orders.
-        Args:
-            product_id (optional) (str): Currency pair such as BTC-USD
-        TODO add return example
-        """
-        pass
-
-    @abc.abstractmethod
-    def get_order(self, currency_id, order_id) -> dict:
-        """
-        Get a certain order
-        Args:
-            currency_id: This is the currency id that the order is under
-            order_id: The unique ID of the order.
-        TODO add return example
-        """
-        pass
-
-    @abc.abstractmethod
-    def get_fees(self) -> dict:
-        """
-        Get market fees
-        TODO add return example
-        """
-        pass
-
-    """
-    Coinbase Pro: get_account_history
-    Binance: 
-        get_deposit_history
-        get_withdraw_history
-
-    """
-
-    @abc.abstractmethod
-    def get_product_history(self, product_id, epoch_start, epoch_stop, granularity) -> pandas.DataFrame:
-        """
-        Returns the product history from an exchange
-        Args:
-            product_id: Blankly product ID format (BTC-USD)
-            epoch_start: Time to begin download
-            epoch_stop: Time to stop download
-            granularity: Resolution in seconds between tick (ex: 60 = 1 per minute)
-        Returns:
-            Dataframe with *at least* 'time (epoch)', 'low', 'high', 'open', 'close', 'volume' as columns.
-            TODO add return example
-        """
-
-    @abc.abstractmethod
-    def get_market_limits(self, product_id):
-        """
-        Find order limits for the exchange
-        Args:
-            product_id: Blankly product ID format (BTC-USD)
-            TODO add return example
-        """
-        pass
-
-    @abc.abstractmethod
-    def get_price(self, currency_pair) -> float:
-        """
-        Returns just the price of a currency pair.
-        Args:
-            currency_pair: Blankly product ID ex: BTC-USD
-            TODO add return example
-        """
-        pass
diff --git a/Blankly/interface/currency_Interface.py b/Blankly/interface/currency_Interface.py
deleted file mode 100644
index d0e4f085..00000000
--- a/Blankly/interface/currency_Interface.py
+++ /dev/null
@@ -1,177 +0,0 @@
-"""
-    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 <https://www.gnu.org/licenses/>.
-"""
-
-import Blankly.utils.utils as utils
-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, preferences_path=None):
-        self.exchange_name = exchange_name
-        self.calls = authenticated_API
-        # Reload user preferences here
-        self.user_preferences = utils.load_user_preferences(preferences_path)
-
-        # TODO, improve creation of its own properties
-        self.exchange_properties = None
-        # Some exchanges like binance will not return a value of 0.00 if there is no balance
-        self.available_currencies = {}
-        self.init_exchange()
-
-        self.needed = {
-            '__init_exchange__': [
-                ['maker_fee_rate', float],
-                ['taker_fee_rate', float]
-            ],
-            'get_products': [
-                ["currency_id", str],
-                ["base_currency", str],
-                ["quote_currency", str],
-                ["base_min_size", float],
-                ["base_max_size", float],
-                ["base_increment", float]
-            ],
-            'get_account': [
-                ["currency", str],
-                ["available", float],
-                ["hold", float]
-            ],
-            'market_order': [
-                ["product_id", str],
-                ["id", str],
-                ["created_at", float],
-                ["funds", float],
-                ["status", str],
-                ["type", str],
-                ["side", str]
-            ],
-            'limit_order': [
-                ["product_id", str],
-                ["id", str],
-                ["created_at", float],
-                ["price", float],
-                ["size", float],
-                ["status", str],
-                ["time_in_force", str],
-                ["type", str],
-                ["side", str]
-            ],
-            'stop_limit': [
-                ["product_id", str],
-                ["id", str],
-                ["created_at", float],
-                ["stop_price", float],
-                ["limit_price", float],
-                ["stop", str],
-                ["size", float],
-                ["status", str],
-                ["time_in_force", str],
-                ["type", str],
-                ["side", str]
-            ],
-            'cancel_order': [
-                ['order_id', str]
-            ],
-            # 'get_open_orders': [  # Key specificity changes based on order type
-            #     ["id", str],
-            #     ["price", float],
-            #     ["size", float],
-            #     ["type", str],
-            #     ["side", str],
-            #     ["status", str],
-            #     ["product_id", str]
-            # ],
-            # 'get_order': [
-            #     ["product_id", str],
-            #     ["id", str],
-            #     ["price", float],
-            #     ["size", float],
-            #     ["type", str],
-            #     ["side", str],
-            #     ["status", str],
-            #     ["funds", float]
-            # ],
-            'get_fees': [
-                ['maker_fee_rate', float],
-                ['taker_fee_rate', float]
-            ],
-            'get_market_limits': [
-                ["market", str],
-                ["base_currency", str],
-                ["quote_currency", str],
-                ["base_min_size", float],  # Minimum size to buy
-                ["base_max_size", float],  # Maximum size to buy
-                ["quote_increment", float],  # Specifies the min order price as well as the price increment.
-                ["base_increment", float],  # Specifies the minimum increment for the base_currency.
-                ["max_orders", int],
-                ["min_price", float],
-                ["max_price", float]
-            ]
-        }
-
-    @abc.abstractmethod
-    def init_exchange(self):
-        """
-        Create the properties for the exchange.
-        """
-        pass
-
-    """ Needs to be overridden here """
-
-    def get_calls(self):
-        """
-        Returns:
-             The exchange's direct calls object. A Blankly Bot class should have immediate access to this by
-             default
-        """
-        return self.calls
-
-    """ Needs to be overridden here """
-
-    def get_exchange_type(self):
-        return self.exchange_name
-
-    def get_account(self, currency=None):
-        """
-        Get all currencies in an account, or sort by currency/account_id
-        Args:
-            currency (Optional): Filter by particular currency
-
-        Coinbase Pro: get_account
-        Binance: get_account["balances"]
-        """
-
-        if currency is not None:
-            currency = utils.get_base_currency(currency)
-
-        return currency
-
-    def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
-        return utils.convert_epochs(epoch_start), utils.convert_epochs(epoch_stop)
-
-    def choose_order_specificity(self, order_type):
-        # This lower should not be necessary if everything is truly homogeneous
-        order_type = order_type.lower()
-        if order_type == 'market':
-            return self.needed['market_order']
-        elif order_type == 'limit':
-            return self.needed['limit_order']
-        else:
-            return self.needed['market_order']
diff --git a/Blankly/interface/currency_factory.py b/Blankly/interface/currency_factory.py
deleted file mode 100644
index 8ec18870..00000000
--- a/Blankly/interface/currency_factory.py
+++ /dev/null
@@ -1,35 +0,0 @@
-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 create_alpaca_client
-from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_Interface import CoinbaseProInterface
-from Blankly.exchanges.Binance.Binance_Interface import BinanceInterface
-import Blankly.utils.utils as utils
-
-class InterfaceFactory:
-    @staticmethod
-    def create_interface(exchange_name: str, auth, preferences_path: str = None):
-        preferences = utils.load_user_preferences(preferences_path)
-        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/")
-            else:
-                # Create the authenticated object
-                calls = Coinbase_Pro_API(auth[0], auth[1], auth[2])
-
-            return CoinbaseProInterface(exchange_name, calls)
-
-        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)
-            else:
-                calls = Client(api_key=auth[0], api_secret=auth[1],
-                                      tld=preferences["settings"]["binance_tld"])
-            return BinanceInterface(exchange_name, calls)
-
-        elif exchange_name == 'alpaca':
-            calls = create_alpaca_client(auth, preferences["settings"]["use_sandbox"])
-            return AlpacaInterface(calls, preferences_path)
\ No newline at end of file
diff --git a/Blankly/metrics/__init__.py b/Blankly/metrics/__init__.py
deleted file mode 100644
index 01a2a0de..00000000
--- a/Blankly/metrics/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-from Blankly.metrics.portfolio import * 
\ No newline at end of file
diff --git a/Blankly/strategy/__init__.py b/Blankly/strategy/__init__.py
deleted file mode 100644
index cff3959b..00000000
--- a/Blankly/strategy/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from Blankly.strategy.order import *
-from Blankly.strategy.strategy_base import *
-from Blankly.strategy.strategy_state import *
\ No newline at end of file
diff --git a/Blankly/strategy/strategy_base.py b/Blankly/strategy/strategy_base.py
deleted file mode 100644
index 8b9a2eaf..00000000
--- a/Blankly/strategy/strategy_base.py
+++ /dev/null
@@ -1,267 +0,0 @@
-"""
-    Abstraction for creating event driven user strategies
-    Copyright (C) 2021  Emerson Dove, Brandon Fan
-
-    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 <https://www.gnu.org/licenses/>.
-"""
-import warnings
-
-from Blankly.strategy.strategy_state import StrategyState
-from Blankly.utils.utils import AttributeDict
-import typing
-import time
-
-import pandas as pd
-import datetime
-import Blankly
-from Blankly.exchanges.Paper_Trade.backtest_controller import BackTestController
-from uuid import uuid4
-from Blankly.exchanges.exchange import Exchange
-from Blankly.utils.time_builder import time_interval_to_seconds
-
-
-class Strategy:
-    def __init__(self, exchange: Exchange, currency_pair='BTC-USD'):
-        self.__exchange = exchange
-        self.Ticker_Manager = Blankly.TickerManager(self.__exchange.get_type(), currency_pair)
-        self.Orderbook_Manager = Blankly.OrderbookManager(self.__exchange.get_type(), currency_pair)
-
-        self.__scheduling_pair = []  # Object to hold a currency and the resolution its pulled at: ["BTC-USD", 60]
-        self.Interface = exchange.get_interface()
-
-        # Create a cache for the current interface, and a wrapped paper trade object for user backtesting
-        self.__interface_cache = self.Interface
-        self.__paper_trade_exchange = Blankly.PaperTrade(self.__exchange)
-        self.__schedulers = []
-        self.__variables = {}
-
-    def add_price_event(self, callback: typing.Callable, currency_pair: str, resolution: str):
-        """
-        Add Orderbook Event
-        Args:
-            callback: The price event callback that will be added to the current ticker and run at the proper resolution
-            currency_pair: Currency pair to create the price event for
-            resolution: The resolution that the callback will be run - in seconds
-        """
-        resolution = time_interval_to_seconds(resolution)
-        
-        self.__scheduling_pair.append([currency_pair, resolution])
-        callback_id = str(uuid4())
-        self.__variables[callback_id] = AttributeDict({})
-        if resolution < 10:
-            # since it's less than 10 sec, we will just use the websocket feed - exchanges don't like fast calls
-            self.Ticker_Manager.create_ticker(self.__idle_event, currency_id=currency_pair)
-            self.__schedulers.append(
-                Blankly.Scheduler(self.__price_event_websocket, resolution,
-                                  initially_stopped=True,
-                                  callback=callback,
-                                  resolution=resolution,
-                                  variables=self.__variables[callback_id],
-                                  currency_pair=currency_pair)
-            )
-        else:
-            # Use the API
-            self.__schedulers.append(
-                Blankly.Scheduler(self.__price_event_rest, resolution,
-                                  initially_stopped=True,
-                                  callback=callback,
-                                  resolution=resolution,
-                                  variables=self.__variables[callback_id],
-                                  currency_pair=currency_pair)
-            )
-
-    def __idle_event(self):
-        """
-        Function to skip & ignore callbacks
-        """
-        pass
-
-    # def __process_orders(self, orders: typing.Any, currency_pair: str):
-    #     if orders is None:
-    #         return
-    #     is_list_of_orders = isinstance(orders, list) and not isinstance(orders[0], Order)
-    #     is_np_array_of_orders = isinstance(orders, np.array) and not isinstance(orders[0], Order)
-    #
-    #     if is_list_of_orders or is_np_array_of_orders or not isinstance(orders, Order):
-    #         raise ValueError("Expected an Order or a list of Orders but instead " + type(orders[0]))
-    #
-    #     if isinstance(orders, list) or isinstance(orders, np.array):
-    #         for order in orders:
-    #             self.__submit_order(order, currency_pair)
-    #     else:
-    #         self.__submit_order(orders, currency_pair)
-    #
-    # def __submit_order(self, order: Order, currency_pair: str):
-    #     if order.type == 'market':
-    #         self.Interface.market_order(currency_pair, order.side, order.amount)
-    #     if order.type == 'limit':
-    #         self.Interface.limit_order(currency_pair, order.side, order.price, order.amount)
-        
-    def __price_event_rest(self, **kwargs):
-        callback = kwargs['callback']
-        currency_pair = kwargs['currency_pair']
-        resolution = kwargs['resolution']
-        variables = kwargs['variables']
-        price = self.Interface.get_price(currency_pair)
-
-        state = StrategyState(self, self.Interface, variables, resolution)
-        orders = callback(price, currency_pair)  # self.Interface, state)
-        # self.__process_orders(orders, currency_pair)
-
-    def __price_event_websocket(self, **kwargs):
-        callback = kwargs['callback']
-        currency_pair = kwargs['currency_pair']
-        resolution = kwargs['resolution']
-        variables = kwargs['variables']
-
-        price = self.Ticker_Manager.get_most_recent_tick(override_currency=currency_pair)
-        state = StrategyState(self, self.Interface, variables, resolution)
-        orders = callback(price, currency_pair)  # self.Interface, state)
-        # self.__process_orders(orders, currency_pair)
-
-    def add_orderbook_event(self, callback: typing.Callable, currency_pair: str):
-        """
-        Add Orderbook Event
-        Args:
-            callback: The price event callback that will be added to the current ticker and run at the proper resolution
-            currency_pair: Currency pair to create the orderbook for
-        """
-        # since it's less than 10 sec, we will just use the websocket feed - exchanges don't like fast calls
-        self.Orderbook_Manager.create_orderbook(callback, currency_id=currency_pair)
-
-        # TODO the tickers need some type of argument passing & saving like scheduler so that the 1 second min isn't
-        #  required
-        # callback_id = str(uuid4())
-        # self.__variables[callback_id] = {}
-        # self.__schedulers.append(
-        #     Blankly.Scheduler(self.__orderbook_event_websocket, 1,
-        #                       initially_stopped=True,
-        #                       variables=self.__variables[callback_id],
-        #                       callback=callback, currency_pair=currency_pair)
-        # )
-
-    def start(self):
-        for i in self.__schedulers:
-            i.start()
-
-    def __orderbook_event_websocket(self, **kwargs):
-        callback = kwargs['callback']
-        currency_pair = kwargs['currency_pair']
-        variables = kwargs['variables']
-
-        book = self.Orderbook_Manager.get_most_recent_orderbook(override_currency_id=currency_pair)
-        state = StrategyState(self, self.Interface, variables)
-        orders = callback(book, currency_pair)  # self.Interface, state)
-        # is_arr_type = isinstance(orders, list) or isinstance(orders, np.array)
-        # if is_arr_type and isinstance(orders[0], Order):
-        #     raise ValueError("It is best that you directly use the interface for orderbook event orders")
-    
-    def backtest(self, 
-                 initial_values: dict = None,
-                 to: str = None,
-                 start_date: str = None,
-                 end_date: str = None,
-                 save: bool = False,
-                 settings_path: str = None,
-                 **kwargs
-                 ):
-        """
-        Turn this strategy into a backtest.
-
-        Args:
-            ** We expect either an initial_value (in USD) or a dictionary of initial values, we also expect
-            either `to` to be set or `start_date` and `end_date` **
-
-            initial_values (dict): Dictionary of initial value sizes (i.e { 'BTC': 3, 'USD': 5650}).
-                Using this will override the values that are currently on your exchange.
-            to (str): Declare an amount of time before now to backtest from: ex: '5y' or '10h'
-            start_date (str): Override argument "to" by specifying a start date such as "03/06/2018"
-            end_date (str): End the backtest at a date such as "03/06/2018"
-            save (bool): Save the price data references to the data required for the backtest as well as
-                overriden settings.
-            settings_path (str): Path to the backtest.json file.
-
-            Keyword Arguments:
-                **Use these to override parameters in the backtest.json file**
-
-                use_price: str = 'close',
-                    Set which price column to use.
-
-                smooth_prices: bool = False,
-                    Create linear connections between downloaded prices
-
-                GUI_output: bool = True,
-                    Enable/disable GUI webpage display after backtest
-
-                show_tickers_with_zero_delta: bool = False,
-                    Exclude tickers that have no change to account value in the GUI
-
-                save_initial_account_value: bool = True,
-                    Put an extra frame which contains the initial account values before any trade
-
-                show_progress_during_backtest: bool = True,
-                    Show a progress bar as the backtest runs
-
-                cache_location: str = './price_caches'
-                    Set a location for the price cache csv's to be written to
-        """
-
-        start = None
-        end = None
-
-        if to is not None:
-            start = time.time() - time_interval_to_seconds(to)
-            end = time.time()
-
-        if start_date is not None:
-            start_date = pd.to_datetime(start_date)
-            epoch = datetime.datetime.utcfromtimestamp(0)
-            start = (start_date - epoch).total_seconds()
-
-        if end_date is not None:
-            end_date = pd.to_datetime(end_date)
-            epoch = datetime.datetime.utcfromtimestamp(0)
-            end = (end_date - epoch).total_seconds()
-
-        self.Interface = self.__paper_trade_exchange.get_interface()
-        backtesting_controller = BackTestController(self.__paper_trade_exchange, backtest_settings_path=settings_path)
-
-        # Write any kwargs as settings to the settings - save if enabled.
-        for k, v in kwargs.items():
-            backtesting_controller.write_setting(k, v, save)
-
-        if initial_values is not None:
-            backtesting_controller.write_initial_price_values(initial_values)
-
-        # Write each scheduling pair as a price event - save if enabled.
-        if start is not None and end is not None:
-            for i in self.__scheduling_pair:
-                backtesting_controller.add_prices(i[0], start, end, i[1], save=save)
-        else:
-            warnings.warn("User-specified start and end time not given. Defaulting to using only cached data.")
-
-        # Append each of the events the class defines into the backtest
-        for i in self.__schedulers:
-            kwargs = i.get_kwargs()
-            backtesting_controller.append_backtest_price_event(callback=kwargs['callback'],
-                                                               asset_id=kwargs['currency_pair'],
-                                                               time_interval=i.get_interval()
-                                                               )
-
-        # Run the backtest & return results
-        results = backtesting_controller.run()
-
-        # Clean up
-        self.Interface = self.__interface_cache
-        return results
diff --git a/Blankly/strategy/strategy_usage_example.py b/Blankly/strategy/strategy_usage_example.py
deleted file mode 100644
index 6e519185..00000000
--- a/Blankly/strategy/strategy_usage_example.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from Blankly.strategy.order import Order
-import Blankly
-from Blankly.strategy.strategy_base import Strategy, StrategyState
-
-
-def golden_cross(price, ticker, interface: Blankly.Interface, state: StrategyState):
-    resolution: str = state.resolution # get the resolution that this price event is stored at
-    variables = state.variables # each price event has it's own local variable state
-
-    account = interface.account # get account holdings
-
-    historical_prices = Blankly.historical(ticker, 50, resolution=resolution)
-    sma50 = Blankly.analysis.calculate_sma(historical_prices, window=50)[-1] # last 50 day sma value is the value we want
-
-    if price > sma50 and variables['order_submitted']:
-        variables['order_submitted'] = True
-        interface.market_order(ticker, 'buy', account.cash * 0.25)
-    elif price < sma50: # only sell if there's an open position
-        variables['order_submitted'] = False
-        stocks_available = account.holdings[ticker].available * 0.5 # sell 50% of available sures
-        interface.market_order(ticker, 'sell', stocks_available)
-
-      
-def rsi(price, ticker, interface: Blankly.Interface, state: StrategyState):
-    resolution: str = state.resolution # get the resolution that this price event is stored at
-    variables = state.variables # each price event has it's own local variable state
-
-    account = interface.account # get account holdings
-    
-    historical_prices = Blankly.historical(ticker, 50, resolution=resolution)
-    rsi = Blankly.analysis.calculate_rsi(historical_prices)[-1] # last 50 day sma value is the value we want
-
-    if rsi > 70 and variables['order_submitted']:
-        variables['order_submitted'] = True
-        interface.market_order(ticker, 'buy', account.cash * 0.25)
-    elif rsi < 30: # only sell if there's an open position
-        variables['order_submitted'] = False
-        stocks_available = account.holdings[ticker].available * 0.5 # sell 50% of available sures
-        interface.market_order(ticker, 'sell', stocks_available)
-
-
-if __name__ == "__main__":
-    coinbase_pro = Blankly.Coinbase_Pro()
-    alpaca = Blankly.Alpaca()
-
-    coinbase_strategy = Strategy(coinbase_pro)
-    alpaca_strategy = Strategy(alpaca)
-
-    coinbase_strategy.add_price_event(golden_cross, currency_pair='BTC-USD', resolution='15m')
-    coinbase_strategy.add_price_event(rsi, currency_pair='XLM-USD', resolution='15m')
-
-    alpaca_strategy.add_price_event(golden_cross, currency_pair='MSFT', resolution='15m')
-    alpaca_strategy.add_price_event(rsi, currency_pair='AAPL', resolution='15m')
-    
diff --git a/Blankly/utils/ci_utils/Settings.json b/Blankly/utils/ci_utils/Settings.json
deleted file mode 100644
index b6c9547d..00000000
--- a/Blankly/utils/ci_utils/Settings.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
-  "settings": {
-    "account_update_time": 5000,
-    "use_sandbox": true,
-    "binance_tld": "us",
-    "websocket_buffer_size": 10000
-  }
-}
\ No newline at end of file
diff --git a/Blankly/utils/ci_utils/requirements.txt b/Blankly/utils/ci_utils/requirements.txt
deleted file mode 100644
index d4c10617..00000000
--- a/Blankly/utils/ci_utils/requirements.txt
+++ /dev/null
@@ -1,8 +0,0 @@
-numpy
-sklearn
-scikit-learn
-zerorpc
-requests
-websocket-client
-pandas
-python-binance
\ No newline at end of file
diff --git a/Blankly/utils/paper_trading/local_account/__init__.py b/Blankly/utils/paper_trading/local_account/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/Blankly/utils/purchases/__init__.py b/Blankly/utils/purchases/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/Examples/settings.json b/Examples/settings.json
deleted file mode 100644
index cb6cc007..00000000
--- a/Examples/settings.json
+++ /dev/null
@@ -1,9 +0,0 @@
-{
-  "settings": {
-    "account_update_time": 5000,
-    "use_sandbox": true,
-    "use_sandbox_websockets": false,
-    "binance_tld": "us",
-    "websocket_buffer_size": 10000
-  }
-}
\ No newline at end of file
diff --git a/README.md b/README.md
index 5edf311f..d16a299c 100644
--- a/README.md
+++ b/README.md
@@ -1,148 +1,161 @@
-![Blankly_Github_Logo](https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/blankly-white.svg?alt=media&token=d5d57c15-4c55-422f-9b06-2e578288fa14)
-[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=Blankly-Finance_Blankly&metric=sqale_rating)](https://sonarcloud.io/dashboard?id=Blankly-Finance_Blankly)
-[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=Blankly-Finance_Blankly&metric=security_rating)](https://sonarcloud.io/dashboard?id=Blankly-Finance_Blankly)
-[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=Blankly-Finance_Blankly&metric=alert_status)](https://sonarcloud.io/dashboard?id=Blankly-Finance_Blankly)
-[![Downloads](https://pepy.tech/badge/blankly/month)](https://pepy.tech/project/blankly)
-[![Docs Badge](https://img.shields.io/badge/Check%20Our%20-DOCS-<COLOR>.svg?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpzZXJpZj0iaHR0cDovL3d3dy5zZXJpZi5jb20vIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgNDEzNCA0MTM0IiB2ZXJzaW9uPSIxLjEiIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MjsiPgogICAgPHBhdGggZD0iTTQxMzMuODYsNzE0Ljk3OUM0MTMzLjg2LDMyMC4zNzEgMzgxMy40OSwwIDM0MTguODgsMEw3MTQuOTc5LDBDMzIwLjM3MSwwIDAsMzIwLjM3MSAwLDcxNC45NzlMMCwzNDE4Ljg4QzAsMzgxMy40OSAzMjAuMzcxLDQxMzMuODYgNzE0Ljk3OSw0MTMzLjg2TDM0MTguODgsNDEzMy44NkMzODEzLjQ5LDQxMzMuODYgNDEzMy44NiwzODEzLjQ5IDQxMzMuODYsMzQxOC44OEw0MTMzLjg2LDcxNC45NzlaIiBzdHlsZT0iZmlsbDp3aGl0ZTsiLz4KICAgIDxnIHRyYW5zZm9ybT0ibWF0cml4KDIwMy42NDYsMCwwLDIwMy42NDYsLTM2MDU2LC0yNTAzLjQzKSI+CiAgICAgICAgPHBhdGggZD0iTTE4OC40MzQsMTQuOTk0TDE4NS45NywxNC45OTRMMTc5LjQ2MiwyOS44OTJMMTgxLjE1NiwyOS44OTJDMTgxLjM0NCwyOS44OTIgMTgxLjUyOSwyOS44NjQgMTgxLjcxMiwyOS44MDhDMTgxLjg5NiwyOS43NTMgMTgyLjA2OCwyOS42NzUgMTgyLjIyOCwyOS41NzVDMTgyLjM4NywyOS40NzQgMTgyLjUyOSwyOS4zNTQgMTgyLjY1NCwyOS4yMTZDMTgyLjc3OSwyOS4wNzcgMTgyLjg3NSwyOC45MjUgMTgyLjk0NSwyOC43NThMMTgzLjgxOCwyNi43NDJMMTgzLjgyLDI2Ljc0MkwxODUuMzk1LDIzLjExNUwxODUuNDA5LDIzLjExNUwxODcuMjAyLDE4Ljk1M0wxODkuMzgsMjQuMDA3TDE4OC4xNDMsMjQuMDA3QzE4Ny4yMTcsMjQuMDA3IDE4Ni4zNzksMjQuNTU3IDE4Ni4wMTEsMjUuNDA2TDE4NS40MzEsMjYuNzQyTDE5MC41ODYsMjYuNzQyTDE5MS40NywyOC43NThDMTkxLjYyOSwyOS4xMTIgMTkxLjg2OCwyOS4zODkgMTkyLjE4NywyOS41OUMxOTIuNTA2LDI5Ljc5MSAxOTIuODU5LDI5Ljg5MiAxOTMuMjQ3LDI5Ljg5MkwxOTQuOTQyLDI5Ljg5MkwxODguNDM0LDE0Ljk5NFoiIHN0eWxlPSJmaWxsLXJ1bGU6bm9uemVybzsiLz4KICAgIDwvZz4KPC9zdmc+)](https://docs.blankly.finance)
-[![Docs Badge](https://img.shields.io/badge/Brandon%20Made%20The-Website-<COLOR>.svg?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpzZXJpZj0iaHR0cDovL3d3dy5zZXJpZi5jb20vIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgNDEzNCA0MTM0IiB2ZXJzaW9uPSIxLjEiIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MjsiPgogICAgPHBhdGggZD0iTTQxMzMuODYsNzE0Ljk3OUM0MTMzLjg2LDMyMC4zNzEgMzgxMy40OSwwIDM0MTguODgsMEw3MTQuOTc5LDBDMzIwLjM3MSwwIDAsMzIwLjM3MSAwLDcxNC45NzlMMCwzNDE4Ljg4QzAsMzgxMy40OSAzMjAuMzcxLDQxMzMuODYgNzE0Ljk3OSw0MTMzLjg2TDM0MTguODgsNDEzMy44NkMzODEzLjQ5LDQxMzMuODYgNDEzMy44NiwzODEzLjQ5IDQxMzMuODYsMzQxOC44OEw0MTMzLjg2LDcxNC45NzlaIiBzdHlsZT0iZmlsbDp3aGl0ZTsiLz4KICAgIDxnIHRyYW5zZm9ybT0ibWF0cml4KDIwMy42NDYsMCwwLDIwMy42NDYsLTM2MDU2LC0yNTAzLjQzKSI+CiAgICAgICAgPHBhdGggZD0iTTE4OC40MzQsMTQuOTk0TDE4NS45NywxNC45OTRMMTc5LjQ2MiwyOS44OTJMMTgxLjE1NiwyOS44OTJDMTgxLjM0NCwyOS44OTIgMTgxLjUyOSwyOS44NjQgMTgxLjcxMiwyOS44MDhDMTgxLjg5NiwyOS43NTMgMTgyLjA2OCwyOS42NzUgMTgyLjIyOCwyOS41NzVDMTgyLjM4NywyOS40NzQgMTgyLjUyOSwyOS4zNTQgMTgyLjY1NCwyOS4yMTZDMTgyLjc3OSwyOS4wNzcgMTgyLjg3NSwyOC45MjUgMTgyLjk0NSwyOC43NThMMTgzLjgxOCwyNi43NDJMMTgzLjgyLDI2Ljc0MkwxODUuMzk1LDIzLjExNUwxODUuNDA5LDIzLjExNUwxODcuMjAyLDE4Ljk1M0wxODkuMzgsMjQuMDA3TDE4OC4xNDMsMjQuMDA3QzE4Ny4yMTcsMjQuMDA3IDE4Ni4zNzksMjQuNTU3IDE4Ni4wMTEsMjUuNDA2TDE4NS40MzEsMjYuNzQyTDE5MC41ODYsMjYuNzQyTDE5MS40NywyOC43NThDMTkxLjYyOSwyOS4xMTIgMTkxLjg2OCwyOS4zODkgMTkyLjE4NywyOS41OUMxOTIuNTA2LDI5Ljc5MSAxOTIuODU5LDI5Ljg5MiAxOTMuMjQ3LDI5Ljg5MkwxOTQuOTQyLDI5Ljg5MkwxODguNDM0LDE0Ljk5NFoiIHN0eWxlPSJmaWxsLXJ1bGU6bm9uemVybzsiLz4KICAgIDwvZz4KPC9zdmc+)](https://blankly.finance)
+<br />
+<div style="text-align: center; margin-top: 25px" align="center">
+   <img style="margin: 0 auto; padding-bottom: 15px; padding-top: 30px" width=70%" src="https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/blankly-github-logo.png?alt=media&token=8f436cd2-3d28-432c-867a-afef780f4260">
+</div>
+<br />
+<div align="center">
+  <b>💨  Rapidly build and deploy quantitative models for stocks and crypto  🚀</b>
+</div>
+<br />
 
+<p align="center">
+   <a target="_blank" href="https://sonarcloud.io/dashboard?id=Blankly-Finance_Blankly"><img src="https://sonarcloud.io/api/project_badges/measure?project=Blankly-Finance_Blankly&metric=sqale_rating"></a>
+   <a target="_blank" href="https://sonarcloud.io/dashboard?id=Blankly-Finance_Blankly"><img src="https://sonarcloud.io/api/project_badges/measure?project=Blankly-Finance_Blankly&metric=security_rating"></a>
+   <a target="_blank" href="https://sonarcloud.io/dashboard?id=Blankly-Finance_Blankly"><img src="https://sonarcloud.io/api/project_badges/measure?project=Blankly-Finance_Blankly&metric=alert_status"></a>
+   <a target="_blank" href="https://github.com/Blankly-Finance/Blankly/actions/workflows/test.yml"><img src="https://github.com/Blankly-Finance/Blankly/actions/workflows/test.yml/badge.svg?branch=main"></a> <br>
+   <a target="_blank" href="https://pepy.tech/project/blankly"><img src="https://pepy.tech/badge/blankly/month"></a>
 
-## What is it?
+</p>
 
-​	Blankly is an elegant python library for interacting with many crypto exchanges on multiple portfolios in a consistent way. Blankly offers a powerful feature-set, optimized for speed and ease of use.
+<p align="center">
+</p>
 
-Try it out: `pip install blankly`
+<p align="center">
+   <a target="_blank" href="https://pepy.tech/project/blankly"><img src="https://forthebadge.com/images/badges/open-source.svg"></a>
+   <a target="_blank" href="https://pepy.tech/project/blankly"><img src="https://forthebadge.com/images/badges/for-you.svg"></a>
+   <a target="_blank" href="https://pepy.tech/project/blankly"><img src="https://forthebadge.com/images/badges/gluten-free.svg"></a>
+   <a target="_blank" href="https://pepy.tech/project/blankly"><img src="https://forthebadge.com/images/badges/check-it-out.svg"></a>
+   <br><a target="_blank" href="https://docs.blankly.finance"><img src="https://img.shields.io/badge/Check%20our%20-docs-<COLOR>.svg?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpzZXJpZj0iaHR0cDovL3d3dy5zZXJpZi5jb20vIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgNDEzNCA0MTM0IiB2ZXJzaW9uPSIxLjEiIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MjsiPgogICAgPHBhdGggZD0iTTQxMzMuODYsNzE0Ljk3OUM0MTMzLjg2LDMyMC4zNzEgMzgxMy40OSwwIDM0MTguODgsMEw3MTQuOTc5LDBDMzIwLjM3MSwwIDAsMzIwLjM3MSAwLDcxNC45NzlMMCwzNDE4Ljg4QzAsMzgxMy40OSAzMjAuMzcxLDQxMzMuODYgNzE0Ljk3OSw0MTMzLjg2TDM0MTguODgsNDEzMy44NkMzODEzLjQ5LDQxMzMuODYgNDEzMy44NiwzODEzLjQ5IDQxMzMuODYsMzQxOC44OEw0MTMzLjg2LDcxNC45NzlaIiBzdHlsZT0iZmlsbDp3aGl0ZTsiLz4KICAgIDxnIHRyYW5zZm9ybT0ibWF0cml4KDIwMy42NDYsMCwwLDIwMy42NDYsLTM2MDU2LC0yNTAzLjQzKSI+CiAgICAgICAgPHBhdGggZD0iTTE4OC40MzQsMTQuOTk0TDE4NS45NywxNC45OTRMMTc5LjQ2MiwyOS44OTJMMTgxLjE1NiwyOS44OTJDMTgxLjM0NCwyOS44OTIgMTgxLjUyOSwyOS44NjQgMTgxLjcxMiwyOS44MDhDMTgxLjg5NiwyOS43NTMgMTgyLjA2OCwyOS42NzUgMTgyLjIyOCwyOS41NzVDMTgyLjM4NywyOS40NzQgMTgyLjUyOSwyOS4zNTQgMTgyLjY1NCwyOS4yMTZDMTgyLjc3OSwyOS4wNzcgMTgyLjg3NSwyOC45MjUgMTgyLjk0NSwyOC43NThMMTgzLjgxOCwyNi43NDJMMTgzLjgyLDI2Ljc0MkwxODUuMzk1LDIzLjExNUwxODUuNDA5LDIzLjExNUwxODcuMjAyLDE4Ljk1M0wxODkuMzgsMjQuMDA3TDE4OC4xNDMsMjQuMDA3QzE4Ny4yMTcsMjQuMDA3IDE4Ni4zNzksMjQuNTU3IDE4Ni4wMTEsMjUuNDA2TDE4NS40MzEsMjYuNzQyTDE5MC41ODYsMjYuNzQyTDE5MS40NywyOC43NThDMTkxLjYyOSwyOS4xMTIgMTkxLjg2OCwyOS4zODkgMTkyLjE4NywyOS41OUMxOTIuNTA2LDI5Ljc5MSAxOTIuODU5LDI5Ljg5MiAxOTMuMjQ3LDI5Ljg5MkwxOTQuOTQyLDI5Ljg5MkwxODguNDM0LDE0Ljk5NFoiIHN0eWxlPSJmaWxsLXJ1bGU6bm9uemVybzsiLz4KICAgIDwvZz4KPC9zdmc+"></a>
+   <a target="_blank" href="https://blankly.finance"><img src="https://img.shields.io/badge/Brandon%20made%20the-website-<COLOR>.svg?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4bWxuczpzZXJpZj0iaHR0cDovL3d3dy5zZXJpZi5jb20vIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiB2aWV3Qm94PSIwIDAgNDEzNCA0MTM0IiB2ZXJzaW9uPSIxLjEiIHhtbDpzcGFjZT0icHJlc2VydmUiIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MjsiPgogICAgPHBhdGggZD0iTTQxMzMuODYsNzE0Ljk3OUM0MTMzLjg2LDMyMC4zNzEgMzgxMy40OSwwIDM0MTguODgsMEw3MTQuOTc5LDBDMzIwLjM3MSwwIDAsMzIwLjM3MSAwLDcxNC45NzlMMCwzNDE4Ljg4QzAsMzgxMy40OSAzMjAuMzcxLDQxMzMuODYgNzE0Ljk3OSw0MTMzLjg2TDM0MTguODgsNDEzMy44NkMzODEzLjQ5LDQxMzMuODYgNDEzMy44NiwzODEzLjQ5IDQxMzMuODYsMzQxOC44OEw0MTMzLjg2LDcxNC45NzlaIiBzdHlsZT0iZmlsbDp3aGl0ZTsiLz4KICAgIDxnIHRyYW5zZm9ybT0ibWF0cml4KDIwMy42NDYsMCwwLDIwMy42NDYsLTM2MDU2LC0yNTAzLjQzKSI+CiAgICAgICAgPHBhdGggZD0iTTE4OC40MzQsMTQuOTk0TDE4NS45NywxNC45OTRMMTc5LjQ2MiwyOS44OTJMMTgxLjE1NiwyOS44OTJDMTgxLjM0NCwyOS44OTIgMTgxLjUyOSwyOS44NjQgMTgxLjcxMiwyOS44MDhDMTgxLjg5NiwyOS43NTMgMTgyLjA2OCwyOS42NzUgMTgyLjIyOCwyOS41NzVDMTgyLjM4NywyOS40NzQgMTgyLjUyOSwyOS4zNTQgMTgyLjY1NCwyOS4yMTZDMTgyLjc3OSwyOS4wNzcgMTgyLjg3NSwyOC45MjUgMTgyLjk0NSwyOC43NThMMTgzLjgxOCwyNi43NDJMMTgzLjgyLDI2Ljc0MkwxODUuMzk1LDIzLjExNUwxODUuNDA5LDIzLjExNUwxODcuMjAyLDE4Ljk1M0wxODkuMzgsMjQuMDA3TDE4OC4xNDMsMjQuMDA3QzE4Ny4yMTcsMjQuMDA3IDE4Ni4zNzksMjQuNTU3IDE4Ni4wMTEsMjUuNDA2TDE4NS40MzEsMjYuNzQyTDE5MC41ODYsMjYuNzQyTDE5MS40NywyOC43NThDMTkxLjYyOSwyOS4xMTIgMTkxLjg2OCwyOS4zODkgMTkyLjE4NywyOS41OUMxOTIuNTA2LDI5Ljc5MSAxOTIuODU5LDI5Ljg5MiAxOTMuMjQ3LDI5Ljg5MkwxOTQuOTQyLDI5Ljg5MkwxODguNDM0LDE0Ljk5NFoiIHN0eWxlPSJmaWxsLXJ1bGU6bm9uemVybzsiLz4KICAgIDwvZz4KPC9zdmc+"></a>
+</p>
 
+---
 
-Check out our [website](http://blankly.finance).
+Blankly is an elegant python library for interacting with many crypto and stock exchanges for automated portfolios in a consistent way. Blankly offers a powerful feature-set, optimized for speed and ease of use. 
 
-## Features
+We're bridging the gap between local development systems & live APIs by building a framework which allows backtesting, 
+paper trading, sandbox testing, and live cross-exchange deployment without modifying a single line of trading logic.
 
-- Full REST API support for non-margin accounts on listed exchanges
+Check out our [website](https://blankly.finance) and our [docs](https://docs.blankly.finance).
 
-- Ticker websocket support
+## Getting Started
 
-- Order book websocket support
+### Installation
 
-- Fully multiprocessed bots with flexible arguments.
+First install Blankly using `pip`. Blankly is hosted on [PyPi](https://pypi.org/project/Blankly/).
 
-- Quickstart access for interacting with exchanges
+```bash
+$ pip install blankly
+```
 
-- Support for multiple portfolios on multiple exchanges, all independently
+Make sure you're using a supported version of python. The module is currently tested on these versions:
 
-- Multi-process communication
+- Python 3.7+
 
-- Long term and high resolution historical data downloads as pandas dataframes
 
-- Single pip module (`pip install Blankly`)
+### Directory Setup and API Keys
 
-- Asynchronous callbacks from ticker feeds
+Next, add [`settings.json`](https://github.com/Blankly-Finance/Blankly/blob/development/Examples/settings.json) and [`keys.json`](https://github.com/Blankly-Finance/Blankly/blob/development/Examples/keys_example.json) to your root project directory. More information can be found on our [docs](https://docs.blankly.finance)
 
-- ZeroRPC server to report to Javascript or React **
+From there, **insert your API keys** from your exchange into your `keys.json` file.
 
-- Easy access to raw API calls
+The working directory format should look similar to this:
 
-- Customizable circular buffer websocket feeds
+```
+Project
+   |-script.py
+   |-keys.json
+   |-settings.json
+```
 
-- Support for coinbase pro & binance sandbox modes
+### Additional Info
 
-- Instant paper trade wrapper for supported exchanges
+For more info, and ways to do more advanced things, check out our [getting started docs](https://docs.blankly.finance).
+## Example Use
 
-- Run scheduled functions natively
+We have a pre-built cookbook examples that implement strategies such as RSI, MACD, and the Golden Cross found in our [examples](https://docs.blankly.finance/examples).
 
-- Logs for websocket feeds
+We have made Blankly extremely easy to integrate with any existing models and price events to make it super easy to switch.
 
-- Status management for purchases
+```python
+import blankly
+from blankly.strategy import Strategy, StrategyState
+from model import my_awesome_model
 
-- Create strategies using an event-based framework
 
-- Instantly backtest your strategies (`strategy.backtest(to='1y')`)
+def price_event(price: float, ticker: str, state: StrategyState):
+    interface = state.interface
 
-- Automatic price downloads & caching for use in backtests
+    # Add this most recent price to be stored for this particular price event
+    history = state.variables['history']
+    history.append(price)
 
-- Interface that allows calls to each supported exchange to be identical:
+    # easily integrate your model
+    decision = my_awesome_model(history)
 
-  > Buy example for Coinbase Pro and Binance:
-  >
-  > Coinbase Pro:
-  >
-  > ```python
-  > self.Interface.market_order(.01, "buy", "BTC-USD")
-  > ```
-  >
-  > Binance:
-  >
-  > ```python
-  > self.Interface.market_order(.01, "buy", "BTC-USD")
-  > ```
+    # buy or sell based on that decision
+    if decision:
+        buy_order = int(.025 * interface.cash)
+        if buy_order > 10:
+            interface.market_order(ticker, 'buy', int(.025 * interface.cash))
+            state.variables['has_buy_order'] = True
+    elif state.variables['has_buy_order'] and not decision:
+        amt = interface.account[ticker]['available']
+        interface.market_order(ticker, 'sell', int(amt))
+        state.variables['has_buy_order'] = False
 
-** Means that the feature is still in development but has some degree of support.
 
-## Coming Features
+# Easily run setup code
+def strategy_init(currency_pair, state: StrategyState):
+    state.variables['history'] = state.interface.history(symbol=currency_pair,
+                                                         to='4w',
+                                                         resolution='1h')['close'].tolist()
 
-- Deployment (`> Blankly push bot/*`)
-- Online hosting
 
-# Quick Start
+if __name__ == "__main__":
+    # All authentication is done in this line
+    exchange = blankly.CoinbasePro()
 
-1. Using Blankly is incredibly easy. It just requires the pip module and 3 basic files. First **install the pip module** by
+    # Now just wrap it into a strategy to gain a huge amount of functionality
+    strategy = Strategy(exchange)
 
-> `pip install Blankly`
+    # Run the code above with a new price once a day
+    strategy.add_price_event(price_event,
+                             symbol='BTC-USD',
+                             resolution='1h',
+                             # Pass an init function to run before any price events
+                             init=strategy_init)
 
-2. Next **you need the files** in the `Examples` folder:
+    # Run the code above with a new price once every thirty minutes
+    strategy.add_price_event(price_event,
+                             symbol='LINK-USD',
+                             resolution='1h',
+                             init=strategy_init)
 
-> basic_example.py
->
-> keys_example.json
->
-> settings.json
+    strategy.start()
 
-3. **Place** these in the `root` or `working directory` of the project.
+```
 
-4. **Rename** `key_example.json` to `keys.json` or create your own `.json` that has the same structure (don't commit this to VSC).
-
-5. **Insert the API keys** from your exchange into the renamed `keys.json` file.
-
-   1. **You can add multiple portfolios**! You can specify the name of the portfolio to load when you construct the exchange.
-   2. Example: `Blankly.Coinbase_Pro(portfolio_name="my cool portfolio")`). 
-   3. If you don't provide one to the constructor, it will just default to the first one given in the `keys.json` file and show a warning.
-
-6. **The script defaults to Coinbase Pro**. If you're using that, great! If not, change the line that says:
-
-   > `exchange = Blankly.Coinbase_Pro()`
-   >
-   > to one that matches your exchange, such as:
-   >
-   > `exchange = Blankly.Binance()`
-
-7. **Everything should work**! Run the `basic_bot` example in `basic_bot.py`. Note a warning will be shown because the `basic_bot` script does not specify the exchange name by default (explained in step 5 above).
-    
-    - Note the library is developed on Python 3.7, but most modern versions of python 3 should work.
-
-For more info, and ways to do more advanced things, check our *docs*.
-
-## Exchanges Status
+## Supported Exchanges
 
 | Exchange     | REST Support | Ticker Websocket | Order Book | Interface |
 | ------------ | ------------ | ---------------- | ---------- | --------- |
 | Coinbase Pro | 🟢           | 🟢               | 🟢        | 🟢        |
-| Binance      | 🟢           | 🟢               | 🟢        | 🟡        |
-| Alpaca       | 🟡           | 🔴               | 🔴        | 🟡        |
+| Binance      | 🟢           | 🟢               | 🟢        | 🟢       |
+| Alpaca       | 🟢         | 🟡             | 🟡      | 🟢        |
+| OANDA | 🔴 | 🔴 | 🔴 | 🔴 |
 
-🟢 = working
+🟢  = working
 
-🟡 = in development, some or most features are working
+🟡  = in development, some or most features are working
 
-🔴 = planned but not yet in development
+🔴  = planned but not yet in development
 
 * Interface calls take ~300 µs extra to homogenize the exchange data.
 
-## Docs
-https://docs.blankly.finance
-
 ## Bugs
 
-Please report any bugs or issues in Github's Issues page.
+Please report any bugs or issues on the Github's Issues page.
 
 ## Disclaimer 
 
@@ -154,4 +167,8 @@ This is free software.
 If you would like to support the project, pull requests are welcome.
 You can also contribute just by telling us what you think of Blankly: https://forms.gle/4oAjG9MKRTYKX2hP9
 
+## Licensing 
+
+**Blankly** is distributed under the [**LGPL License**](https://www.gnu.org/licenses/lgpl-3.0.en.html). See the [LICENSE](/LICENSE) for more details.
+
 New updates every day 💪.
diff --git a/blankly/PENDING.md b/blankly/PENDING.md
new file mode 100644
index 00000000..b328cfdc
--- /dev/null
+++ b/blankly/PENDING.md
@@ -0,0 +1,11 @@
+## Pending documentation changes
+
+- Alpaca `get_clock` in the exchange class
+- Changes in the homogeneity of the websocket trade feeds
+- Alpaca websocket streaming setting in `settings.json`
+- Orderbooks are going to become a list of tuples
+- Alpaca also has a weird orderbook
+- How to generate alpaca paper keys
+- Go over features page again because got so many features
+- curr_diff is not defined in the MACD example page code, don't exactly know how to fix
+- base and quote asset in StrategyState
\ No newline at end of file
diff --git a/blankly/__init__.py b/blankly/__init__.py
new file mode 100644
index 00000000..98445a77
--- /dev/null
+++ b/blankly/__init__.py
@@ -0,0 +1,35 @@
+"""
+    __init__ file to give the module access to the libraries.
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro import CoinbasePro
+from blankly.exchanges.interfaces.binance.binance import Binance
+from blankly.exchanges.interfaces.alpaca.alpaca import Alpaca
+from blankly.exchanges.interfaces.paper_trade.paper_trade import PaperTrade
+from blankly.strategy import Strategy as Strategy
+from blankly.strategy import StrategyState as StrategyState
+
+from blankly.exchanges.managers.ticker_manager import TickerManager
+from blankly.exchanges.managers.orderbook_manager import OrderbookManager
+from blankly.exchanges.managers.general_stream_manager import GeneralManager
+from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface as Interface
+from blankly.strategy.blankly_bot import BlanklyBot
+from blankly.utils.utils import trunc
+import blankly.utils.utils as utils
+from blankly.utils.scheduler import Scheduler
+import blankly.indicators as indicators
+from blankly.utils import time_builder
diff --git a/Blankly/auth/Alpaca/__init__.py b/blankly/deployment/__init__.py
similarity index 100%
rename from Blankly/auth/Alpaca/__init__.py
rename to blankly/deployment/__init__.py
diff --git a/Blankly/Constants.py b/blankly/deployment/cli.py
similarity index 56%
rename from Blankly/Constants.py
rename to blankly/deployment/cli.py
index 525bc31d..5792f388 100644
--- a/Blankly/Constants.py
+++ b/blankly/deployment/cli.py
@@ -1,5 +1,5 @@
 """
-    Definitions for constants used in the software.
+    CLI for interacting with and uploading blankly models.
     Copyright (C) 2021  Emerson Dove
 
     This program is free software: you can redistribute it and/or modify
@@ -16,22 +16,6 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-# TODO allow these to be dynamically changed and updated
-""" BTC """
-MINIMUM_BUY_SELL = .001
-CURRENT_HIGHEST_USD = 42000
 
-""" Coinbase """
-# This is currently set to the actual fee rate, sometimes .0002 (.02%) is added as padding.
-PRETEND_FEE_RATE = .005
-
-# Actual fee rates
-TAKER_FEE_RATE = .005
-MAKER_FEE_RATE = .005
-
-""" Margins """
-# This is used with the fee rate. This allows a guaranteed profit if it reaches past fees and this extra rate
-SELL_MIN = .002
-
-""" Settings """
-EMERGENCY_SELL_SAMPLE = 10
+def main():
+    print("blankly deployment is coming soon!")
diff --git a/Blankly/Server.py b/blankly/deployment/server.py
similarity index 86%
rename from Blankly/Server.py
rename to blankly/deployment/server.py
index f3a108d4..aa44ef2f 100644
--- a/Blankly/Server.py
+++ b/blankly/deployment/server.py
@@ -18,11 +18,13 @@
 
 from __future__ import print_function
 
-import Blankly
-from calc import calc as real_calc
 import sys
+
 import zerorpc
 
+import blankly
+from blankly.utils.calc import calc as real_calc
+
 
 # Tickers shouldn't be accessed from this class. Interfaces will not have access to them
 class TradeInterface:
@@ -47,17 +49,18 @@ def init(self, auth_path, preferences_path):
 
     def create_portfolio(self, exchange_type, portfolio_name):
         if exchange_type == "coinbase_pro":
-            self.__exchanges.append(Blankly.Coinbase_Pro(portfolio_name=portfolio_name,
-                                                         auth_path=self.__auth_path,
-                                                         preferences_path=self.__preferences_path))
+            self.__exchanges.append(blankly.CoinbasePro(portfolio_name=portfolio_name,
+                                                        keys_path=self.__auth_path,
+                                                        settings_path=self.__preferences_path))
         elif exchange_type == "binance":
-            self.__exchanges.append(Blankly.Binance(portfolio_name=portfolio_name,
-                                                    auth_path=self.__auth_path,
-                                                    preferences_path=self.__preferences_path))
+            self.__exchanges.append(blankly.Binance(portfolio_name=portfolio_name,
+                                                    keys_path=self.__auth_path,
+                                                    settings_path=self.__preferences_path))
         else:
             raise ValueError("Exchange not found or unsupported")
 
     """ External State """
+
     def get_exchange_state(self, name):
         for i in range(len(self.__exchanges)):
             if self.__exchanges[i].get_name() == name:
@@ -67,6 +70,7 @@ def get_exchange_state(self, name):
     Internal State, this has all the currencies. This is mainly used for an initial definition of which currencies
     are being used, get model state is what will matter for the reporting into these blocks
     """
+
     def get_portfolio_state(self, name):
         for i in range(len(self.__exchanges)):
             if self.__exchanges[i].get_name() == name:
@@ -85,6 +89,7 @@ def assign_model(self, portfolio_name, currency_pair, model_name, args=None):
     """
     Three indicators at the top of the GUI - JSON with only 3 keys
     """
+
     def update_indicators(self, name):
         for i in range(len(self.__exchanges)):
             if self.__exchanges[i].get_name() == name:
diff --git a/Blankly/auth/Binance/__init__.py b/blankly/exchanges/__init__.py
similarity index 100%
rename from Blankly/auth/Binance/__init__.py
rename to blankly/exchanges/__init__.py
diff --git a/Blankly/exchanges/IExchange.py b/blankly/exchanges/abc_exchange.py
similarity index 87%
rename from Blankly/exchanges/IExchange.py
rename to blankly/exchanges/abc_exchange.py
index 63dc0421..a6fb78d6 100644
--- a/Blankly/exchanges/IExchange.py
+++ b/blankly/exchanges/abc_exchange.py
@@ -19,7 +19,7 @@
 import abc
 
 
-class IExchange(abc.ABC):
+class ABCExchange(abc.ABC):
     """
     Functions required for easy interaction with the GUI/main
     """
@@ -29,15 +29,15 @@ def append_model(self, model, coin, args):
         pass
 
     @abc.abstractmethod
-    def get_full_state(self, currency):
+    def get_full_state(self, symbol):
         pass
 
     @abc.abstractmethod
-    def get_model_state(self, currency):
+    def get_model_state(self, symbol):
         pass
 
     @abc.abstractmethod
-    def get_currency_state(self, currency):
+    def get_asset_state(self, symbol):
         pass
 
     def get_exchange_state(self):
diff --git a/Blankly/exchanges/IExchange_Websocket.py b/blankly/exchanges/abc_exchange_websocket.py
similarity index 97%
rename from Blankly/exchanges/IExchange_Websocket.py
rename to blankly/exchanges/abc_exchange_websocket.py
index 90194894..f4452e3c 100644
--- a/Blankly/exchanges/IExchange_Websocket.py
+++ b/blankly/exchanges/abc_exchange_websocket.py
@@ -19,10 +19,11 @@
 import abc
 
 
-class IExchangeWebsocket(abc.ABC):
+class ABCExchangeWebsocket(abc.ABC):
     """
     Each of these functions are required to be implemented for interaction with the ticker manager class.
     """
+
     @abc.abstractmethod
     def is_websocket_open(self):
         pass
diff --git a/Blankly/auth/Coinbase/__init__.py b/blankly/exchanges/auth/__init__.py
similarity index 100%
rename from Blankly/auth/Coinbase/__init__.py
rename to blankly/exchanges/auth/__init__.py
diff --git a/blankly/exchanges/auth/abc_auth.py b/blankly/exchanges/auth/abc_auth.py
new file mode 100644
index 00000000..54db3b7d
--- /dev/null
+++ b/blankly/exchanges/auth/abc_auth.py
@@ -0,0 +1,70 @@
+"""
+    Inherited authentication object
+    Copyright (C) 2021  Arun Annamalai, 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 <https://www.gnu.org/licenses/>.
+"""
+
+import abc
+import warnings
+
+from blankly.exchanges.auth.auth_constructor import load_auth
+from blankly.utils.exceptions import AuthError
+
+
+class ABCAuth(abc.ABC):
+    def __init__(self, keys_file: str, portfolio_name: str, exchange: str):
+        """
+        Create a auth interface
+        Args:
+            keys_file (str): filepath to keys.json
+            portfolio_name (str): name of portfolio
+            exchange (str): name of exchange
+        """
+        # self.portfolio_name = portfolio_name
+        self.exchange = exchange
+
+        # Load from file
+        self.portfolio_name, self.keys = load_auth(exchange, keys_file, portfolio_name)
+
+    def validate_credentials(self, needed_keys: list):
+        """
+        Args:
+            needed_keys (list): List of keys that the exchange needs in the format ['API_KEY', 'API_SECRET]
+
+        Returns:
+            A boolean that describes if the auth is good or bad (True if good).
+        """
+
+        # Create an error message template to throw if needed
+        error_message = ""
+
+        # Make a copy of the keys dict & list to avoid modifying passed variables
+        keys_dict = {**self.keys}
+        needed_keys = list.copy(needed_keys)
+        for i in needed_keys:
+            if i in keys_dict.keys():
+                keys_dict.pop(i)
+            else:
+                # Append a description header if not present
+                if error_message == "":
+                    error_message += "Error while loading authentication. Required keys for this are missing: \n"
+                error_message += str(str(i) + " is needed, but not defined.\n")
+
+        if len(keys_dict.keys()) > 0:
+            warnings.warn(f"Additional keys for Exchange: {self.exchange} Portfolio: {self.portfolio_name} will be"
+                          f" ignored.")
+
+        if error_message != "":
+            raise AuthError(error_message)
diff --git a/Blankly/auth_constructor.py b/blankly/exchanges/auth/auth_constructor.py
similarity index 71%
rename from Blankly/auth_constructor.py
rename to blankly/exchanges/auth/auth_constructor.py
index eed01e82..16aed39a 100644
--- a/Blankly/auth_constructor.py
+++ b/blankly/exchanges/auth/auth_constructor.py
@@ -19,6 +19,11 @@
 import json
 import warnings
 
+keys_path_cache = None
+
+# This is currently unused
+auth_cache = {}
+
 
 def load_json(keys_file):
     try:
@@ -28,27 +33,38 @@ def load_json(keys_file):
         return contents
     except FileNotFoundError:
         raise FileNotFoundError("Make sure a keys.json file is placed in the same folder as the project working "
-                                "directory!")
-
-
-auth_cache = {}
-
-
-def load_auth_coinbase_pro(keys_file, name):
-    exchange_type = "coinbase_pro"
-    name, portfolio = __load_auth(keys_file, name, exchange_type)
-
-    return [portfolio["API_KEY"], portfolio["API_SECRET"], portfolio["API_PASS"]], name
-
-
-def load_auth_binance(keys_file, name):
-    exchange_type = "binance"
-    name, portfolio = __load_auth(keys_file, name, exchange_type)
-
-    return [portfolio["API_KEY"], portfolio["API_SECRET"]], name
-
+                                "directory (or specified in the exchange constructor)!")
+
+
+# def load_auth_coinbase_pro(keys_file, name):
+#     exchange_type = "coinbase_pro"
+#     name, portfolio = __load_auth(keys_file, name, exchange_type)
+#
+#     return [portfolio["API_KEY"], portfolio["API_SECRET"], portfolio["API_PASS"]], name
+#
+#
+# def load_auth_binance(keys_file, name):
+#     exchange_type = "binance"
+#     name, portfolio = __load_auth(keys_file, name, exchange_type)
+#
+#     return [portfolio["API_KEY"], portfolio["API_SECRET"]], name
+
+
+def load_auth(exchange_type, keys_file=None, name=None):
+    global keys_path_cache
+
+    if keys_file is None:
+        if keys_path_cache is None:
+            # Add a default for if info is passed in and nobody knows anything about paths
+            keys_path_cache = './keys.json'
+            keys_file = keys_path_cache
+        else:
+            # Default to the cached path if the passed variable is wrong
+            keys_file = keys_path_cache
+    else:
+        # If its not non then there's no problem, just write it to the cache though
+        keys_path_cache = keys_file
 
-def __load_auth(keys_file, name, exchange_type):
     auth_object = load_json(keys_file)
     exchange_keys = auth_object[exchange_type]
     if name is None:
diff --git a/blankly/exchanges/auth/auth_factory.py b/blankly/exchanges/auth/auth_factory.py
new file mode 100644
index 00000000..3232898d
--- /dev/null
+++ b/blankly/exchanges/auth/auth_factory.py
@@ -0,0 +1,36 @@
+"""
+    Switch system to find correct authentication method
+    Copyright (C) 2021  Arun Annamalai, 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 <https://www.gnu.org/licenses/>.
+"""
+
+from blankly.exchanges.interfaces.alpaca.alpaca_auth import AlpacaAuth
+from blankly.exchanges.interfaces.binance.binance_auth import BinanceAuth
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_auth import CoinbaseProAuth
+
+
+class AuthFactory:
+    @staticmethod
+    def create_auth(keys_file, exchange_name, portfolio_name):
+        if exchange_name == 'alpaca':
+            return AlpacaAuth(keys_file, portfolio_name)
+        elif exchange_name == 'binance':
+            return BinanceAuth(keys_file, portfolio_name)
+        elif exchange_name == 'coinbase_pro':
+            return CoinbaseProAuth(keys_file, portfolio_name)
+        elif exchange_name == 'paper_trade':
+            return None
+        else:
+            raise KeyError("Exchange not supported")
diff --git a/blankly/exchanges/auth/utils.py b/blankly/exchanges/auth/utils.py
new file mode 100644
index 00000000..9418f0b3
--- /dev/null
+++ b/blankly/exchanges/auth/utils.py
@@ -0,0 +1,46 @@
+"""
+    Utils for constructing authentication
+    Copyright (C) 2021  Arun Annamalai, 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 <https://www.gnu.org/licenses/>.
+"""
+
+# TODO get rid of this file and merge with the auth constructor
+
+import json
+import warnings
+
+
+def load_json(keys_file):
+    try:
+        f = open(keys_file)
+        contents = json.load(f)
+        f.close()
+        return contents
+    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
diff --git a/Blankly/exchanges/exchange.py b/blankly/exchanges/exchange.py
similarity index 71%
rename from Blankly/exchanges/exchange.py
rename to blankly/exchanges/exchange.py
index 7c43102b..5c4325d6 100644
--- a/Blankly/exchanges/exchange.py
+++ b/blankly/exchanges/exchange.py
@@ -15,24 +15,34 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-import Blankly
-from Blankly.exchanges.IExchange import IExchange
-from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_Interface import CoinbaseProInterface
-from Blankly.exchanges.Binance.Binance_Interface import BinanceInterface
-
-from Blankly.interface.abc_currency_interface import ICurrencyInterface
-import time
 import abc
+import time
+
+import blankly
+from blankly.exchanges.abc_exchange import ABCExchange
+from blankly.exchanges.auth.auth_constructor import write_auth_cache
+from blankly.exchanges.auth.auth_factory import AuthFactory
+from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface
+from blankly.exchanges.interfaces.binance.binance_interface import BinanceInterface
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_interface import CoinbaseProInterface
+from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory
+
+
+class Exchange(ABCExchange, abc.ABC):
+    interface: ABCExchangeInterface
 
+    def __init__(self, exchange_type, portfolio_name, keys_path, preferences_path):
+        self.__type = exchange_type  # coinbase_pro, binance, alpaca
+        self.__name = portfolio_name  # my_cool_portfolio
+        self.__factory = AuthFactory()
 
-class Exchange(IExchange, abc.ABC):
+        self.__auth = self.__factory.create_auth(keys_path, self.__type, self.__name)
+        self.__direct_calls_factory = DirectCallsFactory()
+        self.calls, self.interface = self.__direct_calls_factory.create(self.__type, self.__auth, preferences_path)
+        write_auth_cache(exchange_type, portfolio_name, self.calls)
 
-    def __init__(self, exchange_type, exchange_name, preferences_path):
-        self.__name = exchange_name  # my_cool_portfolio
-        self.__type = exchange_type  # coinbase_pro, binance,
-        self.preferences = Blankly.utils.load_user_preferences(preferences_path)
+        self.preferences = blankly.utils.load_user_preferences(preferences_path)
 
-        self.Interface = None
         # Create the model container
         self.models = {}
 
@@ -47,15 +57,15 @@ def get_preferences(self):
 
     def construct_interface(self, calls):
         if self.__type == "coinbase_pro":
-            self.Interface = CoinbaseProInterface(self.__type, calls)
+            self.interface = CoinbaseProInterface(self.__type, calls)
         elif self.__type == "binance":
-            self.Interface = BinanceInterface(self.__type, calls)
+            self.interface = BinanceInterface(self.__type, calls)
 
-    def get_interface(self) -> ICurrencyInterface:
+    def get_interface(self) -> ABCExchangeInterface:
         """
         Get the the authenticated interface for the object. This will provide authenticated API calls.
         """
-        return self.Interface
+        return self.interface
 
     def start_models(self, coin_id=None):
         """
@@ -84,7 +94,7 @@ def __get_model(self, coin):
 
     def get_model_state(self, currency):
         """
-        Returns JUST the model state, as opposed to all the data returned by get_currency_state()
+        Returns JUST the model state, as opposed to all the data returned by get_asset_state()
 
         Args:
             currency: Currency that the selected model is running on.
@@ -99,7 +109,7 @@ def get_full_state(self, currency):
         Args:
             currency: Currency to filter for. This filters model information and the exchange information.
         """
-        state = self.get_currency_state(currency)
+        state = self.get_asset_state(currency)
 
         return {
             "account": state,
@@ -131,10 +141,10 @@ def append_model(self, model, coin_id, args=None):
             "args": args
         }
         model.setup(self.__type, coin_id, self.preferences, self.get_full_state(coin_id),
-                    self.Interface)
+                    self.interface)
 
     @abc.abstractmethod
-    def get_currency_state(self, currency):
+    def get_asset_state(self, symbol):
         pass
 
     @abc.abstractmethod
diff --git a/Blankly/auth/__init__.py b/blankly/exchanges/interfaces/__init__.py
similarity index 100%
rename from Blankly/auth/__init__.py
rename to blankly/exchanges/interfaces/__init__.py
diff --git a/blankly/exchanges/interfaces/abc_exchange_interface.py b/blankly/exchanges/interfaces/abc_exchange_interface.py
new file mode 100644
index 00000000..3fea6f3f
--- /dev/null
+++ b/blankly/exchanges/interfaces/abc_exchange_interface.py
@@ -0,0 +1,259 @@
+"""
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+import abc
+from datetime import datetime as dt
+from typing import Union
+
+import pandas
+
+from blankly.exchanges.orders.limit_order import LimitOrder
+from blankly.exchanges.orders.market_order import MarketOrder
+
+
+class ABCExchangeInterface(abc.ABC):
+    @abc.abstractmethod
+    def __init__(self, exchange_name, authenticated_API):
+        """
+        Create an abstract exchange interface
+        Args:
+            exchange_name (str): Define exchange name ex: 'binance' or 'coinbase_pro'
+            authenticated_API (obj): Authenticated direct calls object
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_calls(self):
+        """
+        Get the direct & authenticated exchange object
+
+        Returns:
+             The exchange's direct calls object. A blankly Bot class should have immediate access to this by
+             default
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_exchange_type(self):
+        """
+        Get the type of exchange ex: "coinbase_pro" or "binance"
+
+        Returns:
+             A string that corresponds to the type of exchange
+
+        TODO add return example
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_products(self):
+        """
+        Get all trading pairs on the exchange & some information about the exchange limits.
+
+        TODO add return example
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_account(self,
+                    symbol: str = None) -> dict:
+        """
+        Get all assets in an account, or sort by symbol/account_id
+        Args:
+            symbol (Optional): Filter by particular symbol
+
+            These arguments are mutually exclusive
+
+        TODO add return example
+        """
+        pass
+
+    @abc.abstractmethod
+    def market_order(self,
+                     symbol: str,
+                     side: str,
+                     funds: float) -> MarketOrder:
+        """
+        Used for buying or selling market orders
+        Args:
+            symbol: asset to buy
+            side: buy/sell
+            funds: desired amount of quote asset to use
+        """
+        pass
+
+    @abc.abstractmethod
+    def limit_order(self,
+                    symbol: str,
+                    side: str,
+                    price: float,
+                    size: float) -> LimitOrder:
+        """
+        Used for buying or selling limit orders
+        Args:
+            symbol: asset to buy
+            side: buy/sell
+            price: price to set limit order
+            size: amount of asset (like BTC) for the limit to be valued
+        """
+        pass
+
+    @abc.abstractmethod
+    def cancel_order(self,
+                     symbol: str,
+                     order_id: str) -> dict:
+        """
+        Cancel an order on a particular symbol & order id
+
+        Args:
+            symbol: This is the asset id that the order is under
+            order_id: The unique ID of the order.
+
+        TODO add return example
+        """
+
+    @property
+    @abc.abstractmethod
+    def account(self) -> dict:
+        """
+        Get all assets in an account, or sort by assets/account_id
+
+        TODO add return example
+        """
+        pass
+
+    @property
+    @abc.abstractmethod
+    def orders(self) -> list:
+        """
+        List open orders.
+        TODO add return example
+        """
+        pass
+
+    @property
+    @abc.abstractmethod
+    def cash(self) -> float:
+        """
+        Get the amount of cash in a portfolio. The cash default is set in the settings .json file
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_open_orders(self,
+                        symbol: str = None) -> list:
+        """
+        List open orders.
+        Args:
+            symbol (optional) (str): Asset such as BTC-USD
+        TODO add return example
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_order(self,
+                  symbol: str,
+                  order_id: str) -> dict:
+        """
+        Get a certain order
+        Args:
+            symbol: Asset that the order is under
+            order_id: The unique ID of the order.
+        TODO add return example
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_fees(self) -> dict:
+        """
+        Get market fees
+        TODO add return example
+        """
+        pass
+
+    """
+    Coinbase Pro: get_account_history
+    binance: 
+        get_deposit_history
+        get_withdraw_history
+
+    """
+
+    @abc.abstractmethod
+    def history(self,
+                symbol: str,
+                to: Union[str, int] = 200,
+                resolution: Union[str, float] = '1d',
+                start_date: Union[str, dt, float] = None,
+                end_date: Union[str, dt, float] = None) -> pandas.DataFrame:
+        """
+        Wrapper for .get_product_history() which allows users to more easily get product history from right now.
+        Args:
+            symbol: Blankly product ID format (BTC-USD)
+            to (str or int): The number of data points back in time either expressed as a string
+                ("1y" meaning 1 year back") or int of points (300 points at specified resolution)
+            resolution: Resolution as a string (i.e. "1d", "4h", "1y")
+            start_date (str or datetime or float): Start Date for data gathering (in either string, datetime or epoch
+                timestamp)
+            end_date (str or datetime or float): End Date for data gathering (in either string, datetime or epoch
+                timestamp)
+        Returns:
+            Dataframe with *at least* 'time (epoch)', 'low', 'high', 'open', 'close', 'volume' as columns.
+            TODO add return example
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_product_history(self,
+                            symbol: str,
+                            epoch_start: float,
+                            epoch_stop: float,
+                            resolution: Union[str, float]) -> pandas.DataFrame:
+        """
+        Returns the product history from an exchange
+        Args:
+            symbol: The asset such as (BTC-USD, or MSFT)
+            epoch_start: Time to begin download
+            epoch_stop: Time to stop download
+            resolution: Resolution in seconds between tick (ex: 60 = 1 per minute)
+        Returns:
+            Dataframe with *at least* 'time (epoch)', 'low', 'high', 'open', 'close', 'volume' as columns.
+            TODO add return example
+        """
+
+    @abc.abstractmethod
+    def get_order_filter(self,
+                         symbol: str):
+        """
+        Find order limits for the exchange
+        Args:
+            symbol: The asset such as (BTC-USD, or MSFT)
+            TODO add return example
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_price(self,
+                  symbol: str) -> float:
+        """
+        Returns just the price of a symbol.
+        Args:
+            symbol: The asset such as (BTC-USD, or MSFT)
+            TODO add return example
+        """
+        pass
diff --git a/Blankly/exchanges/Alpaca/__init__.py b/blankly/exchanges/interfaces/alpaca/__init__.py
similarity index 100%
rename from Blankly/exchanges/Alpaca/__init__.py
rename to blankly/exchanges/interfaces/alpaca/__init__.py
diff --git a/Blankly/exchanges/Alpaca/Alpaca.py b/blankly/exchanges/interfaces/alpaca/alpaca.py
similarity index 54%
rename from Blankly/exchanges/Alpaca/Alpaca.py
rename to blankly/exchanges/interfaces/alpaca/alpaca.py
index c6b1e932..74b508e3 100644
--- a/Blankly/exchanges/Alpaca/Alpaca.py
+++ b/blankly/exchanges/interfaces/alpaca/alpaca.py
@@ -1,6 +1,6 @@
 """
-    Alpaca exchange definitions and setup
-    Copyright (C) 2021  Emerson Dove, Arun Annamalai, Brandon Fan
+    alpaca exchange definitions and setup
+    Copyright (C) 2021 Arun Annamalai, Emerson Dove, Brandon Fan
 
     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
@@ -16,17 +16,23 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-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
+import alpaca_trade_api
+
+from blankly.exchanges.exchange import Exchange
+
 
 class Alpaca(Exchange):
     def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
-        if not portfolio_name:
-            portfolio_name = default_first_portfolio(keys_path, 'alpaca')
-        Exchange.__init__(self, 'alpaca', portfolio_name, settings_path)
-        alpaca_auth = AuthFactory(keys_path, 'alpaca', portfolio_name)
-        self.interface = InterfaceFactory('alpaca', alpaca_auth, self.preferences)
+        Exchange.__init__(self, 'alpaca', portfolio_name, keys_path, settings_path)
+
+    def get_exchange_state(self):
+        return self.interface.get_fees()
+
+    def get_asset_state(self, symbol):
+        return self.interface.get_account(symbol)
 
+    def get_direct_calls(self) -> alpaca_trade_api.REST:
+        return self.calls
 
+    def get_market_clock(self):
+        return self.calls.get_clock()
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_api.py b/blankly/exchanges/interfaces/alpaca/alpaca_api.py
new file mode 100644
index 00000000..7de0bd08
--- /dev/null
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_api.py
@@ -0,0 +1,49 @@
+"""
+    Alpaca API creation definition.
+    Copyright (C) 2021 Arun Annamalai
+
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+import os
+
+import alpaca_trade_api
+
+from blankly.exchanges.auth.abc_auth import ABCAuth
+
+APCA_API_LIVE_URL = "https://api.alpaca.markets"
+APCA_API_PAPER_URL = "https://paper-api.alpaca.markets"
+
+
+def create_alpaca_client(auth: ABCAuth, sandbox_mode=True):
+    if sandbox_mode:
+        api_url = APCA_API_PAPER_URL
+    else:
+        api_url = APCA_API_LIVE_URL
+
+    return alpaca_trade_api.REST(auth.keys['API_KEY'], auth.keys['API_SECRET'], api_url, 'v2', raw_data=True)
+
+
+class API:
+    def __init__(self, auth: ABCAuth, paper_trading=True):
+        if paper_trading:
+            self.__api_url = APCA_API_PAPER_URL
+        else:
+            self.__api_url = APCA_API_LIVE_URL
+
+        self.alp_client = alpaca_trade_api.REST(auth.keys['API_KEY'], auth.keys['API_SECRET'], self.__api_url, 'v2')
+
+
+api = os.getenv("ALPACA_PUBLIC")
+secret = os.getenv("ALPACA_PRIVATE")
diff --git a/tests/exchanges/Binance/test_Binance_Interface.py b/blankly/exchanges/interfaces/alpaca/alpaca_auth.py
similarity index 50%
rename from tests/exchanges/Binance/test_Binance_Interface.py
rename to blankly/exchanges/interfaces/alpaca/alpaca_auth.py
index c6a22870..bb1de963 100644
--- a/tests/exchanges/Binance/test_Binance_Interface.py
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_auth.py
@@ -1,6 +1,6 @@
 """
-    Unit tests for Binance.
-    Copyright (C) 2021  Emerson Dove
+    alpaca authentication base class
+    Copyright (C) 2021  Arun Annamalai, 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
@@ -15,19 +15,12 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-import Blankly
-import unittest
-from Blankly.utils.utils import compare_dictionaries
 
+from blankly.exchanges.auth.abc_auth import ABCAuth
 
-class BinanceInterface(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls) -> None:
-        cls.Binance = Blankly.Binance(portfolio_name="Spot Test Key",
-                                      keys_path="./tests/config/keys.json",
-                                      settings_path="./tests/config/settings.json")
-        cls.Binance_Interface = cls.Binance.get_interface()
 
-    def test_get_exchange_type(self):
-        binance = self.Binance_Interface.get_exchange_type()
-        self.assertEqual(binance, "binance")
+class AlpacaAuth(ABCAuth):
+    def __init__(self, keys_file, portfolio_name):
+        super().__init__(keys_file, portfolio_name, 'alpaca')
+        needed_keys = ['API_KEY', 'API_SECRET']
+        self.validate_credentials(needed_keys)
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_fixes.md b/blankly/exchanges/interfaces/alpaca/alpaca_fixes.md
new file mode 100644
index 00000000..ae77921a
--- /dev/null
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_fixes.md
@@ -0,0 +1,61 @@
+## Alpaca Fixes
+
+### get_products()
+
+- Need correct base_min_size
+- Need correct base_max_size
+- Need a correct base increment that depends on if the equity is fractional
+- Needs capital USD (**fixed**)
+- Needs correct currency ID (**fixed**)
+
+### get_account()
+
+- Need a correct hold value
+    - This is described here https://alpaca.markets/docs/trading-on-alpaca/orders/ in buying power
+- Get account with no arguments needs to list every single account, including those with no size at all. This involves
+  parsing the products and comparing those to the account output (this may be implemented I can really tell).
+- USD also needs to be in there
+
+### market_order()
+
+- Looks great
+- Up for debate but isoparse could use the implementation in utils to clear up an import
+
+### limit_order()
+
+- Let it throw the alpaca APIError instead of a warning and returning the wrong type
+- Limit order should be `GTC` because all other exchanges are strictly GTC
+
+### cancel_order()
+
+- Looks great to me
+
+### get_open_orders()
+
+- The isolated version is never assigned back to the array unless the reference works here
+
+### get_order()
+
+- Looks great
+
+### get_fees()
+
+- Looks great
+
+### get_product_history()
+
+- Needs to auto round to needed granularity
+
+### get_market_limits()
+
+- Needs doing
+
+### get_price()
+
+- Completely broken
+
+## Also need:
+
+- Ability to support a currency pair or just a single id (`AAPL` or `AAPL-USD`) for all functions
+- Timezone checking
+- Error printing *using blankly exceptions*
\ No newline at end of file
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_interface.py b/blankly/exchanges/interfaces/alpaca/alpaca_interface.py
new file mode 100644
index 00000000..bafa66e4
--- /dev/null
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_interface.py
@@ -0,0 +1,539 @@
+"""
+    alpaca API Interface Definition
+    Copyright (C) 2021  Arun Annamalai, Emerson Dove, Brandon Fan
+
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+import time
+import warnings
+from datetime import datetime as dt, timezone
+from typing import Union
+
+import alpaca_trade_api
+import dateparser
+import pandas as pd
+from alpaca_trade_api.rest import APIError as AlpacaAPIError, TimeFrame
+from dateutil import parser
+
+from blankly.exchanges.interfaces.alpaca.alpaca_api import API
+from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
+from blankly.exchanges.orders.limit_order import LimitOrder
+from blankly.exchanges.orders.market_order import MarketOrder
+from blankly.utils import utils as utils
+from blankly.utils.exceptions import APIException
+from blankly.utils.time_builder import build_minute, time_interval_to_seconds
+
+NY = 'America/New_York'
+
+
+class AlpacaInterface(ExchangeInterface):
+    def __init__(self, authenticated_API: API, preferences_path: str):
+        super().__init__('alpaca', authenticated_API, preferences_path)
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+
+    def init_exchange(self):
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+        account_info = self.calls.get_account()
+        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
+        }
+
+        filtered_assets = []
+        products = self.calls.list_assets(status=None, asset_class=None)
+        for i in products:
+            if i['symbol'] not in filtered_assets:
+                filtered_assets.append(i['symbol'])
+            else:
+                # TODO handle duplicate symbols
+                pass
+
+        self.__unique_assets = filtered_assets
+
+    def get_products(self) -> dict:
+        """
+        [
+            {
+              "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)
+
+        for asset in assets:
+            base_asset = asset['symbol']
+            asset['symbol'] = base_asset
+            asset['base_asset'] = base_asset
+            asset['quote_asset'] = 'USD'
+            if asset['fractionable']:
+                asset['base_min_size'] = .000000001
+                asset['base_increment'] = .000000001
+            else:
+                asset['base_min_size'] = 1
+                asset['base_increment'] = 1
+            asset['base_max_size'] = 10000000000
+
+        for i in range(len(assets)):
+            assets[i] = utils.isolate_specific(needed, assets[i])
+
+        return assets
+
+    @property
+    def cash(self):
+        account_dict = self.calls.get_account()
+        return float(account_dict['buying_power'])
+
+    def get_account(self, symbol=None):
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+        needed = self.needed['get_account']
+
+        positions = self.calls.list_positions()
+        positions_dict = utils.AttributeDict({})
+
+        for position in positions:
+            curr_symbol = position.pop('symbol')
+            positions_dict[curr_symbol] = utils.AttributeDict({
+                'available': float(position.pop('qty')),
+                'hold': 0
+            })
+
+        symbols = list(positions_dict.keys())
+        open_orders = self.calls.list_orders(status='open', symbols=symbols)
+        snapshot_price = self.calls.get_snapshots(symbols=symbols)
+
+        # now grab the available cash in the account
+        account = self.calls.get_account()
+        positions_dict['USD'] = utils.AttributeDict({
+            'available': float(account['cash']),
+            'hold': 0
+        })
+
+        for order in open_orders:
+            curr_symbol = order['symbol']
+            if order['side'] == 'buy':  # buy orders only affect USD holds
+                if order['qty']:  # this case handles qty market buy and limit buy
+                    if order['type'] == 'limit':
+                        dollar_amt = float(order['qty']) * float(order['limit_price'])
+                    elif order['type'] == 'market':
+                        dollar_amt = float(order['qty']) * snapshot_price[curr_symbol]['latestTrade']['p']
+                    else:  # we dont have support for stop_order, stop_limit_order
+                        dollar_amt = 0
+                else:  # this is the case for notional market buy
+                    dollar_amt = order['notional']
+
+                positions_dict['USD']['available'] -= dollar_amt
+                positions_dict['USD']['hold'] += dollar_amt
+
+            else:
+                if order['qty']:  # this case handles qty market sell and limit sell
+                    qty = float(order['qty'])
+                else:  # this is the case for notional market sell, calculate the qty with cash/price
+                    qty = float(order['notional']) / snapshot_price[curr_symbol]['latestTrade']['p']
+
+                positions_dict[curr_symbol]['available'] -= qty
+                positions_dict[curr_symbol]['hold'] += qty
+
+                if symbol is not None and curr_symbol == symbol:
+                    return utils.AttributeDict({
+                        'available': positions_dict[curr_symbol]['available'],
+                        'hold': positions_dict[curr_symbol]['hold']
+                    })
+
+        if symbol == 'USD':
+            return utils.AttributeDict({
+                'available': positions_dict['USD']['available'],
+                'hold': positions_dict['USD']['available']
+            })
+
+        if symbol is not None:
+            # if we haven't found the currency, then we'll end up here
+            return utils.AttributeDict({
+                'available': 0,
+                'hold': 0
+            })
+
+        # This is a patch fix that should be fixed to be more optimized
+        if symbol is not None:
+            for i in self.__unique_assets:
+                if i not in positions_dict:
+                    positions_dict[i] = {
+                        'available': 0,
+                        'hold': 0
+                    }
+
+        return positions_dict
+
+    def market_order(self, symbol, side, funds) -> MarketOrder:
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+        needed = self.needed['market_order']
+
+        renames = [
+            ['notional', 'funds']
+        ]
+
+        order = {
+            'funds': funds,
+            'side': side,
+            'symbol': symbol,
+            'type': 'market'
+        }
+        response = self.calls.submit_order(symbol, side=side, type='market', time_in_force='day', notional=funds)
+        response['created_at'] = parser.isoparse(response['created_at']).timestamp()
+        response = utils.rename_to(renames, response)
+        response = utils.isolate_specific(needed, response)
+        return MarketOrder(order, response, self)
+
+    def limit_order(self, symbol: str, side: str, price: float, quantity: int) -> LimitOrder:
+        needed = self.needed['limit_order']
+
+        renames = [
+            ['limit_price', 'price'],
+            ['qty', 'size']
+        ]
+
+        order = {
+            'quantity': quantity,
+            'side': side,
+            'price': price,
+            'symbol': symbol,
+            'type': 'limit'
+        }
+        response = self.calls.submit_order(symbol,
+                                           side=side,
+                                           type='limit',
+                                           time_in_force='gtc',
+                                           qty=quantity,
+                                           limit_price=price)
+
+        response['created_at'] = parser.isoparse(response['created_at']).timestamp()
+        response = utils.rename_to(renames, response)
+        response = utils.isolate_specific(needed, response)
+        response['time_in_force'] = response['time_in_force'].upper()
+        return LimitOrder(order, response, self)
+
+    def cancel_order(self, symbol, order_id) -> dict:
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+        self.calls.cancel_order(order_id)
+
+        # TODO: handle the different response codes
+        return {'order_id': order_id}
+
+    def get_open_orders(self, symbol=None):
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+        orders = self.calls.list_orders(status='open')
+
+        for i in range(len(orders)):
+            # orders[i] = utils.rename_to(renames, orders[i])
+            # if orders[i]['type'] == "limit":
+            #     orders[i]['price'] = orders[i]['limit_price']
+            # if orders[i]['type'] == "market":
+            #     if orders[i]['notional']:
+            #         orders[i]['funds'] = orders[i]['notional']
+            #     else:
+            #         orders[i]['funds'] = orders[i]['notional']
+            # orders[i]['created_at'] = parser.isoparse(orders[i]['created_at']).timestamp()
+            orders[i] = self.homogenize_order(orders[i])
+
+        return orders
+
+    def get_order(self, symbol, order_id) -> dict:
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+        order = self.calls.get_order(order_id)
+        order = self.homogenize_order(order)
+        return order
+
+    # TODO: fix this function
+    def homogenize_order(self, order):
+        if order['type'] == "limit":
+            renames = [
+                ["qty", "size"],
+                ["limit_price", "price"]
+            ]
+            order = utils.rename_to(renames, order)
+
+        elif order['type'] == "market":
+            if order['notional']:
+                renames = [
+                    ["notional", "funds"]
+                ]
+                order = utils.rename_to(renames, order)
+
+            else: # market order of number of shares
+                qty = order['qty']
+                price = self.calls.get_last_trade(symbol=order['symbol'])
+                order['funds'] = qty * price
+
+        # TODO: test stop_limit orders
+        elif order['type'] == "stop_limit":
+            renames = [
+                ["qty", "size"],
+            ]
+            order = utils.rename_to(renames, order)
+
+        order['created_at'] = parser.isoparse(order['created_at']).timestamp()
+        needed = self.choose_order_specificity(order['type'])
+
+        order = utils.isolate_specific(needed, order)
+        return order
+
+    def get_fees(self):
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+        return {
+            'maker_fee_rate': 0.0,
+            'taker_fee_rate': 0.0
+        }
+
+    def get_product_history(self, symbol: str, epoch_start: float, epoch_stop: float, resolution: int):
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+
+        supported_multiples = [60, 3600, 86400]
+        if resolution not in supported_multiples:
+            warnings.warn("Granularity is not an accepted granularity...rounding to nearest valid value.")
+            resolution = supported_multiples[min(range(len(supported_multiples)),
+                                             key=lambda i: abs(supported_multiples[i] - resolution))]
+
+        found_multiple = -1
+        for multiple in reversed(supported_multiples):
+            if resolution % multiple == 0:
+                found_multiple = multiple
+                break
+        if found_multiple < 0:
+            raise ValueError("alpaca currently does not support this specific resolution, please make the resolution a "
+                             "multiple of 1 minute, 1 hour or 1 day")
+
+        row_divisor = resolution / found_multiple
+        row_divisor = int(row_divisor)
+
+        if row_divisor > 100:
+            raise Warning("The resolution you requested is an extremely high of the base resolutions supported and may "
+                          "slow down the performance of your model: {} * {}".format(found_multiple, row_divisor))
+
+        if found_multiple == 60:
+            time_interval = TimeFrame.Minute
+        elif found_multiple == 3600:
+            time_interval = TimeFrame.Hour
+        else:
+            time_interval = TimeFrame.Day
+
+        epoch_start_str = dt.fromtimestamp(epoch_start, tz=timezone.utc).isoformat()
+        epoch_stop_str = dt.fromtimestamp(epoch_stop, tz=timezone.utc).isoformat()
+
+        try:
+            bars = self.calls.get_bars(symbol, time_interval, epoch_start_str, epoch_stop_str, adjustment='raw').df
+        except AlpacaAPIError as e:
+            if e.code == 42210000:
+                warning_string = "Your alpaca subscription does not permit querying data from the last 15 minutes. " \
+                                 "Blankly is adjusting your query."
+                warnings.warn(warning_string)
+                epoch_stop = time.time() - (build_minute() * 15)
+                if epoch_stop >= epoch_start:
+                    return self.get_product_history(symbol, epoch_start, epoch_stop, resolution)
+                else:
+                    warning_string = "No data range queried after time adjustmnet."
+                    warnings.warn(warning_string)
+                    return pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
+            else:
+                raise e
+        bars.rename(columns={"t": "time", "o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"},
+                    inplace=True)
+
+        return utils.get_ohlcv(bars, row_divisor)
+
+    def history(self,
+                symbol: str,
+                to: Union[str, int] = None,
+                resolution: Union[str, float] = '1d',
+                start_date: Union[str, dt, float] = None,
+                end_date: Union[str, dt, float] = None):
+
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+        if not to and not start_date:
+            raise ValueError("history() call needs only 1 of {start_date, to} defined")
+        if to and start_date:
+            raise ValueError("history() call needs only 1 of {start_date, to} defined")
+
+        if not end_date:
+            end_date = dt.now(tz=timezone.utc)
+
+        # convert end_date to datetime object
+        if isinstance(end_date, str):
+            end_date = dateparser.parse(end_date)
+        elif isinstance(end_date, float):
+            end_date = dt.fromtimestamp(end_date)
+
+        # end_date object is naive datetime, so need to convert
+        if end_date.tzinfo is None or end_date.tzinfo.utcoffset(end_date) is None:
+            end_date = end_date.replace(tzinfo=timezone.utc)
+
+        if start_date:
+            # convert start_date to datetime object
+            if isinstance(start_date, str):
+                start_date = dateparser.parse(start_date)
+            elif isinstance(start_date, float):
+                start_date = dt.fromtimestamp(start_date)
+
+            # end_date object is naive datetime, so need to convert
+            if start_date.tzinfo is None or start_date.tzinfo.utcoffset(start_date) is None:
+                start_date = start_date.replace(tzinfo=timezone.utc)
+
+        alpaca_v1_resolutions = [60 * 1, 60 * 5, 60 * 15, 60 * 60 * 24]
+
+        # convert resolution into epoch seconds
+        resolution_seconds = time_interval_to_seconds(resolution)
+
+        found_multiple = -1
+        for multiple in reversed(alpaca_v1_resolutions):
+            if resolution_seconds % multiple == 0:
+                found_multiple = multiple
+                break
+        if found_multiple < 0:
+            raise ValueError("alpaca currently does not support this specific resolution, please make the resolution a "
+                             "multiple of 1 minute, 1 hour or 1 day")
+
+        row_divisor = resolution_seconds / found_multiple
+        row_divisor = int(row_divisor)
+
+        if row_divisor > 100:
+            raise Warning("The resolution you requested is an extremely high of the base resolutions supported and may "
+                          "slow down the performance of your model: {} * {}".format(found_multiple, row_divisor))
+
+        if found_multiple == 60:
+            time_interval = '1Min'
+        elif found_multiple == 60 * 5:
+            time_interval = '5Min'
+        elif found_multiple == 60 * 15:
+            time_interval = '15Min'
+        elif found_multiple == 60 * 60 * 24:
+            time_interval = '1D'
+
+        if to:
+            aggregated_limit = to * row_divisor
+            bars = self.calls.get_barset(symbol, time_interval, limit=int(aggregated_limit), end=end_date.isoformat())[
+                symbol]
+            return_df = pd.DataFrame(bars)
+            return_df.rename(columns={"t": "time", "o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"},
+                             inplace=True)
+
+            return utils.get_ohlcv_2(return_df, row_divisor)
+        else:
+            # bars = self.calls.get_barset(symbol, time_interval, start=start_date.isoformat(), end=end_date.isoformat())[symbol]
+            return self.get_product_history(symbol, start_date.timestamp(), end_date.timestamp(), resolution_seconds)
+        # return_df = pd.DataFrame(bars)
+        # return_df.rename(columns={"t": "time", "o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"}, inplace=True)
+        #
+        # return utils.get_ohlcv_2(return_df, row_divisor)
+
+    # TODO: tbh not sure how this one works or if it applies to alpaca
+    def get_order_filter(self, symbol: str):
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+        current_price = self.get_price(symbol)
+
+        products = self.get_products()
+
+        product = None
+        for i in products:
+            if i['symbol'] == symbol:
+                product = i
+                break
+        if product is None:
+            raise APIException("Symbol not found.")
+
+        exchange_specific = product['exchange_specific']
+        fractionable = exchange_specific['fractionable']
+
+        if fractionable:
+            quote_increment = 1e-9
+            min_funds_buy = 1
+            min_funds_sell = 1e-9 * current_price
+
+            # base_min_size = product['base_min_size']
+            base_max_size = product['base_max_size']
+            # base_increment = product['base_increment']
+            min_price = 0.0001
+            max_price = 10000000000
+        else:
+            quote_increment = current_price
+            min_funds_buy = current_price
+            min_funds_sell = current_price
+
+            # base_min_size = product['base_min_size']
+            base_max_size = product['base_max_size']
+            # base_increment = product['base_increment']
+            min_price = 0.0001
+            max_price = 10000000000
+
+        max_funds = current_price * 10000000000
+
+        return {
+            "symbol": symbol,
+            "base_asset": symbol,
+            "quote_asset": 'USD',
+            "max_orders": 500,  # More than this and we can't calculate account value (alpaca is very bad)
+            "limit_order": {
+                "base_min_size": 1,  # Minimum size to buy
+                "base_max_size": base_max_size,  # Maximum size to buy
+                "base_increment": 1,  # Specifies the minimum increment for the base_asset.
+
+                "price_increment": min_price,  # TODO test this at market open
+
+                "min_price": min_price,
+                "max_price": max_price,
+            },
+            'market_order': {
+                "fractionable": fractionable,
+                "quote_increment": quote_increment,  # Specifies the min order price as well as the price increment.
+                "buy": {
+                    "min_funds": min_funds_buy,
+                    "max_funds": max_funds,
+                },
+                "sell": {
+                    "min_funds": min_funds_sell,
+                    "max_funds": max_funds,
+                },
+            },
+            "exchange_specific": {
+                "id": exchange_specific['id'],
+                "class": exchange_specific['class'],
+                "exchange": exchange_specific['exchange'],
+                "status": exchange_specific['status'],
+                "tradable": exchange_specific['tradable'],
+                "marginable": exchange_specific['marginable'],
+                "shortable": exchange_specific['shortable'],
+                "easy_to_borrow": exchange_specific['easy_to_borrow'],
+                "price": current_price
+            }
+        }
+
+    def get_price(self, symbol) -> float:
+        assert isinstance(self.calls, alpaca_trade_api.REST)
+        response = self.calls.get_last_trade(symbol=symbol)
+        return float(response['price'])
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py b/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py
new file mode 100644
index 00000000..4cf51875
--- /dev/null
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py
@@ -0,0 +1,241 @@
+"""
+    Alpaca websocket definitions
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+import collections
+import ssl
+import threading
+import time
+import traceback
+import warnings
+
+import msgpack
+from websocket import create_connection
+
+import blankly
+from blankly.exchanges.abc_exchange_websocket import ABCExchangeWebsocket
+from blankly.exchanges.auth.auth_constructor import load_auth
+from blankly.exchanges.interfaces.alpaca.alpaca_websocket_utils import parse_alpaca_timestamp, switch_type
+
+
+def create_ticker_connection(symbol, url, channel):
+    ws = create_connection(url, sslopt={"cert_reqs": ssl.CERT_NONE}, header={'Content-Type': 'application/msgpack'})
+    _, auth = load_auth('alpaca')
+
+    ws.send(msgpack.packb({
+        'action': 'auth',
+        'key': auth['API_KEY'],
+        'secret': auth['API_SECRET']
+    }))
+    # Check if the response is valid
+    response = msgpack.unpackb(ws.recv())
+    if response[0]['msg'] != 'connected':
+        warnings.warn("Connection failed.")
+
+    ws.send(
+        msgpack.packb({
+            'action': 'subscribe',
+            channel: tuple([symbol]),
+        })
+    )
+
+    response = msgpack.unpackb(ws.recv())
+    if response[0]['msg'] != 'authenticated':
+        warnings.warn("Authentication failed.")
+
+    return ws
+
+
+# List of valid subscriptions
+# 'trades': tuple(['AAPL']),
+# 'quotes': tuple(['AAPL']),
+# 'bars': tuple(['AAPL']),
+# 'dailyBars': tuple(['AAPL']),
+# 'statuses': tuple(['AAPL']),
+# 'lulds': tuple(['AAPL'])
+
+
+class Tickers(ABCExchangeWebsocket):
+    def __init__(self, symbol, stream, log=None,
+                 pre_event_callback=None, initially_stopped=False,
+                 WEBSOCKET_URL="wss://stream.data.alpaca.markets/v2/iex/"):
+        """
+        Create and initialize the ticker
+        Args:
+            symbol: Currency to initialize on such as "BTC-USD"
+            stream: Valid exchange stream to attach to, these are:
+                trades, quotes, bars, dailyBars, statuses, lulds
+            log: Fill this with a path to a log file that should be created
+            WEBSOCKET_URL: Default websocket URL feed.
+        """
+        self.__symbol = symbol
+        self.__stream = stream
+        self.__logging_callback, self.__interface_callback, log_message = switch_type(stream)
+
+        # Initialize log file
+        if log is not None:
+            self.__log = True
+            self.__filePath = log
+            try:
+                self.__file = open(log, 'x+')
+                self.__file.write(log_message)
+            except FileExistsError:
+                self.__file = open(log, 'a')
+        else:
+            self.__log = False
+
+        self.URL = WEBSOCKET_URL
+        self.ws = None
+        self.__response = None
+        self.__subscription_response = None
+        self.__most_recent_tick = None
+        self.__most_recent_time = None
+        self.__callbacks = []
+        self.__pre_event_callback = pre_event_callback
+
+        # Reload preferences
+        self.__preferences = blankly.utils.load_user_preferences()
+        buffer_size = self.__preferences["settings"]["websocket_buffer_size"]
+        self.__ticker_feed = collections.deque(maxlen=buffer_size)
+        self.__time_feed = collections.deque(maxlen=buffer_size)
+
+        # Start the websocket
+        if not initially_stopped:
+            self.start_websocket()
+
+    def start_websocket(self):
+        """
+        Restart websocket if it was asked to stop.
+        """
+        if self.ws is None:
+            self.ws = create_ticker_connection(self.__symbol, self.URL, self.__stream)
+            self.__response = self.ws.recv()
+            thread = threading.Thread(target=self.read_websocket)
+            thread.start()
+        else:
+            if self.ws.connected:
+                print("Already running...")
+                pass
+            else:
+                # Use recursion to restart, continue appending to time feed and ticker feed
+                self.ws = None
+                self.start_websocket()
+
+    def read_websocket(self):
+        counter = 0
+        # TODO port this to "WebSocketApp" found in the websockets documentation
+        while self.ws.connected:
+            # In case the user closes while its reading from the websocket, this will let it expire
+            persist_connected = self.ws.connected
+            try:
+                received = msgpack.unpackb(self.ws.recv())[0]  # type: dict
+                # Modify time to use epoch
+
+                if 't' in received:
+                    received['t'] = parse_alpaca_timestamp(received['t'])
+                    recent_time = received['t']
+                else:
+                    recent_time = time.time()
+
+                if self.__log:
+                    if counter % 100 == 0:
+                        self.__file.close()
+                        self.__file = open(self.__filePath, 'a')
+                    line = self.__logging_callback(received)
+                    self.__file.write(line)
+
+                # Manage price events and fire for each manager attached
+                interface_message = self.__interface_callback(received)
+
+                self.__most_recent_time = recent_time
+                self.__time_feed.append(self.__most_recent_time)
+                self.__most_recent_tick = interface_message
+                self.__ticker_feed.append(interface_message)
+
+                try:
+                    for i in self.__callbacks:
+                        i(interface_message)
+                except Exception:
+                    traceback.print_exc()
+
+                counter += 1
+            except Exception as e:
+                traceback.print_exc()
+                if persist_connected:
+                    pass
+                else:
+                    print("Error reading ticker websocket for " + self.__symbol + " on " +
+                          self.__stream + ": attempting to re-initialize")
+                    # Give a delay so this doesn't eat up from the main thread if it takes many tries to initialize
+                    time.sleep(2)
+                    self.ws.close()
+                    self.ws = create_ticker_connection(self.__symbol, self.URL, self.__stream)
+                    # Update response
+                    self.__response = self.ws.recv()
+
+    """ Required in manager """
+
+    def is_websocket_open(self):
+        return self.ws.connected
+
+    def get_currency_id(self):
+        return self.__symbol
+
+    """ Required in manager """
+
+    def append_callback(self, obj):
+        self.__callbacks.append(obj)
+
+    """ Define a variable each time so there is no array manipulation """
+    """ Required in manager """
+
+    def get_most_recent_tick(self):
+        return self.__most_recent_tick
+
+    """ Required in manager """
+
+    def get_most_recent_time(self):
+        return self.__most_recent_time
+
+    """ Required in manager """
+
+    def get_time_feed(self):
+        return list(self.__time_feed)
+
+    """ Parallel with time feed """
+    """ Required in manager """
+
+    def get_feed(self):
+        return list(self.__ticker_feed)
+
+    """ Required in manager """
+
+    def get_response(self):
+        return self.__response
+
+    """ Required in manager """
+
+    def close_websocket(self):
+        if self.ws.connected:
+            self.ws.close()
+        else:
+            print("Websocket for " + self.__symbol + ' on channel ' + self.__stream + " is already closed")
+
+    """ Required in manager """
+
+    def restart_ticker(self):
+        self.start_websocket()
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_websocket_utils.py b/blankly/exchanges/interfaces/alpaca/alpaca_websocket_utils.py
new file mode 100644
index 00000000..d16256eb
--- /dev/null
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_websocket_utils.py
@@ -0,0 +1,116 @@
+"""
+    Alpaca websocket homogenization definitions.
+    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 <https://www.gnu.org/licenses/>.
+"""
+from alpaca_trade_api.entity_v2 import trade_mapping_v2
+from msgpack.ext import Timestamp
+
+from blankly.utils.utils import isolate_specific, rename_to
+
+
+def parse_alpaca_timestamp(value: Timestamp):
+    return value.seconds + (value.nanoseconds * float(1e-9))
+
+
+def alpaca_remapping(dictionary: dict, mapping: dict):
+    # From https://stackoverflow.com/a/67573821/8087739
+    try:
+        dictionary.pop('T')
+    except KeyError:
+        pass
+    return {mapping[k]: dictionary[k] for k in dictionary}
+
+
+def switch_type(stream):
+    if stream == "trades":
+        return trades_logging, \
+               trades_interface, \
+               "id,symbol,conditions,exchange,price,size,timestamp,tape\n"
+    elif stream == "quotes":
+        return quotes_logging, \
+               no_callback, \
+               "symbol,ask_exchange,ask_price,ask_size,bid_exchange,bid_price,bid_size,conditions,timestamp,tape\n"
+    else:
+        return no_logging_callback, no_callback, ""
+
+
+def no_callback(message):
+    return message
+
+
+def no_logging_callback(message):
+    response = ""
+    for i in list(message.keys()):
+        response += str(message[i])
+        response += ','
+
+    response += '\n'
+    return response
+
+
+def trades_logging(message: dict):
+    return str(str(message["i"]) + "," +
+               str(message["S"]) + "," +
+               str(message["c"]) + "," +
+               str(message["x"]) + "," +
+               str(message["p"]) + "," +
+               str(message["s"]) + "," +
+               str(message["t"]) + "," +
+               str(message["z"]) + "\n")
+
+
+def trades_interface(message):
+    """
+    Similar ticks with coinbase pro
+    {
+        'type': 'ticker',
+        'symbol': 'BTC-USD',
+        'price': '50141.55',
+        'time': 1619286924.397969,
+        'trade_id': 160775277,
+        'size': .35324
+    }
+    """
+    message = alpaca_remapping(message, trade_mapping_v2)
+    message['time'] = message.pop('timestamp')
+
+    renames = [
+        ["id", "trade_id"],
+    ]
+
+    message = rename_to(renames, message)
+
+    needed = [
+        ["symbol", str],
+        ["price", float],
+        ["time", float],
+        ["trade_id", int],
+        ["size", float]
+    ]
+    return isolate_specific(needed, message)
+
+
+def quotes_logging(message):
+    return str(str(message["S"]) + "," +
+               str(message["ax"]) + "," +
+               str(message["ap"]) + "," +
+               str(message["as"]) + "," +
+               str(message["bx"]) + "," +
+               str(message["bp"]) + "," +
+               str(message["bs"]) + "," +
+               str(message["c"]) + "," +
+               str(message["t"]) + "," +
+               str(message["z"]) + "\n")
diff --git a/Blankly/exchanges/Binance/__init__.py b/blankly/exchanges/interfaces/binance/__init__.py
similarity index 100%
rename from Blankly/exchanges/Binance/__init__.py
rename to blankly/exchanges/interfaces/binance/__init__.py
diff --git a/blankly/exchanges/interfaces/binance/binance.py b/blankly/exchanges/interfaces/binance/binance.py
new file mode 100644
index 00000000..7e4f10ae
--- /dev/null
+++ b/blankly/exchanges/interfaces/binance/binance.py
@@ -0,0 +1,50 @@
+"""
+    binance 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 <https://www.gnu.org/licenses/>.
+"""
+
+from binance.client import Client
+
+from blankly.exchanges.exchange import Exchange
+from blankly.utils import utils
+
+
+class Binance(Exchange):
+    def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
+        Exchange.__init__(self, "binance", portfolio_name, keys_path, settings_path)
+
+    """
+    Builds information about the symbol on this exchange by making particular API calls
+    """
+
+    def get_asset_state(self, symbol):
+        """
+        Portfolio state is the internal properties for the exchange block.
+        """
+        # TODO Populate this with useful information
+        symbol = utils.get_base_asset(symbol)
+        account = self.interface.get_account(symbol=symbol)
+        return account
+
+    def get_exchange_state(self):
+        """
+        Exchange state is the external properties for the exchange block
+        """
+        # TODO Populate this with useful information
+        return self.interface.get_fees()
+
+    def get_direct_calls(self) -> Client:
+        return self.calls
diff --git a/Blankly/exchanges/Binance/Binance_API.py b/blankly/exchanges/interfaces/binance/binance_api.py
similarity index 97%
rename from Blankly/exchanges/Binance/Binance_API.py
rename to blankly/exchanges/interfaces/binance/binance_api.py
index 15b5acda..a69e15d0 100644
--- a/Blankly/exchanges/Binance/Binance_API.py
+++ b/blankly/exchanges/interfaces/binance/binance_api.py
@@ -1,5 +1,5 @@
 """
-    Unused Binance API calls.
+    Unused binance API calls.
     Copyright (C) 2021  Emerson Dove
 
     This program is free software: you can redistribute it and/or modify
@@ -17,14 +17,17 @@
 """
 import hashlib
 import hmac
-import requests
 import time
-from urllib.parse import urlencode
-from requests.auth import AuthBase
 from collections import OrderedDict
+from urllib.parse import urlencode
 
+import requests
+from requests.auth import AuthBase
 
 # Create custom authentication for Exchange
+from blankly.exchanges.interfaces.binance.binance_auth import BinanceAuth
+
+
 class BinanceExchangeAuth(AuthBase):
     def __init__(self, api_key):
         self.api_key = api_key
@@ -46,10 +49,11 @@ class API:
     API_URL = 'https://api.binance.{}/api'
     API_TESTNET_URL = 'https://testnet.binance.vision/api'
 
-    def __init__(self, API_KEY, API_SECRET, tld='.us', testnet=False):
-        self.api_key = API_KEY
-        self.secret_key = API_SECRET
-        self.__auth = BinanceExchangeAuth(API_KEY)
+    def __init__(self, auth: BinanceAuth, tld: str = '.us', testnet: bool = False):
+        self.api_key = auth.keys['API_KEY']
+        self.secret_key = auth.keys['API_SECRET']
+
+        self.__auth = BinanceExchangeAuth(self.api_key)
         if not testnet:
             self.__api_url = self.API_URL.format(tld)
         else:
@@ -90,6 +94,7 @@ def _send_request(self, method, url, signed, params=None, data=None):
     """
     Wallet Endpoints
     """
+
     def system_status(self):
         endpoint = '/wapi/v3/systemStatus.html'
         signed = False
@@ -207,6 +212,7 @@ def query_universal_transfer(self, type, **optional_params):
     """
     Market Data Endpoints
     """
+
     def test_connectivity(self):
         endpoint = '/api/v3/ping'
         signed = False
@@ -292,6 +298,7 @@ def symbol_orderbook_ticker(self, **optional_params):
     """
     spot account/trade endpoints
     """
+
     def test_new_order(self, symbol, side, type, **optional_params):
         endpoint = '/api/v3/order/test'
         signed = True
@@ -393,7 +400,7 @@ def account_information(self):
         signed = True
         return self._send_request('get', self.__api_url + endpoint, signed)
 
-    def account_trade_list(self, symbol,  **optional_params):
+    def account_trade_list(self, symbol, **optional_params):
         endpoint = '/api/v3/myTrades'
         signed = True
         params = OrderedDict()
diff --git a/blankly/exchanges/interfaces/binance/binance_auth.py b/blankly/exchanges/interfaces/binance/binance_auth.py
new file mode 100644
index 00000000..e6236589
--- /dev/null
+++ b/blankly/exchanges/interfaces/binance/binance_auth.py
@@ -0,0 +1,26 @@
+"""
+    binance authentication base class
+    Copyright (C) 2021  Arun Annamalai, 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 <https://www.gnu.org/licenses/>.
+"""
+
+from blankly.exchanges.auth.abc_auth import ABCAuth
+
+
+class BinanceAuth(ABCAuth):
+    def __init__(self, keys_file, portfolio_name):
+        super().__init__(keys_file, portfolio_name, 'binance')
+        needed_keys = ['API_KEY', 'API_SECRET']
+        self.validate_credentials(needed_keys)
diff --git a/Blankly/exchanges/Binance/Binance_Interface.py b/blankly/exchanges/interfaces/binance/binance_interface.py
similarity index 76%
rename from Blankly/exchanges/Binance/Binance_Interface.py
rename to blankly/exchanges/interfaces/binance/binance_interface.py
index 290d21d3..40912411 100644
--- a/Blankly/exchanges/Binance/Binance_Interface.py
+++ b/blankly/exchanges/interfaces/binance/binance_interface.py
@@ -21,15 +21,15 @@
 import binance.exceptions
 import pandas as pd
 
-import Blankly.utils.exceptions as exceptions
-import Blankly.utils.utils
-import Blankly.utils.utils as utils
-from Blankly.utils.purchases.limit_order import LimitOrder
-from Blankly.utils.purchases.market_order import MarketOrder
-from Blankly.interface.currency_Interface import CurrencyInterface
+import blankly.utils.exceptions as exceptions
+import blankly.utils.utils
+import blankly.utils.utils as utils
+from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
+from blankly.exchanges.orders.limit_order import LimitOrder
+from blankly.exchanges.orders.market_order import MarketOrder
 
 
-class BinanceInterface(CurrencyInterface):
+class BinanceInterface(ExchangeInterface):
     def __init__(self, exchange_name, authenticated_API):
         super().__init__(exchange_name, authenticated_API)
 
@@ -60,12 +60,7 @@ def init_exchange(self):
         self.__available_currencies = filtered_base_assets
 
     def get_products(self):
-        needed = [["currency_id", str],
-                  ["base_currency", str],
-                  ["quote_currency", str],
-                  ["base_min_size", float],
-                  ["base_max_size", float],
-                  ["base_increment", float]]
+        needed = self.needed['get_products']
         """
         This is a section of the symbols array
         [
@@ -133,9 +128,9 @@ def get_products(self):
         products = self.calls.get_exchange_info()["symbols"]
         for i in range(len(products)):
             # Rename needed
-            products[i]["currency_id"] = products[i]["baseAsset"] + "-" + products[i]["quoteAsset"]
-            products[i]["base_currency"] = products[i].pop("baseAsset")
-            products[i]["quote_currency"] = products[i].pop("quoteAsset")
+            products[i]["symbol"] = products[i]["baseAsset"] + "-" + products[i]["quoteAsset"]
+            products[i]["base_asset"] = products[i].pop("baseAsset")
+            products[i]["quote_asset"] = products[i].pop("quoteAsset")
             filters = products[i]["filters"]
             # Iterate to find the next few
             min_qty = None
@@ -154,17 +149,17 @@ def get_products(self):
             products[i] = utils.isolate_specific(needed, products[i])
         return products
 
-    def get_account(self, currency=None):
+    def get_account(self, symbol=None) -> utils.AttributeDict:
         """
-        Get all currencies in an account, or sort by currency/account_id
+        Get all currencies in an account, or sort by asset/account_id
         Args:
-            currency (Optional): Filter by particular currency
+            symbol (Optional): Filter by particular asset
 
             These arguments are mutually exclusive
         Coinbase Pro: get_account
-        Binance: get_account["balances"]
+        binance: get_account["balances"]
         """
-        currency = super().get_account(currency=currency)
+        symbol = super().get_account(symbol=symbol)
 
         needed = self.needed['get_account']
 
@@ -193,67 +188,71 @@ def get_account(self, currency=None):
         }
         """
         renames = [
-            ["asset", "currency"],
+            ["asset", "symbol"],
             ["free", "available"],
             ["locked", "hold"],
         ]
 
         accounts = self.calls.get_account()["balances"]
 
-        # Isolate for currency, warn if not found or default to just returning a parsed version
-        if currency is not None:
-            if currency in self.__available_currencies:
+        # Isolate for symbol, warn if not found or default to just returning a parsed version
+        if symbol is not None:
+            if symbol in self.__available_currencies:
                 for i in range(len(accounts)):
                     # If it was in the accounts return we can just isolate & parse
-                    if accounts[i]["asset"] == currency:
+                    if accounts[i]["asset"] == symbol:
                         accounts = utils.rename_to(renames, accounts[i])
                         parsed_value = utils.isolate_specific(needed, accounts)
-                        return parsed_value
-                # If not just return a default 0 value. This is safe because we already checked if the currency
+                        return utils.AttributeDict({
+                            'available': parsed_value['available'],
+                            'hold': parsed_value['hold']
+                        })
+                # If not just return a default 0 value. This is safe because we already checked if the symbol
                 #  was valid
-                return {
-                    "currency": currency,
+                return utils.AttributeDict({
                     "available": 0.0,
                     "hold": 0.0
-                }
+                })
             else:
-                raise LookupError("Currency not found")
+                raise LookupError("Symbol was not found")
 
-        # Binance only returns things you own, so extract those - scan and add that to the array.
+        # binance only returns things you own, so extract those - scan and add that to the array.
         # We can fill it to a balance of zero later
         owned_assets = []
         for i in accounts:
             owned_assets.append(i['asset'])
-        # Fill this list for return
-        filled_dict_list = []
+        # Create an empty list for return
+        parsed_dictionary = {}
         # Iterate through
         for i in self.__available_currencies:
             # Iterate through everything binance returned
             found = False
-            for index, val in enumerate(accounts):
-                # If the current available currency matches one from binance
+            for val in accounts:
+                # If the current available asset matches one from binance
                 if val['asset'] == i:
                     # Do the normal thing above and append
                     mutated = utils.rename_to(renames, val)
                     mutated = utils.isolate_specific(needed, mutated)
-                    filled_dict_list.append(mutated)
+                    parsed_dictionary[mutated['symbol']] = utils.AttributeDict({
+                        'available': mutated['available'],
+                        'hold': mutated['hold']
+                    })
                     found = True
             # If it wasn't found just default here
             if not found:
-                filled_dict_list.append({
-                    "currency": i,
-                    "available": 0.0,
-                    "hold": 0.0
+                parsed_dictionary[i] = utils.AttributeDict({
+                    'available': 0.0,
+                    'hold': 0.0
                 })
-        return filled_dict_list
+        return utils.AttributeDict(parsed_dictionary)
 
-    def market_order(self, product_id, side, funds) -> MarketOrder:
+    def market_order(self, symbol, side, funds) -> MarketOrder:
         """
         Used for buying or selling market orders
         Args:
-            product_id: currency to buy
+            symbol: asset to buy
             side: buy/sell
-            funds: desired amount of quote currency to use
+            funds: desired amount of quote asset to use
         """
         needed = self.needed['market_order']
         """
@@ -302,7 +301,6 @@ def market_order(self, product_id, side, funds) -> MarketOrder:
         }
         """
         renames = [
-            ["symbol", "product_id"],
             ["orderId", "id"],
             ["transactTime", "created_at"],
             ["origQty", "size"],
@@ -312,26 +310,27 @@ def market_order(self, product_id, side, funds) -> MarketOrder:
         order = {
             'funds': funds,
             'side': side,
-            'product_id': product_id,
+            'symbol': symbol,
             'type': 'market'
         }
-        modified_product_id = utils.to_exchange_coin_id(product_id, "binance")
+        modified_symbol = utils.to_exchange_coin_id(symbol, "binance")
         # The interface here will be the query of order status from this object, because orders are dynamic
         # creatures
-        response = self.calls.order_market(symbol=modified_product_id, side=side, quoteOrderQty=funds)
+        response = self.calls.order_market(symbol=modified_symbol, side=side, quoteOrderQty=funds)
         response['side'] = response['side'].lower()
         response['type'] = response['type'].lower()
-        response['status'] = response['status'].lower()
+        response['status'] = super().homogenize_order_status('binance', response['status'].lower())
         response["transactTime"] = response["transactTime"] / 1000
+        response['symbol'] = utils.to_blankly_coin_id(response['symbol'], 'binance')
         response = utils.rename_to(renames, response)
         response = utils.isolate_specific(needed, response)
         return MarketOrder(order, response, self)
 
-    def limit_order(self, product_id, side, price, size) -> LimitOrder:
+    def limit_order(self, symbol, side, price, size) -> LimitOrder:
         """
         Used for buying or selling limit orders
         Args:
-            product_id: currency to buy
+            symbol: currency to buy
             side: buy/sell
             price: price to set limit order
             size: amount of currency (like BTC) for the limit to be valued
@@ -373,23 +372,26 @@ def limit_order(self, product_id, side, price, size) -> LimitOrder:
             'size': size,
             'side': side,
             'price': price,
-            'product_id': product_id,
+            'symbol': symbol,
             'type': 'limit'
         }
-        modified_product_id = utils.to_exchange_coin_id(product_id, 'binance')
-        response = self.calls.order_limit(symbol=modified_product_id, side=side, price=price, quantity=size)
+        modified_symbol = utils.to_exchange_coin_id(symbol, 'binance')
+        response = self.calls.order_limit(symbol=modified_symbol, side=side, price=price, quantity=size)
         renames = [
-            ["symbol", "product_id"],
             ["orderId", "id"],
             ["transactTime", "created_at"],
             ["origQty", "size"],
             ["timeInForce", "time_in_force"],
         ]
+        response['side'] = response['side'].lower()
+        response['type'] = response['type'].lower()
+        response['status'] = super().homogenize_order_status('binance', response['status'].lower())
+        response['symbol'] = utils.to_blankly_coin_id(response['symbol'], 'binance')
         response = utils.rename_to(renames, response)
         response = utils.isolate_specific(needed, response)
         return LimitOrder(order, response, self)
 
-    def cancel_order(self, currency_id, order_id) -> dict:
+    def cancel_order(self, symbol, order_id) -> dict:
         """Cancel an active order. Either orderId or origClientOrderId must be sent.
 
         https://github.com/binance/binance-spot-api-docs/blob/master/rest-api.md#cancel-order-trade
@@ -421,11 +423,12 @@ def cancel_order(self, currency_id, order_id) -> dict:
         renames = [
             ["orderId", "order_id"]
         ]
-        response = self.calls.cancel_order(symbol=currency_id, orderId=order_id)
+        modified_symbol = utils.to_exchange_coin_id(symbol, 'binance')
+        response = self.calls.cancel_order(symbol=modified_symbol, orderId=order_id)
         response = utils.rename_to(renames, response)
         return utils.isolate_specific(needed, response)
 
-    def get_open_orders(self, product_id=None):
+    def get_open_orders(self, symbol=None):
         """
         List open orders.
         """
@@ -456,14 +459,13 @@ def get_open_orders(self, product_id=None):
         renames = [
             ["orderId", "id"],
             ["origQty", "size"],
-            ["symbol", "product_id"],
             ["cummulativeQuoteQty", "funds"],
             ["time", "created_at"]
         ]
 
-        if product_id is not None:
-            product_id = utils.to_exchange_coin_id(product_id, "binance")
-            orders = self.calls.get_open_orders(symbol=product_id)
+        if symbol is not None:
+            symbol = utils.to_exchange_coin_id(symbol, "binance")
+            orders = self.calls.get_open_orders(symbol=symbol)
         else:
             orders = self.calls.get_open_orders()
 
@@ -471,26 +473,16 @@ def get_open_orders(self, product_id=None):
             orders[i] = utils.rename_to(renames, orders[i])
             orders[i]['type'] = orders[i]['type'].lower()
             orders[i]['side'] = orders[i]['side'].lower()
-            orders[i]['status'] = orders[i]['status'].lower()
+            orders[i]['status'] = super().homogenize_order_status('binance', orders[i]['status'].lower())
             orders[i]['created_at'] = orders[i]['created_at'] / 1000
-            orders[i]['product_id'] = utils.to_blankly_coin_id(orders[i]['product_id'], 'binance')
             needed = self.choose_order_specificity(orders[i]['type'])
             orders[i] = utils.isolate_specific(needed, orders[i])
-            orders[i]['product_id'] = utils.to_blankly_coin_id(orders[i]['product_id'], 'binance', quote_guess=None)
+            orders[i]['symbol'] = utils.to_blankly_coin_id(orders[i]['symbol'], 'binance', quote_guess=None)
 
         return orders
 
-    def get_order(self, currency_id, order_id) -> dict:
+    def get_order(self, symbol, order_id) -> dict:
         """
-        :param symbol: required
-        :type symbol: str
-        :param orderId: The unique order id
-        :type orderId: int
-        :param origClientOrderId: optional
-        :type origClientOrderId: str
-        :param recvWindow: the number of milliseconds the request is valid for
-        :type recvWindow: int
-
         {
             "symbol": "LTCBTC",
             "orderId": 1,
@@ -510,20 +502,20 @@ def get_order(self, currency_id, order_id) -> dict:
         renames = [
             ["orderId", "id"],
             ["origQty", "size"],
-            ["symbol", "product_id"],
             ["cummulativeQuoteQty", "funds"],
-            ["time", "created_at"]
+            ["time", "created_at"],
+            ["timeInForce", "time_in_force"]
         ]
 
-        currency_id = utils.to_exchange_coin_id(currency_id, 'binance')
-        response = self.calls.get_order(symbol=currency_id, orderId=int(order_id))
+        symbol = utils.to_exchange_coin_id(symbol, 'binance')
+        response = self.calls.get_order(symbol=symbol, orderId=int(order_id))
 
         response = utils.rename_to(renames, response)
         response['type'] = response['type'].lower()
         response['side'] = response['side'].lower()
-        response['status'] = response['status'].lower()
-        response['created_at'] = response['created_at']/1000
-        response['product_id'] = utils.to_blankly_coin_id(response['product_id'], 'binance')
+        response['status'] = super().homogenize_order_status('binance', response['status'].lower())
+        response['created_at'] = response['created_at'] / 1000
+        response['symbol'] = utils.to_blankly_coin_id(response['symbol'], 'binance')
         needed = self.choose_order_specificity(response['type'])
         response = utils.isolate_specific(needed, response)
 
@@ -531,7 +523,7 @@ def get_order(self, currency_id, order_id) -> dict:
 
     """
     Coinbase Pro: get_fees
-    Binance: get_trade_fee
+    binance: get_trade_fee
     """
 
     def get_fees(self) -> dict:
@@ -559,7 +551,7 @@ def get_fees(self) -> dict:
             ]
         }
         """
-        # TODO: make sure this supports with Binance Coin (BNB) discount
+        # TODO: make sure this supports with binance Coin (BNB) discount
         account = self.calls.get_account()
         # Get rid of the stuff we really don't need this time
         account.pop('canTrade')
@@ -574,34 +566,39 @@ def get_fees(self) -> dict:
 
     """
     Coinbase Pro: get_account_history
-    Binance:
+    binance:
         get_deposit_history
         get_withdraw_history
 
     """
 
-    def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
+    def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
         """
         Returns the product history from an exchange
         Args:
-            product_id: Blankly product ID format (BTC-USD)
+            symbol: blankly product ID format (BTC-USD)
             epoch_start: Time to begin download
             epoch_stop: Time to stop download
-            granularity: Resolution in seconds between tick (ex: 60 = 1 per minute)
+            resolution: Resolution in seconds between tick (ex: 60 = 1 per minute)
         Returns:
             Dataframe with *at least* 'time (epoch)', 'low', 'high', 'open', 'close', 'volume' as columns.
         """
-        epoch_start, epoch_stop = super().get_product_history(product_id, epoch_start, epoch_stop, granularity)
+
+        resolution = blankly.time_builder.time_interval_to_seconds(resolution)
+
+        # epoch_start, epoch_stop = super().get_product_history(symbol, epoch_start, epoch_stop, resolution)
+        epoch_start = utils.convert_epochs(epoch_start)
+        epoch_stop = utils.convert_epochs(epoch_stop)
 
         epoch_start = int(epoch_start)
         epoch_stop = int(epoch_stop)
 
         accepted_grans = [60, 180, 300, 900, 1800, 3600, 7200, 14400,
                           21600, 28800, 43200, 86400, 259200, 604800, 2592000]
-        if granularity not in accepted_grans:
+        if resolution not in accepted_grans:
             warnings.warn("Granularity is not an accepted granularity...rounding to nearest valid value.")
-            granularity = accepted_grans[min(range(len(accepted_grans)),
-                                             key=lambda i: abs(accepted_grans[i] - granularity))]
+            resolution = accepted_grans[min(range(len(accepted_grans)),
+                                            key=lambda i: abs(accepted_grans[i] - resolution))]
         lookup_dict = {
             60: "1m",
             180: "3m",
@@ -619,20 +616,20 @@ def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
             604800: "1w",
             2592000: "1M"
         }
-        gran_string = lookup_dict[granularity]
+        gran_string = lookup_dict[resolution]
 
         # Figure out how many points are needed
-        need = int((epoch_stop - epoch_start) / granularity)
+        need = int((epoch_stop - epoch_start) / resolution)
         initial_need = need
         window_open = epoch_start
         history = []
 
         # Convert coin id to binance coin
-        product_id = utils.to_exchange_coin_id(product_id, 'binance')
+        symbol = utils.to_exchange_coin_id(symbol, 'binance')
         while need > 1000:
             # Close is always 300 points ahead
-            window_close = int(window_open + 1000 * granularity)
-            history = history + self.calls.get_klines(symbol=product_id, startTime=window_open * 1000,
+            window_close = int(window_open + 1000 * resolution)
+            history = history + self.calls.get_klines(symbol=symbol, startTime=window_open * 1000,
                                                       endTime=window_close * 1000, interval=gran_string,
                                                       limit=1000)
 
@@ -642,7 +639,7 @@ def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
             utils.update_progress((initial_need - need) / initial_need)
 
         # Fill the remainder
-        history_block = history + self.calls.get_klines(symbol=product_id, startTime=window_open * 1000,
+        history_block = history + self.calls.get_klines(symbol=symbol, startTime=window_open * 1000,
                                                         endTime=epoch_stop * 1000, interval=gran_string,
                                                         limit=1000)
 
@@ -669,17 +666,15 @@ def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
                                         })
 
         # Convert time to seconds
-        data_frame['time'] = data_frame['time'].div(1000)
-
+        data_frame['time'] = data_frame['time'].div(1000).astype(int)
         return data_frame.reindex(columns=['time', 'low', 'high', 'open', 'close', 'volume'])
 
     """
     Coinbase Pro: Get Currencies
-    Binance: get_products
+    binance: get_products
     """
 
-    def get_market_limits(self, product_id):
-        needed = self.needed['get_market_limits']
+    def get_order_filter(self, symbol):
         """
         Optimally we'll just remove the filter section and make the returns accurate
         {
@@ -743,7 +738,7 @@ def get_market_limits(self, product_id):
         },
         """
 
-        converted_symbol = utils.to_exchange_coin_id(product_id, 'binance')
+        converted_symbol = utils.to_exchange_coin_id(symbol, 'binance')
         current_price = None
         symbol_data = self.calls.get_exchange_info()["symbols"]
         for i in symbol_data:
@@ -759,43 +754,63 @@ def get_market_limits(self, product_id):
         hard_max_price = float(filters[0]["maxPrice"])
         quote_increment = float(filters[0]["tickSize"])
 
-        percent_min_price = float(filters[1]["multiplierUp"]) * current_price
-        percent_max_price = float(filters[1]["multiplierDown"]) * current_price
+        percent_min_price = float(filters[1]["multiplierDown"]) * current_price
+        percent_max_price = float(filters[1]["multiplierUp"]) * current_price
 
         min_quantity = float(filters[2]["minQty"])
         max_quantity = float(filters[2]["maxQty"])
         base_increment = float(filters[2]["stepSize"])
 
+        min_market_notational = float(filters[3]['minNotional'])
+        max_market_notational = 92233720368.547752  # For some reason equal to the first *11 digits* of 2^63 then
+        # it gets weird past the decimal
+
         max_orders = int(filters[6]["maxNumOrders"])
 
-        if hard_min_price < percent_min_price:
-            min_price = percent_min_price
-        else:
+        if percent_min_price < hard_min_price:
             min_price = hard_min_price
-
-        if hard_max_price > percent_max_price:
-            max_price = percent_max_price
         else:
+            min_price = percent_min_price
+
+        if percent_max_price > hard_max_price:
             max_price = hard_max_price
+        else:
+            max_price = percent_max_price
 
         return {
-            "market": product_id,
-            "base_currency": symbol_data["baseAsset"],
-            "quote_currency": symbol_data["quoteAsset"],
-            "base_min_size": min_quantity,  # Minimum size to buy
-            "base_max_size": max_quantity,  # Maximum size to buy
-            "quote_increment": quote_increment,  # Specifies the min order price as well as the price increment.
-            "base_increment": base_increment,  # Specifies the minimum increment for the base_currency.
+            "symbol": symbol,
+            "base_asset": symbol_data["baseAsset"],
+            "quote_asset": symbol_data["quoteAsset"],
             "max_orders": max_orders,
-            "min_price": min_price,
-            "max_price": max_price,
+            "limit_order": {
+                "base_min_size": min_quantity,  # Minimum size to buy
+                "base_max_size": max_quantity,  # Maximum size to buy
+                "base_increment": base_increment,  # Specifies the minimum increment for the base_asset.
+
+                "price_increment": quote_increment,
+
+                "min_price": min_price,
+                "max_price": max_price,
+            },
+            'market_order': {
+                "fractionable": True,
+                "quote_increment": quote_increment,  # Specifies the min order price as well as the price increment.
+                "buy": {
+                    "min_funds": min_market_notational,
+                    "max_funds": max_market_notational,
+                },
+                "sell": {
+                    "min_funds": min_market_notational,
+                    "max_funds": max_market_notational,
+                },
+            },
             "exchange_specific": {}
         }
 
-    def get_price(self, currency_pair) -> float:
+    def get_price(self, symbol) -> float:
         """
-        Returns just the price of a currency pair.
+        Returns just the price of an asset.
         """
-        currency_pair = utils.to_exchange_coin_id(currency_pair, "binance")
-        response = self.calls.get_symbol_ticker(symbol=currency_pair)
+        symbol = utils.to_exchange_coin_id(symbol, "binance")
+        response = self.calls.get_symbol_ticker(symbol=symbol)
         return float(response['price'])
diff --git a/Blankly/exchanges/Binance/Binance_Websocket.py b/blankly/exchanges/interfaces/binance/binance_websocket.py
similarity index 89%
rename from Blankly/exchanges/Binance/Binance_Websocket.py
rename to blankly/exchanges/interfaces/binance/binance_websocket.py
index 85e02c9c..8d37d18b 100644
--- a/Blankly/exchanges/Binance/Binance_Websocket.py
+++ b/blankly/exchanges/interfaces/binance/binance_websocket.py
@@ -1,5 +1,5 @@
 """
-    Binance ticker class.
+    binance ticker class.
     Copyright (C) 2021  Emerson Dove
 
     This program is free software: you can redistribute it and/or modify
@@ -16,18 +16,21 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-import websocket
-from Blankly.exchanges.IExchange_Websocket import IExchangeWebsocket
-import Blankly.exchanges.Binance.websocket_utils as websocket_utils
 import collections
 import json
-import Blankly
 import threading
 import traceback
 
+import websocket
+
+import blankly
+import blankly.exchanges.interfaces.binance.binance_websocket_utils as websocket_utils
+from blankly.exchanges.abc_exchange_websocket import ABCExchangeWebsocket
+
 
-class Tickers(IExchangeWebsocket):
-    def __init__(self, symbol, stream, log=None, WEBSOCKET_URL="wss://stream.binance.com:9443/ws"):
+class Tickers(ABCExchangeWebsocket):
+    def __init__(self, symbol, stream, log=None, initially_stopped=False,
+                 WEBSOCKET_URL="wss://stream.binance.com:9443/ws"):
         """
         Create and initialize the ticker
         Args:
@@ -64,13 +67,14 @@ def __init__(self, symbol, stream, log=None, WEBSOCKET_URL="wss://stream.binance
         self.__message_count = 0
 
         # Reload preferences
-        self.__preferences = Blankly.utils.load_user_preferences()
+        self.__preferences = blankly.utils.load_user_preferences()
         buffer_size = self.__preferences["settings"]["websocket_buffer_size"]
         self.__ticker_feed = collections.deque(maxlen=buffer_size)
         self.__time_feed = collections.deque(maxlen=buffer_size)
 
         # Start the websocket
-        self.start_websocket()
+        if not initially_stopped:
+            self.start_websocket()
 
     def start_websocket(self):
         """
@@ -118,12 +122,18 @@ def on_message(self, ws, message):
             for i in self.__callbacks:
                 i(interface_message)
         except KeyError:
+            # If the try below figures this out then we don't have to traceback
+            error_found = False
             try:
                 if message['result'] is None:
                     self.__response = message
+                    error_found = True
             except KeyError:
                 traceback.print_exc()
 
+            if not error_found:
+                traceback.print_exc()
+
     def on_error(self, ws, error):
         print(error)
 
@@ -144,6 +154,7 @@ def on_open(self, ws):
         ws.send(request)
 
     """ Required in manager """
+
     def is_websocket_open(self):
         return self.__thread.is_alive()
 
@@ -151,32 +162,39 @@ def get_currency_id(self):
         return self.__id
 
     """ Required in manager """
+
     def append_callback(self, obj):
         self.__callbacks.append(obj)
 
     """ Define a variable each time so there is no array manipulation """
     """ Required in manager """
+
     def get_most_recent_tick(self):
         return self.__most_recent_tick
 
     """ Required in manager """
+
     def get_most_recent_time(self):
         return self.__most_recent_time
 
     """ Required in manager """
+
     def get_time_feed(self):
         return list(self.__time_feed)
 
     """ Parallel with time feed """
     """ Required in manager """
+
     def get_feed(self):
         return list(self.__ticker_feed)
 
     """ Required in manager """
+
     def get_response(self):
         return self.__response
 
     """ Required in manager """
+
     def close_websocket(self):
         if self.__thread.is_alive():
             self.ws.close()
@@ -184,5 +202,6 @@ def close_websocket(self):
             print("Websocket for " + self.__id + '@' + self.__stream + " is already closed")
 
     """ Required in manager """
+
     def restart_ticker(self):
         self.start_websocket()
diff --git a/Blankly/exchanges/Binance/websocket_utils.py b/blankly/exchanges/interfaces/binance/binance_websocket_utils.py
similarity index 83%
rename from Blankly/exchanges/Binance/websocket_utils.py
rename to blankly/exchanges/interfaces/binance/binance_websocket_utils.py
index 7beb7f0a..f34aa463 100644
--- a/Blankly/exchanges/Binance/websocket_utils.py
+++ b/blankly/exchanges/interfaces/binance/binance_websocket_utils.py
@@ -1,5 +1,5 @@
 """
-    Websocket feeds need to be modular due to the subscription methods, this provides dynamic management for Binance.
+    Websocket feeds need to be modular due to the subscription methods, this provides dynamic management for binance.
     Copyright (C) 2021  Emerson Dove
 
     This program is free software: you can redistribute it and/or modify
@@ -17,19 +17,20 @@
 """
 
 import time
-import Blankly.utils.utils as utils
+
+import blankly.utils.utils as utils
 
 
 def switch_type(stream):
     if stream == "trade":
         return trade, \
                trade_interface, \
-               "event_time,system_time,event_type,symbol,trade_id,price,quantity,buyer_order_id,seller_order_id,"\
+               "event_time,system_time,event_type,symbol,trade_id,price,quantity,buyer_order_id,seller_order_id," \
                "trade_time,buyer_is_maker\n"
     elif stream == "depth":
         return depth, depth_interface, ""
     else:
-        return no_callback, no_callback
+        return no_callback, no_callback, ""
 
 
 def no_callback(message):
@@ -53,7 +54,7 @@ def trade(message):
 def trade_interface(message):
     """
     Homogenizing with coinbase's return:
-    {'type': 'ticker', 'sequence': 24587251151, 'product_id': 'BTC-USD', 'price': '56178.52', 'open_24h': '56881.78',
+    {'type': 'ticker', 'sequence': 24587251151, 'symbol': 'BTC-USD', 'price': '56178.52', 'open_24h': '56881.78',
     'volume_24h': '17606.23228984', 'low_24h': '55288', 'high_24h': '58400', 'volume_30d': '506611.70878868',
     'best_bid': '56171.12', 'best_ask': '56178.53', 'side': 'sell', 'time': 1620331254.43236, 'trade_id': 165659167,
     'last_size': '0.04'}
@@ -76,25 +77,28 @@ def trade_interface(message):
     Similar ticks with coinbase pro
     {
         'type': 'ticker',
-        'product_id': 'BTC-USD',
+        'symbol': 'BTC-USD',
         'price': '50141.55',
         'time': 1619286924.397969,
         'trade_id': 160775277,
+        'size': .3432
     }
     """
     renames = [
         ["e", "type"],
-        ["s", "product_id"],
+        ["s", "symbol"],
         ["p", "price"],
         ["T", "time"],
-        ["t", "trade_id"],
+        ["t", "symbol"],
+        ['q', "size"]
     ]
     message = utils.rename_to(renames, message)
     needed = [
-        ["product_id", str],
+        ["symbol", str],
         ["price", float],
-        ["time", int],
-        ["trade_id", int]
+        ["time", float],
+        ["trade_id", int],
+        ["size", float]
     ]
-    message["time"] = message["time"]/1000
+    message["time"] = message["time"] / 1000
     return utils.isolate_specific(needed, message)
diff --git a/Blankly/exchanges/Coinbase_Pro/__init__.py b/blankly/exchanges/interfaces/coinbase_pro/__init__.py
similarity index 100%
rename from Blankly/exchanges/Coinbase_Pro/__init__.py
rename to blankly/exchanges/interfaces/coinbase_pro/__init__.py
diff --git a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
similarity index 66%
rename from Blankly/exchanges/Coinbase_Pro/Coinbase_Pro.py
rename to blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
index 581f1a41..c97d981c 100644
--- a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
@@ -16,56 +16,43 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-from Blankly.exchanges.exchange import Exchange
-import Blankly.auth_constructor
-from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_API import API as Coinbase_Pro_API
+from blankly.exchanges.exchange import Exchange
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_api import API as CoinbaseProAPI
 
 
-class Coinbase_Pro(Exchange):
+class CoinbasePro(Exchange):
     def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
-        # Load the auth from the keys file
-        auth, defined_name = Blankly.auth_constructor.load_auth_coinbase_pro(keys_path, portfolio_name)
-
         # Giving the preferences path as none allows us to create a default
-        Exchange.__init__(self, "coinbase_pro", defined_name, settings_path)
-
-        if self.preferences["settings"]["use_sandbox"]:
-            self.__calls = Coinbase_Pro_API(auth[0], auth[1], auth[2],
-                                            API_URL="https://api-public.sandbox.pro.coinbase.com/")
-        else:
-            # Create the authenticated object
-            self.__calls = Coinbase_Pro_API(auth[0], auth[1], auth[2])
-
-        Blankly.auth_constructor.write_auth_cache("coinbase_pro", defined_name, self.__calls)
-
-        self.construct_interface(self.__calls)
+        Exchange.__init__(self, "coinbase_pro", portfolio_name, keys_path, settings_path)
 
     """
-    Builds information about the currency on this exchange by making particular API calls
+    Builds information about the asset on this exchange by making particular API calls
     """
-    def get_currency_state(self, currency):
+
+    def get_asset_state(self, symbol):
         """
         This determines the internal properties of the exchange block.
         Should be implemented per-class because it requires different types of interaction with each exchange.
         """
         # TODO Populate this with useful information
-        return self.Interface.get_account(currency)
+        return self.interface.get_account(symbol)
 
     def get_exchange_state(self):
         """
         Exchange state is the external properties for the exchange block
         """
         # TODO Populate this with useful information
-        return self.Interface.get_fees()
+        return self.interface.get_fees()
 
-    def get_direct_calls(self) -> Coinbase_Pro_API:
-        return self.__calls
+    def get_direct_calls(self) -> CoinbaseProAPI:
+        return self.calls
 
     """
-    GUI Testing Functions | These only exist in the Coinbase_Pro class:
+    GUI Testing Functions | These only exist in the coinbase_pro class:
     """
+
     def get_indicators(self):
-        return self.__calls.get_fees()
+        return self.calls.get_fees()
 
     def filter_relevant(self, dict):
         """
diff --git a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_API.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_api.py
similarity index 99%
rename from Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_API.py
rename to blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_api.py
index 949ec75b..9a777d3f 100644
--- a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_API.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_api.py
@@ -16,21 +16,23 @@
 import base64
 import hashlib
 import hmac
-import requests
+import json
 import time
 import warnings
-import json
 
+import requests
 from requests.auth import AuthBase
 
-
 # Create custom authentication for Exchange
+from blankly.exchanges.auth.abc_auth import ABCAuth
+
+
 class CoinbaseExchangeAuth(AuthBase):
     # Provided by CBPro: https://docs.pro.coinbase.com/#signing-a-message
-    def __init__(self, api_key, secret_key, passphrase):
-        self.api_key = api_key
-        self.secret_key = secret_key
-        self.passphrase = passphrase
+    def __init__(self, cb_auth: ABCAuth):
+        self.api_key = cb_auth.keys['API_KEY']
+        self.secret_key = cb_auth.keys['API_SECRET']
+        self.passphrase = cb_auth.keys['API_PASS']
 
     def __call__(self, request):
         timestamp = str(time.time())
@@ -58,8 +60,8 @@ def get_auth_headers(timestamp, message, api_key, secret_key, passphrase):
 
 
 class API:
-    def __init__(self, API_KEY, API_SECRET, API_PASS, API_URL='https://api.pro.coinbase.com/'):
-        self.__auth = CoinbaseExchangeAuth(API_KEY, API_SECRET, API_PASS)
+    def __init__(self, cb_auth: ABCAuth, API_URL='https://api.pro.coinbase.com/'):
+        self.__auth = CoinbaseExchangeAuth(cb_auth)
         self.__api_url = API_URL
         self.session = requests.Session()
 
@@ -68,6 +70,7 @@ def __init__(self, API_KEY, API_SECRET, API_PASS, API_URL='https://api.pro.coinb
     """
 
     """ Full interface support """
+
     def get_products(self):
         """Get a list of available currency pairs for trading.
 
@@ -129,6 +132,7 @@ def get_product_order_book(self, product_id, level=1):
         return requests.get(self.__api_url + "products/{}/book".format(product_id), params=params).json()
 
     """ PAGINATED """ """ Full interface support """
+
     def get_product_trades(self, product_id, before='', after='', limit=None, result=None):
         """List the latest trades for a product.
 
@@ -162,6 +166,7 @@ def get_product_trades(self, product_id, before='', after='', limit=None, result
                                             .format(product_id))
 
     """ Full interface support """
+
     def get_product_historic_rates(self, product_id, start=None, end=None, granularity=None):
         """Historic rates for a product.
 
@@ -228,6 +233,7 @@ def get_product_24hr_stats(self, product_id):
         return requests.get(self.__api_url + 'products/{}/stats'.format(product_id), auth=self.__auth).json()
 
     """ Full interface support """
+
     def get_currencies(self):
         """List known currencies.
 
@@ -263,6 +269,7 @@ def get_time(self):
     """
     Private API Calls
     """
+
     def get_accounts(self):
         """ Get a list of trading all accounts.
 
@@ -292,6 +299,7 @@ def get_accounts(self):
         return requests.get(self.__api_url + 'accounts', auth=self.__auth).json()
 
     """ Full interface support """
+
     def get_account(self, account_id):
         """ Get information for a single account.
 
@@ -313,6 +321,7 @@ def get_account(self, account_id):
         return requests.get(self.__api_url + 'accounts/' + account_id, auth=self.__auth).json()
 
     """ PAGINATED """
+
     def get_account_history(self, account_id, **kwargs):
         """ List account activity. Account activity either increases or
         decreases your account balance.
@@ -354,6 +363,7 @@ def get_account_history(self, account_id, **kwargs):
         return self._send_paginated_message(endpoint, params=kwargs)
 
     """ PAGINATED """
+
     def get_account_holds(self, account_id, **kwargs):
         """ Get holds on an account.
 
@@ -402,6 +412,7 @@ def get_account_holds(self, account_id, **kwargs):
     """
     Buy & sell
     """
+
     def place_order(self, product_id, side, order_type, **kwargs):
         """ Place an order.
 
@@ -623,6 +634,7 @@ def place_stop_order(self, product_id, side, price, size=None, funds=None,
         return self.place_order(**params)
 
     """ Full interface support """
+
     def cancel_order(self, order_id):
         """ Cancel a previously placed order.
 
@@ -647,6 +659,7 @@ def cancel_order(self, order_id):
 
     """ PAGINATED """
     """ Full interface support (untested) """
+
     def get_orders(self, product_id=None, status=None, **kwargs):
         """ List your current open orders.
 
@@ -713,6 +726,7 @@ def get_orders(self, product_id=None, status=None, **kwargs):
         return self._send_paginated_message('orders', params=params)
 
     """ Full interface support (untested) """
+
     def get_order(self, order_id):
         """ Get a single order by order id.
 
@@ -749,6 +763,7 @@ def get_order(self, order_id):
         return requests.get(self.__api_url + "orders/" + order_id, auth=self.__auth).json()
 
     """ PAGINATED """
+
     def get_fills(self, order_id=None, product_id=None, **kwargs):
         """ Get a list of recent fills.
 
@@ -808,6 +823,7 @@ def get_fills(self, order_id=None, product_id=None, **kwargs):
         return self._send_paginated_message('/fills', params=params)
 
     """ Full interface support (tested) """
+
     def get_fees(self):
         """
             Returns:
@@ -864,6 +880,7 @@ def _send_paginated_message(self, endpoint, params=None):
     """
     UNDOCUMENTED
     """
+
     def create_report(self, report_type, start_date, end_date, product_id=None,
                       account_id=None, report_format='pdf', email=None):
         """ Create report of historic information about your account.
@@ -973,19 +990,6 @@ def get_product_ticker(self, product_id):
         """
         return requests.get(self.__api_url + 'products/' + product_id + '/ticker', auth=self.__auth).json()
 
-
-
-
-
-
-
-
-
-
-
-
-
-
 # # Create custom authentication for Exchange
 # class CoinbaseExchangeAuth(AuthBase):
 #     # Provided by CBPro: https://docs.pro.coinbase.com/#signing-a-message
@@ -1023,7 +1027,7 @@ def get_product_ticker(self, product_id):
 #     def __init__(self, API_KEY, API_SECRET, API_PASS, API_URL='https://api.pro.coinbase.com/'):
 #         self.__auth = CoinbaseExchangeAuth(API_KEY, API_SECRET, API_PASS)
 #         self.__api_url = API_URL
-#         self.__Utils = Blankly.Utils.Utils()
+#         self.__Utils = blankly.Utils.Utils()
 #
 #     def get_portfolio(self, currency=None, show=False):
 #         output = requests.get(self.__api_url + 'accounts', auth=self.__auth).json()
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_auth.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_auth.py
new file mode 100644
index 00000000..5bdcd76c
--- /dev/null
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_auth.py
@@ -0,0 +1,26 @@
+"""
+    Coinbase authentication base class
+    Copyright (C) 2021  Arun Annamalai, 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 <https://www.gnu.org/licenses/>.
+"""
+
+from blankly.exchanges.auth.abc_auth import ABCAuth
+
+
+class CoinbaseProAuth(ABCAuth):
+    def __init__(self, keys_file, portfolio_name):
+        super().__init__(keys_file, portfolio_name, 'coinbase_pro')
+        needed_keys = ['API_KEY', 'API_SECRET', 'API_PASS']
+        self.validate_credentials(needed_keys)
diff --git a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Interface.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
similarity index 69%
rename from Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Interface.py
rename to blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
index e1ea8b48..85cc1f4e 100644
--- a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Interface.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
@@ -16,23 +16,21 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-
 import time
 import warnings
-import pandas as pd
-
-import Blankly.utils.utils as utils
-from Blankly.utils.exceptions import InvalidOrder
-from Blankly.utils.exceptions import APIException
-from Blankly.utils.purchases.limit_order import LimitOrder
-from Blankly.utils.purchases.market_order import MarketOrder
-from Blankly.utils.purchases.stop_limit import StopLimit
 
+import pandas as pd
 
-from Blankly.interface.currency_Interface import CurrencyInterface
+import blankly.utils.time_builder
+import blankly.utils.utils as utils
+from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
+from blankly.exchanges.orders.limit_order import LimitOrder
+from blankly.exchanges.orders.market_order import MarketOrder
+from blankly.exchanges.orders.stop_limit import StopLimit
+from blankly.utils.exceptions import APIException, InvalidOrder
 
 
-class CoinbaseProInterface(CurrencyInterface):
+class CoinbaseProInterface(ExchangeInterface):
     def __init__(self, exchange_name, authenticated_API):
         super().__init__(exchange_name, authenticated_API)
 
@@ -77,20 +75,22 @@ def get_products(self):
         products = self.calls.get_products()
         for i in range(len(products)):
             # Rename needed
-            products[i]["currency_id"] = products[i].pop("id")
+            products[i]["symbol"] = products[i].pop("id")
+            products[i]["base_asset"] = products[i].pop("base_currency")
+            products[i]["quote_asset"] = products[i].pop("quote_currency")
             products[i] = utils.isolate_specific(needed, products[i])
         return products
 
-    def get_account(self, currency=None):
+    def get_account(self, symbol=None) -> utils.AttributeDict:
         """
-        Get all currencies in an account, or sort by currency/account_id
+        Get all currencies in an account, or sort by symbol/account_id
         Args:
-            currency (Optional): Filter by particular currency
+            symbol (Optional): Filter by particular symbol
 
             These arguments are mutually exclusive
         Coinbase Pro: get_account
         """
-        currency = super().get_account(currency=currency)
+        symbol = super().get_account(symbol=symbol)
 
         needed = self.needed['get_account']
         """
@@ -109,22 +109,32 @@ def get_account(self, currency=None):
         ]
         """
         accounts = self.calls.get_accounts()
+
+        parsed_dictionary = {}
+
         # We have to sort through it if the accounts are none
-        if currency is not None:
+        if symbol is not None:
             for i in accounts:
-                if i["currency"] == currency:
+                if i["currency"] == symbol:
                     parsed_value = utils.isolate_specific(needed, i)
-                    return parsed_value
-            raise ValueError("Currency not found")
+                    return utils.AttributeDict({
+                        'available': parsed_value['available'],
+                        'hold': parsed_value['hold']
+                    })
+            raise ValueError("Symbol not found")
         for i in range(len(accounts)):
-            accounts[i] = utils.isolate_specific(needed, accounts[i])
-        return accounts
+            parsed_dictionary[accounts[i]['currency']] = utils.AttributeDict({
+                'available': float(accounts[i]['available']),
+                'hold': float(accounts[i]['hold'])
+            })
 
-    def market_order(self, product_id, side, funds) -> MarketOrder:
+        return utils.AttributeDict(parsed_dictionary)
+
+    def market_order(self, symbol, side, funds) -> MarketOrder:
         """
         Used for buying or selling market orders
         Args:
-            product_id: currency to buy
+            symbol: currency to buy
             side: buy/sell
             funds: desired amount of quote currency to use
         """
@@ -150,13 +160,14 @@ def market_order(self, product_id, side, funds) -> MarketOrder:
         order = {
             'funds': funds,
             'side': side,
-            'product_id': product_id,
+            'symbol': symbol,
             'type': 'market'
         }
-        response = self.calls.place_market_order(product_id, side, funds=funds)
+        response = self.calls.place_market_order(symbol, side, funds=funds)
         if "message" in response:
             raise InvalidOrder("Invalid Order: " + response["message"])
         response["created_at"] = utils.epoch_from_ISO8601(response["created_at"])
+        response["symbol"] = response.pop('product_id')
         response = utils.isolate_specific(needed, response)
         return MarketOrder(order, response, self)
 
@@ -193,13 +204,14 @@ def limit_order(self, product_id, side, price, size) -> LimitOrder:
             'size': size,
             'side': side,
             'price': price,
-            'product_id': product_id,
+            'symbol': product_id,
             'type': 'limit'
         }
         response = self.calls.place_limit_order(product_id, side, price, size=size)
         if "message" in response:
             raise InvalidOrder("Invalid Order: " + response["message"])
         response["created_at"] = utils.epoch_from_ISO8601(response["created_at"])
+        response["symbol"] = response.pop('product_id')
         response = utils.isolate_specific(needed, response)
         return LimitOrder(order, response, self)
 
@@ -209,11 +221,12 @@ def limit_order(self, product_id, side, price, size) -> LimitOrder:
     If you want to use this function you can, just do interface.stop_limit(args) if you're using a coinbase pro 
     interface
     """
-    def stop_limit(self, product_id, side, stop_price, limit_price, size, stop='loss') -> StopLimit:
+
+    def stop_limit(self, symbol, side, stop_price, limit_price, size, stop='loss') -> StopLimit:
         """
         Used for placing stop orders
         Args:
-            product_id (str): currency to buy
+            symbol (str): currency to buy
             side (str): buy/sell
             stop_price (float): Price to trigger the stop order at
             limit_price (float): Price to set the stop limit
@@ -243,7 +256,7 @@ def stop_limit(self, product_id, side, stop_price, limit_price, size, stop='loss
         }
         """
         order = {
-            'product_id': product_id,
+            'symbol': symbol,
             'side': side,
             'type': 'stop',
             'stop': stop,
@@ -251,7 +264,7 @@ def stop_limit(self, product_id, side, stop_price, limit_price, size, stop='loss
             'size': size,
             'price': limit_price
         }
-        response = self.calls.place_order(product_id=product_id,
+        response = self.calls.place_order(product_id=symbol,
                                           order_type='limit',
                                           side=side,
                                           stop=stop,  # loss
@@ -260,17 +273,18 @@ def stop_limit(self, product_id, side, stop_price, limit_price, size, stop='loss
                                           price=limit_price
                                           )
         response['limit_price'] = response.pop('price')
+        response["symbol"] = response.pop('product_id')
         return StopLimit(order, response, self)
 
-    def cancel_order(self, currency_id, order_id) -> dict:
+    def cancel_order(self, symbol, order_id) -> dict:
         """
         Returns:
             dict: Containing the order_id of cancelled order. Example::
             { "order_id": "c5ab5eae-76be-480e-8961-00792dc7e138" }
         """
-        return {"order_id": self.calls.cancel_order(order_id)[0]}
+        return {"order_id": self.calls.cancel_order(order_id)}
 
-    def get_open_orders(self, product_id=None):
+    def get_open_orders(self, symbol=None):
         """
         List open orders.
         """
@@ -298,10 +312,10 @@ def get_open_orders(self, product_id=None):
             }
         ]
         """
-        if product_id is None:
+        if symbol is None:
             orders = list(self.calls.get_orders())
         else:
-            orders = list(self.calls.get_orders(product_id=product_id))
+            orders = list(self.calls.get_orders(product_id=symbol))
 
         if len(orders) == 0:
             return []
@@ -311,11 +325,12 @@ def get_open_orders(self, product_id=None):
         for i in range(len(orders)):
             orders[i]["created_at"] = utils.epoch_from_ISO8601(orders[i]["created_at"])
             needed = self.choose_order_specificity(orders[i]['type'])
+            orders[i]["symbol"] = orders[i].pop('product_id')
             orders[i] = utils.isolate_specific(needed, orders[i])
 
         return orders
 
-    def get_order(self, currency_id, order_id) -> dict:
+    def get_order(self, symbol, order_id) -> dict:
         """
         {
             "created_at": "2017-06-18T00:27:42.920136Z",
@@ -355,6 +370,21 @@ def get_order(self, currency_id, order_id) -> dict:
         }
         """
         response = self.calls.get_order(order_id)
+        if 'message' in response:
+            # This part will run through all orders if the user enables the setting
+            # Leaving this commented because its useful in getting all orders if we add that
+            # if self.user_preferences['settings']['coinbase_pro']['search_all_fills']:
+            #     all_orders = list(self.calls.get_orders(product_id=currency_id, status='all'))
+            #     found = False
+            #     for i in all_orders:
+            #         if i['id'] == order_id:
+            #             found = True
+            #             response = i
+            #
+            #     if not found:
+            #         raise APIException("Invalid: " + str(response['message']) + ", was the order canceled?")
+            # else:
+            raise APIException("Invalid: " + str(response['message']) + ", was the order canceled?")
         response["created_at"] = utils.epoch_from_ISO8601(response["created_at"])
 
         if response['type'] == 'market':
@@ -363,11 +393,13 @@ def get_order(self, currency_id, order_id) -> dict:
             needed = self.needed['limit_order']
         else:
             needed = self.needed['market_order']
+
+        response["symbol"] = response.pop('product_id')
         return utils.isolate_specific(needed, response)
 
     """
     Coinbase Pro: get_fees
-    Binance: get_trade_fee
+    binance: get_trade_fee
     """
 
     def get_fees(self) -> dict:
@@ -384,45 +416,50 @@ def get_fees(self) -> dict:
 
     """
     Coinbase Pro: get_account_history
-    Binance: 
+    binance: 
         get_deposit_history
         get_withdraw_history
 
     """
 
-    def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
+    def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
         """
         Returns the product history from an exchange
         Args:
-            product_id: Blankly product ID format (BTC-USD)
+            symbol: blankly product ID format (BTC-USD)
             epoch_start: Time to begin download
             epoch_stop: Time to stop download
-            granularity: Resolution in seconds between tick (ex: 60 = 1 per minute)
+            resolution: Resolution in seconds between tick (ex: 60 = 1 per minute)
         Returns:
             Dataframe with *at least* 'time (epoch)', 'low', 'high', 'open', 'close', 'volume' as columns.
         """
-        epoch_start, epoch_stop = super().get_product_history(product_id, epoch_start, epoch_stop, granularity)
+
+        resolution = blankly.time_builder.time_interval_to_seconds(resolution)
+
+        # epoch_start, epoch_stop = super().get_product_history(symbol, epoch_start, epoch_stop, resolution)
+        epoch_start = utils.convert_epochs(epoch_start)
+        epoch_stop = utils.convert_epochs(epoch_stop)
 
         accepted_grans = [60, 300, 900, 3600, 21600, 86400]
-        if granularity not in accepted_grans:
+        if resolution not in accepted_grans:
             warnings.warn("Granularity is not an accepted granularity...rounding to nearest valid value.")
-            granularity = accepted_grans[min(range(len(accepted_grans)),
-                                             key=lambda i: abs(accepted_grans[i] - granularity))]
+            resolution = accepted_grans[min(range(len(accepted_grans)),
+                                            key=lambda i: abs(accepted_grans[i] - resolution))]
 
-        granularity = int(granularity)
+        resolution = int(resolution)
 
         # Figure out how many points are needed
-        need = int((epoch_stop - epoch_start) / granularity)
+        need = int((epoch_stop - epoch_start) / resolution)
         initial_need = need
         window_open = epoch_start
         history = []
         # Iterate while its more than max
         while need > 300:
             # Close is always 300 points ahead
-            window_close = window_open + 300 * granularity
+            window_close = window_open + 300 * resolution
             open_iso = utils.ISO8601_from_epoch(window_open)
             close_iso = utils.ISO8601_from_epoch(window_close)
-            response = self.calls.get_product_historic_rates(product_id, open_iso, close_iso, granularity)
+            response = self.calls.get_product_historic_rates(symbol, open_iso, close_iso, resolution)
             if isinstance(response, dict):
                 raise APIException(response['message'])
             history = history + response
@@ -435,23 +472,25 @@ def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
         # Fill the remainder
         open_iso = utils.ISO8601_from_epoch(window_open)
         close_iso = utils.ISO8601_from_epoch(epoch_stop)
-        response = self.calls.get_product_historic_rates(product_id, open_iso, close_iso, granularity)
+        response = self.calls.get_product_historic_rates(symbol, open_iso, close_iso, resolution)
         if isinstance(response, dict):
             raise APIException(response['message'])
         history_block = history + response
         history_block.sort(key=lambda x: x[0])
-        return pd.DataFrame(history_block, columns=['time', 'low', 'high', 'open', 'close', 'volume'])
+
+        df = pd.DataFrame(history_block, columns=['time', 'low', 'high', 'open', 'close', 'volume'])
+        # df[['time']] = df[['time']].astype(int)
+        # Have to cast this for some reason
+        df[['low', 'high', 'open', 'close', 'volume']] = df[['low', 'high', 'open', 'close', 'volume']].astype(float)
+
+        return df
 
     """
     Coinbase Pro: Get Currencies
-    Binance: get_products
+    binance: get_products
     """
 
-    def get_market_limits(self, product_id):
-        needed = self.needed['get_market_limits']
-        renames = [
-            ["id", "market"]
-        ]
+    def get_order_filter(self, symbol: str):
         """
         Returns:
         list: List of available currency pairs. Example::
@@ -480,20 +519,45 @@ def get_market_limits(self, product_id):
         response = self.calls.get_products()
         products = None
         for i in response:
-            if i["id"] == product_id:
+            if i["id"] == symbol:
                 products = i
+                break
 
         if products is None:
             raise LookupError("Specified market not found")
 
-        products = utils.rename_to(renames, products)
-        products["min_price"] = 0
-        # These don't really exist on this exchange
-        products["max_price"] = -1
-        products["max_orders"] = -1
-        return utils.isolate_specific(needed, products)
+        return {
+            "symbol": products.pop('id'),
+            "base_asset": products.pop('base_currency'),
+            "quote_asset": products.pop('quote_currency'),
+            "max_orders": 1000000000000,
+            "limit_order": {
+                "base_min_size": float(products.pop('base_min_size')),  # Minimum size to buy
+                "base_max_size": float(products.pop('base_max_size')),  # Maximum size to buy
+                "base_increment": float(products.pop('base_increment')),  # Specifies the minimum increment
+                # for the base_asset.
+                "price_increment": float(products['quote_increment']),
+
+                "min_price": float(products['quote_increment']),
+                "max_price": 9999999999,
+            },
+            'market_order': {
+                "fractionable": True,
+                "quote_increment": float(products.pop('quote_increment')),  # Specifies the min order price as well
+                # as the price increment.
+                "buy": {
+                    "min_funds": float(products['min_market_funds']),
+                    "max_funds": float(products['max_market_funds']),
+                },
+                "sell": {
+                    "min_funds": float(products.pop('min_market_funds')),
+                    "max_funds": float(products.pop('max_market_funds')),
+                },
+            },
+            "exchange_specific": {**products}
+        }
 
-    def get_price(self, currency_pair) -> float:
+    def get_price(self, symbol) -> float:
         """
         Returns just the price of a currency pair.
         """
@@ -508,7 +572,7 @@ def get_price(self, currency_pair) -> float:
             'volume': '31137.51184419'
         }
         """
-        response = self.calls.get_product_ticker(currency_pair)
+        response = self.calls.get_product_ticker(symbol)
         if 'message' in response:
             raise APIException("Error: " + response['message'])
         return float(response['price'])
diff --git a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Utils.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_utils.py
similarity index 99%
rename from Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Utils.py
rename to blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_utils.py
index e629f2b8..6f4b725c 100644
--- a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Utils.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_utils.py
@@ -37,6 +37,7 @@ def __init__(self):
     }
     (size in currency (like .01 BTC), buy/sell (string), product id (BTC-USD))
     """
+
     def generate_limit_order(self, size, price, side, product_id):
         order = {
             'size': size,
diff --git a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Websocket.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py
similarity index 91%
rename from Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Websocket.py
rename to blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py
index 485b221f..a364e6a6 100644
--- a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Websocket.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py
@@ -16,18 +16,19 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-import Blankly
-import threading
+import collections
 import json
 import ssl
+import threading
 import time
 import traceback
-from Blankly.exchanges.IExchange_Websocket import IExchangeWebsocket
-import Blankly.exchanges.Coinbase_Pro.websocket_utils as websocket_utils
-import collections
 
 from websocket import create_connection
 
+import blankly
+import blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket_utils as websocket_utils
+from blankly.exchanges.abc_exchange_websocket import ABCExchangeWebsocket
+
 
 def create_ticker_connection(id, url, channel):
     ws = create_connection(url, sslopt={"cert_reqs": ssl.CERT_NONE})
@@ -41,6 +42,7 @@ def create_ticker_connection(id, url, channel):
     ws.send(request)
     return ws
 
+
 # This could be needed:
 # "channels": [
 #     {
@@ -52,16 +54,17 @@ def create_ticker_connection(id, url, channel):
 # ]
 
 
-class Tickers(IExchangeWebsocket):
-    def __init__(self, currency_id, stream, log=None, pre_event_callback=None, WEBSOCKET_URL="wss://ws-feed.pro.coinbase.com"):
+class Tickers(ABCExchangeWebsocket):
+    def __init__(self, symbol, stream, log=None,
+                 pre_event_callback=None, initially_stopped=False, WEBSOCKET_URL="wss://ws-feed.pro.coinbase.com"):
         """
         Create and initialize the ticker
         Args:
-            currency_id: Currency to initialize on such as "BTC-USD"
+            symbol: Currency to initialize on such as "BTC-USD"
             log: Fill this with a path to a log file that should be created
             WEBSOCKET_URL: Default websocket URL feed.
         """
-        self.__id = currency_id
+        self.__id = symbol
         self.__stream = stream
         self.__logging_callback, self.__interface_callback, log_message = websocket_utils.switch_type(stream)
 
@@ -86,13 +89,14 @@ def __init__(self, currency_id, stream, log=None, pre_event_callback=None, WEBSO
         self.__pre_event_callback = pre_event_callback
 
         # Reload preferences
-        self.__preferences = Blankly.utils.load_user_preferences()
+        self.__preferences = blankly.utils.load_user_preferences()
         buffer_size = self.__preferences["settings"]["websocket_buffer_size"]
         self.__ticker_feed = collections.deque(maxlen=buffer_size)
         self.__time_feed = collections.deque(maxlen=buffer_size)
 
         # Start the websocket
-        self.start_websocket()
+        if not initially_stopped:
+            self.start_websocket()
 
     def start_websocket(self):
         """
@@ -131,7 +135,7 @@ def read_websocket(self):
                 received_string = self.ws.recv()
                 received = json.loads(received_string)
                 # Modify time to use epoch
-                self.__most_recent_time = Blankly.utils.epoch_from_ISO8601(received["time"])
+                self.__most_recent_time = blankly.utils.epoch_from_ISO8601(received["time"])
                 received["time"] = self.__most_recent_time
                 self.__time_feed.append(self.__most_recent_time)
                 self.__most_recent_tick = received
@@ -169,6 +173,7 @@ def read_websocket(self):
                     self.__response = self.ws.recv()
 
     """ Required in manager """
+
     def is_websocket_open(self):
         return self.ws.connected
 
@@ -176,32 +181,39 @@ def get_currency_id(self):
         return self.__id
 
     """ Required in manager """
+
     def append_callback(self, obj):
         self.__callbacks.append(obj)
 
     """ Define a variable each time so there is no array manipulation """
     """ Required in manager """
+
     def get_most_recent_tick(self):
         return self.__most_recent_tick
 
     """ Required in manager """
+
     def get_most_recent_time(self):
         return self.__most_recent_time
 
     """ Required in manager """
+
     def get_time_feed(self):
         return list(self.__time_feed)
 
     """ Parallel with time feed """
     """ Required in manager """
+
     def get_feed(self):
         return list(self.__ticker_feed)
 
     """ Required in manager """
+
     def get_response(self):
         return self.__response
 
     """ Required in manager """
+
     def close_websocket(self):
         if self.ws.connected:
             self.ws.close()
@@ -209,5 +221,6 @@ def close_websocket(self):
             print("Websocket for " + self.__id + ' on channel ' + self.__stream + " is already closed")
 
     """ Required in manager """
+
     def restart_ticker(self):
         self.start_websocket()
diff --git a/Blankly/exchanges/Coinbase_Pro/websocket_utils.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket_utils.py
similarity index 86%
rename from Blankly/exchanges/Coinbase_Pro/websocket_utils.py
rename to blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket_utils.py
index 74363418..cff30614 100644
--- a/Blankly/exchanges/Coinbase_Pro/websocket_utils.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket_utils.py
@@ -17,7 +17,8 @@
 """
 
 import time
-import Blankly.utils.utils as utils
+
+import blankly.utils.utils as utils
 
 
 def switch_type(stream):
@@ -51,12 +52,16 @@ def trade_interface(message):
     Homogenizing with binance's return:
 
     Args:
-        message (str): Message from the exchange.
+        message (dict): Message from the exchange.
     """
     needed = [
-        ["product_id", str],
+        ["symbol", str],
         ["price", float],
-        ["time", int],
-        ["trade_id", int]
+        ["time", float],
+        ["trade_id", int],
+        ["size", float]
     ]
+
+    message['symbol'] = message.pop('product_id')
+    message['size'] = message.pop('last_size')
     return utils.isolate_specific(needed, message)
diff --git a/blankly/exchanges/interfaces/direct_calls_factory.py b/blankly/exchanges/interfaces/direct_calls_factory.py
new file mode 100644
index 00000000..50a092d2
--- /dev/null
+++ b/blankly/exchanges/interfaces/direct_calls_factory.py
@@ -0,0 +1,58 @@
+"""
+    Final constructor for authentication objects
+    Copyright (C) 2021  Arun Annamalai, 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 <https://www.gnu.org/licenses/>.
+"""
+
+from binance.client import Client
+
+import blankly.utils.utils as utils
+from blankly.exchanges.auth.abc_auth import ABCAuth
+from blankly.exchanges.interfaces.alpaca.alpaca_api import create_alpaca_client
+from blankly.exchanges.interfaces.alpaca.alpaca_interface import AlpacaInterface
+from blankly.exchanges.interfaces.binance.binance_interface import BinanceInterface
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_api import API as CoinbaseProAPI
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_interface import CoinbaseProInterface
+
+
+class DirectCallsFactory:
+    @staticmethod
+    def create(exchange_name: str, auth: ABCAuth, preferences_path: str = None):
+        preferences = utils.load_user_preferences(preferences_path)
+        if exchange_name == 'coinbase_pro':
+            if preferences["settings"]["use_sandbox"]:
+                calls = CoinbaseProAPI(auth, API_URL="https://api-public.sandbox.pro.coinbase.com/")
+            else:
+                # Create the authenticated object
+                calls = CoinbaseProAPI(auth)
+
+            return calls, CoinbaseProInterface(exchange_name, calls)
+
+        elif exchange_name == 'binance':
+            if preferences["settings"]["use_sandbox"]:
+                calls = Client(api_key=auth.keys['API_KEY'], api_secret=auth.keys['API_SECRET'],
+                               tld=preferences["settings"]['binance']["binance_tld"],
+                               testnet=True)
+            else:
+                calls = Client(api_key=auth.keys['API_KEY'], api_secret=auth.keys['API_SECRET'],
+                               tld=preferences["settings"]['binance']["binance_tld"])
+            return calls, BinanceInterface(exchange_name, calls)
+
+        elif exchange_name == 'alpaca':
+            calls = create_alpaca_client(auth, preferences["settings"]["use_sandbox"])
+            return calls, AlpacaInterface(calls, preferences_path)
+
+        elif exchange_name == 'paper_trade':
+            return None, None
diff --git a/blankly/exchanges/interfaces/exchange_interface.py b/blankly/exchanges/interfaces/exchange_interface.py
new file mode 100644
index 00000000..a570f565
--- /dev/null
+++ b/blankly/exchanges/interfaces/exchange_interface.py
@@ -0,0 +1,288 @@
+"""
+    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 <https://www.gnu.org/licenses/>.
+"""
+import abc
+import time
+import warnings
+from datetime import datetime as dt
+from typing import Union
+
+from dateutil import parser
+
+import blankly.utils.utils as utils
+from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface
+from blankly.utils.time_builder import time_interval_to_seconds
+
+
+# TODO: need to add a cancel all orders function
+class ExchangeInterface(ABCExchangeInterface, abc.ABC):
+    def __init__(self, exchange_name, authenticated_API, preferences_path=None):
+        self.exchange_name = exchange_name
+        self.calls = authenticated_API
+        # Reload user preferences here
+        self.user_preferences = utils.load_user_preferences(preferences_path)
+
+        # TODO, improve creation of its own properties
+        self.exchange_properties = None
+        # Some exchanges like binance will not return a value of 0.00 if there is no balance
+        self.available_currencies = {}
+        self.init_exchange()
+
+        self.needed = {
+            '__init_exchange__': [
+                ['maker_fee_rate', float],
+                ['taker_fee_rate', float]
+            ],
+            'get_products': [
+                ["symbol", str],
+                ["base_asset", str],
+                ["quote_asset", str],
+                ["base_min_size", float],
+                ["base_max_size", float],
+                ["base_increment", float]
+            ],
+            'get_account': [
+                ["symbol", str],
+                ["available", float],
+                ["hold", float]
+            ],
+            'market_order': [
+                ["symbol", str],
+                ["id", str],
+                ["created_at", float],
+                ["funds", float],
+                ["status", str],
+                ["type", str],
+                ["side", str]
+            ],
+            'limit_order': [
+                ["symbol", str],
+                ["id", str],
+                ["created_at", float],
+                ["price", float],
+                ["size", float],
+                ["status", str],
+                ["time_in_force", str],
+                ["type", str],
+                ["side", str]
+            ],
+            'stop_limit': [
+                ["symbol", str],
+                ["id", str],
+                ["created_at", float],
+                ["stop_price", float],
+                ["limit_price", float],
+                ["stop", str],
+                ["size", float],
+                ["status", str],
+                ["time_in_force", str],
+                ["type", str],
+                ["side", str]
+            ],
+            'cancel_order': [
+                ['order_id', str]
+            ],
+            # 'get_open_orders': [  # Key specificity changes based on order type
+            #     ["id", str],
+            #     ["price", float],
+            #     ["size", float],
+            #     ["type", str],
+            #     ["side", str],
+            #     ["status", str],
+            #     ["product_id", str]
+            # ],
+            # 'get_order': [
+            #     ["product_id", str],
+            #     ["id", str],
+            #     ["price", float],
+            #     ["size", float],
+            #     ["type", str],
+            #     ["side", str],
+            #     ["status", str],
+            #     ["funds", float]
+            # ],
+            'get_fees': [
+                ['maker_fee_rate', float],
+                ['taker_fee_rate', float]
+            ],
+            'get_order_filter': [
+                ["symbol", str],
+                ["base_asset", str],
+                ["quote_asset", str],
+                ["max_orders", float],
+                ["limit_order", dict],
+                ["market_order", dict]
+            ]
+        }
+
+    @abc.abstractmethod
+    def init_exchange(self):
+        """
+        Create the properties for the exchange.
+        """
+        pass
+
+    """ Needs to be overridden here """
+
+    def get_calls(self):
+        """
+        Returns:
+             The exchange's direct calls object. A blankly Bot class should have immediate access to this by
+             default
+        """
+        return self.calls
+
+    """ Needs to be overridden here """
+
+    def get_exchange_type(self):
+        return self.exchange_name
+
+    @property
+    def account(self):
+        return utils.AttributeDict(self.get_account())
+
+    @property
+    def orders(self):
+        return self.get_open_orders()
+
+    @property
+    def cash(self):
+        using_setting = self.user_preferences['settings'][self.exchange_name]['cash']
+        return self.get_account(using_setting)['available']
+
+    def history(self,
+                symbol: str,
+                to: Union[str, int] = 200,
+                resolution: Union[str, float] = '1d',
+                start_date: Union[str, dt, float] = None,
+                end_date: Union[str, dt, float] = None):
+
+        to_present = False
+        if end_date is None:
+            to_present = True
+
+        # convert resolution into epoch seconds
+        resolution_seconds = time_interval_to_seconds(resolution)
+
+        if end_date is None:
+            # Figure out the next point and then subtract to the last stamp
+            most_recent_valid_resolution = utils.ceil_date(dt.now(),
+                                                           seconds=resolution_seconds).timestamp() - resolution_seconds
+            # Binance is nice enough to update OHLCV data so we have to exclude that by subtracting a resolution
+            epoch_stop = most_recent_valid_resolution - resolution_seconds
+            count_from = most_recent_valid_resolution
+        else:
+            if isinstance(end_date, str):
+                parsed_date = parser.parse(end_date)
+                print(parsed_date.timestamp())
+            elif isinstance(end_date, float):
+                parsed_date = dt.fromtimestamp(end_date)
+            else:
+                parsed_date = end_date
+            valid_time_in_past = utils.ceil_date(parsed_date,
+                                                 seconds=resolution_seconds).timestamp() - resolution_seconds
+            epoch_stop = valid_time_in_past - resolution_seconds
+            count_from = valid_time_in_past
+
+        if start_date is None and end_date is None:
+            if isinstance(to, int):
+                # use number of points to calculate the start epoch
+                epoch_start = count_from - (to * resolution_seconds)
+            else:
+                epoch_start = count_from - time_interval_to_seconds(to)
+        elif start_date is None and end_date is not None:
+            if isinstance(to, int):
+                epoch_start = count_from - (to * resolution_seconds)
+            else:
+                epoch_start = count_from - time_interval_to_seconds(to)
+        else:
+            epoch_start = utils.convert_input_to_epoch(start_date)
+
+        response = self.get_product_history(symbol, epoch_start, epoch_stop, resolution_seconds)
+
+        # Add a check to make sure that coinbase pro has updated
+        if to_present and self.get_exchange_type() == "coinbase_pro":
+            data_append = None
+            tries = 0
+            while True:
+                if data_append is None:
+                    if response['time'].iloc[-1] == epoch_stop:
+                        break
+                else:
+                    if data_append[0]['time'] == epoch_stop:
+                        break
+                time.sleep(.5)
+                tries += 1
+                if tries > 5:
+                    # Admit failure and return
+                    warnings.warn("Exchange failed to provide updated data within the timeout.")
+                    return response
+                data_append = [self.get_product_history(symbol,
+                                                        epoch_stop - resolution_seconds,
+                                                        epoch_stop,
+                                                        resolution_seconds).iloc[-1].to_dict()]
+                data_append[0]['time'] = int(data_append[0]['time'])
+
+            response = response.append(data_append, ignore_index=True)
+
+        return response
+
+    def get_account(self, symbol=None):
+        """
+        Get all assets in an account, or sort by assets/account_id
+        Args:
+            symbol (Optional): Filter by particular symbol
+
+        Coinbase Pro: get_account
+        binance: get_account["balances"]
+        """
+
+        if symbol is not None:
+            symbol = utils.get_base_asset(symbol)
+
+        return symbol
+
+    @abc.abstractmethod
+    def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
+        pass
+
+    def choose_order_specificity(self, order_type):
+        # This lower should not be necessary if everything is truly homogeneous
+        order_type = order_type.lower()
+        if order_type == 'market':
+            return self.needed['market_order']
+        elif order_type == 'limit':
+            return self.needed['limit_order']
+        else:
+            return self.needed['market_order']
+
+    """
+    Order lifecycle should be:
+    Accepted -> live -> done -> settled
+    """
+
+    def homogenize_order_status(self, exchange, status):
+        if exchange == "binance":
+            if status == "new":
+                return "open"
+        elif exchange == 'alpaca':
+            if status == 'accepted':
+                return 'accepted'
+            if status == 'new':
+                return 'new'
+
+        return status
diff --git a/Blankly/exchanges/Paper_Trade/__init__.py b/blankly/exchanges/interfaces/paper_trade/__init__.py
similarity index 100%
rename from Blankly/exchanges/Paper_Trade/__init__.py
rename to blankly/exchanges/interfaces/paper_trade/__init__.py
diff --git a/Blankly/exchanges/__init__.py b/blankly/exchanges/interfaces/paper_trade/local_account/__init__.py
similarity index 100%
rename from Blankly/exchanges/__init__.py
rename to blankly/exchanges/interfaces/paper_trade/local_account/__init__.py
diff --git a/Blankly/utils/paper_trading/local_account/local_account.py b/blankly/exchanges/interfaces/paper_trade/local_account/local_account.py
similarity index 99%
rename from Blankly/utils/paper_trading/local_account/local_account.py
rename to blankly/exchanges/interfaces/paper_trade/local_account/local_account.py
index 86b33b78..c5db9005 100644
--- a/Blankly/utils/paper_trading/local_account/local_account.py
+++ b/blankly/exchanges/interfaces/paper_trade/local_account/local_account.py
@@ -16,5 +16,4 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-
 account = {}
diff --git a/Blankly/utils/paper_trading/local_account/trade_local.py b/blankly/exchanges/interfaces/paper_trade/local_account/trade_local.py
similarity index 74%
rename from Blankly/utils/paper_trading/local_account/trade_local.py
rename to blankly/exchanges/interfaces/paper_trade/local_account/trade_local.py
index 72390fd4..528995ca 100644
--- a/Blankly/utils/paper_trading/local_account/trade_local.py
+++ b/blankly/exchanges/interfaces/paper_trade/local_account/trade_local.py
@@ -15,17 +15,17 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-import Blankly.utils.paper_trading.local_account.local_account as local_account
-import Blankly.utils.utils as utils
-from Blankly.utils.exceptions import InvalidOrder
+import blankly.exchanges.interfaces.paper_trade.local_account.local_account as local_account
+import blankly.utils.utils as utils
+from blankly.utils.exceptions import InvalidOrder
 
 
-def trade_local(currency_pair, side, base_delta, quote_delta) -> None:
+def trade_local(symbol, side, base_delta, quote_delta, quote_resolution, base_resolution) -> None:
     """
     Trade on the local & static account
 
     Args:
-        currency_pair (string): Pair such as 'BTC-USD'
+        symbol (string): Pair such as 'BTC-USD'
         side (string): Purchase side such as 'buy' or 'sell' (currently unused but required)
         base_delta (float): A number specifying the change in base currency, such as -2.4 BTC by selling or
          +1.2 BTC by buying
@@ -34,17 +34,19 @@ def trade_local(currency_pair, side, base_delta, quote_delta) -> None:
     """
 
     # Extract the base and quote pairs of the currency
-    base = utils.get_base_currency(currency_pair)
-    quote = utils.get_quote_currency(currency_pair)
+    base = utils.get_base_asset(symbol)
+    quote = utils.get_quote_asset(symbol)
 
     # Push these abstracted deltas to the local account
     try:
-        local_account.account[base]['available'] = local_account.account[base]['available'] + base_delta
+        local_account.account[base]['available'] = utils.trunc(local_account.account[base]['available'] + base_delta,
+                                                               base_resolution)
     except KeyError:
         raise KeyError("Base currency specified not found in local account")
 
     try:
-        local_account.account[quote]['available'] = local_account.account[quote]['available'] + quote_delta
+        local_account.account[quote]['available'] = utils.trunc(local_account.account[quote]['available'] + quote_delta,
+                                                                quote_resolution)
     except KeyError:
         raise KeyError("Quote currency specified not found in local account")
 
@@ -59,7 +61,7 @@ def init_local_account(currencies: dict) -> None:
     local_account.account = currencies
 
 
-def test_trade(currency_pair, side, qty, quote_price) -> bool:
+def test_trade(currency_pair, side, qty, quote_price, quote_resolution, base_resolution) -> bool:
     """
     Test a paper trade to see if you have the funds
 
@@ -70,13 +72,13 @@ def test_trade(currency_pair, side, qty, quote_price) -> bool:
         quote_price (float): Price of the base currency in the currency pair - (1 BTC is valued at 40,245 in BTC-USD)
     """
     if side == 'buy':
-        quote = utils.get_quote_currency(currency_pair)
+        quote = utils.get_quote_asset(currency_pair)
         account = local_account.account[quote]
-        current_funds = account['available']
-        purchase_funds = quote_price * qty
+        current_funds = utils.trunc(account['available'], quote_resolution)
+        purchase_funds = utils.trunc(quote_price * qty, quote_resolution)
 
         # If you have more funds than the purchase requires then return true
-        if current_funds > purchase_funds:
+        if current_funds >= purchase_funds:
             return True
         else:
             raise InvalidOrder("Insufficient funds. Available:" +
@@ -87,12 +89,12 @@ def test_trade(currency_pair, side, qty, quote_price) -> bool:
                                str(qty) + ".")
 
     elif side == 'sell':
-        base = utils.get_base_currency(currency_pair)
+        base = utils.get_base_asset(currency_pair)
         account = local_account.account[base]
-        current_base = account['available']
+        current_base = utils.trunc(account['available'], base_resolution)
 
         # If you have more base than the sell requires then return true
-        if current_base > qty:
+        if current_base >= qty:
             return True
         else:
             raise InvalidOrder("Not enough base currency. Available: " +
diff --git a/blankly/exchanges/interfaces/paper_trade/metrics.py b/blankly/exchanges/interfaces/paper_trade/metrics.py
new file mode 100644
index 00000000..f4a3cb6a
--- /dev/null
+++ b/blankly/exchanges/interfaces/paper_trade/metrics.py
@@ -0,0 +1,89 @@
+"""
+    Metrics wrapper for backtesting
+    Copyright (C) 2021  Brandon Fan, 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 <https://www.gnu.org/licenses/>.
+"""
+
+import blankly.metrics as metrics
+from blankly.utils.time_builder import build_year
+
+
+def cagr(backtest_data):
+    account_values = backtest_data['resampled_account_value']
+    years = (account_values['time'].iloc[-1] - account_values['time'][0]) / build_year()
+    return metrics.cagr(account_values['time'][0], account_values['time'].iloc[-1], years)
+
+
+def cum_returns(backtest_data):
+    account_values = backtest_data['resampled_account_value']
+    return metrics.cum_returns(account_values['value'][0], account_values['value'].iloc[-1])
+
+
+def sortino(backtest_data):
+    # TODO: Need to pass in the specific resolution
+    # Defaulting to 1d
+    returns = backtest_data['returns']['value']
+    return metrics.sortino(returns)
+
+
+def sharpe(backtest_data):
+    # TODO: Need to pass in the specific resolution
+    # Defaulting to 1d
+    returns = backtest_data['returns']['value']
+    return metrics.sharpe(returns)
+
+
+def calmar(backtest_data):
+    # TODO: Need to pass in the specific resolution
+    # Defaulting to 1d
+    returns = backtest_data['returns']['value']
+    return metrics.calmar(returns)
+
+
+def volatility(backtest_data):
+    returns = backtest_data['returns']['value']
+    return metrics.volatility(returns)
+
+
+def variance(backtest_data):
+    returns = backtest_data['returns']['value']
+    return metrics.variance(returns)
+
+
+def beta(backtest_data):
+    # TODO: Need to pass in the specific resolution
+    # Defaulting to 1d
+    # Need to get some sort of baseline for this one...
+    # Use SP500 as default for all of them (can we get this data?)
+    # Or pick one of the assets as a baseline
+    returns = backtest_data['returns']['value']
+    return metrics.beta(returns)
+
+
+def var(backtest_data):
+    returns = backtest_data['returns']['value']
+    account_values = backtest_data['resampled_account_value']
+    return metrics.var(account_values['value'][0], returns, 0.95)
+
+
+def cvar(backtest_data):
+    returns = backtest_data['returns']['value']
+    account_values = backtest_data['resampled_account_value']
+    return metrics.cvar(account_values['value'][0], returns, 0.95)
+
+
+def max_drawdown(backtest_data):
+    returns = backtest_data['returns']['value']
+    return metrics.max_drawdown()
diff --git a/Blankly/exchanges/Paper_Trade/Paper_Trade.py b/blankly/exchanges/interfaces/paper_trade/paper_trade.py
similarity index 65%
rename from Blankly/exchanges/Paper_Trade/Paper_Trade.py
rename to blankly/exchanges/interfaces/paper_trade/paper_trade.py
index 7d19476a..686ec198 100644
--- a/Blankly/exchanges/Paper_Trade/Paper_Trade.py
+++ b/blankly/exchanges/interfaces/paper_trade/paper_trade.py
@@ -16,38 +16,36 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-
-from Blankly.exchanges.Paper_Trade.Paper_Trade_Interface import PaperTradeInterface
-from Blankly.exchanges.exchange import Exchange
+from blankly.exchanges.exchange import Exchange
+from blankly.exchanges.interfaces.paper_trade.paper_trade_interface import PaperTradeInterface
 
 
 class PaperTrade(Exchange):
-    def __init__(self, authenticated_exchange: Exchange):
+    def __init__(self, authenticated_exchange: Exchange, initial_account_values: dict = None):
         # Giving the preferences path as none allows us to create a default
-        Exchange.__init__(self, "paper_trade", "", None)
-
-        self.backtest_price_events = []
-        self.price_data = {}
+        Exchange.__init__(self, "paper_trade", "", None, None)
 
-        self.Interface = PaperTradeInterface(authenticated_exchange.get_interface())
+        self.interface = PaperTradeInterface(authenticated_exchange.get_interface(), initial_account_values)
+        self.calls = authenticated_exchange.get_direct_calls()
 
     """
-    Builds information about the currency on this exchange by making particular API calls
+    Builds information about the symbol on this exchange by making particular API calls
     """
-    def get_currency_state(self, currency):
+
+    def get_asset_state(self, symbol):
         """
         This determines the internal properties of the exchange block.
         Should be implemented per-class because it requires different types of interaction with each exchange.
         """
         # TODO Populate this with useful information
-        return self.Interface.get_account(currency)
+        return self.interface.get_account(symbol)
 
     def get_exchange_state(self):
         """
         Exchange state is the external properties for the exchange block
         """
         # TODO Populate this with useful information
-        return self.Interface.get_fees()
+        return self.interface.get_fees()
 
     def get_direct_calls(self):
-        return None
+        return self.calls
diff --git a/Blankly/exchanges/Paper_Trade/Paper_Trade_Interface.py b/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
similarity index 57%
rename from Blankly/exchanges/Paper_Trade/Paper_Trade_Interface.py
rename to blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
index 17743638..9de963a2 100644
--- a/Blankly/exchanges/Paper_Trade/Paper_Trade_Interface.py
+++ b/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
@@ -16,35 +16,30 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-
-from Blankly.interface.currency_Interface import CurrencyInterface
-from Blankly.exchanges.Paper_Trade.backtesting_wrapper import BacktestingWrapper
-
-
-from Blankly.utils.exceptions import InvalidOrder
-from Blankly.utils.exceptions import APIException
-
-import Blankly.utils.paper_trading.local_account.trade_local as trade_local
-import Blankly.utils.paper_trading.utils as paper_trade
-import Blankly.utils.utils as utils
-
-
-from Blankly.utils.purchases.limit_order import LimitOrder
-from Blankly.utils.purchases.market_order import MarketOrder
-
-import warnings
-import time
+import decimal
 import threading
+import time
 import traceback
+import warnings
+
+import blankly.exchanges.interfaces.paper_trade.local_account.trade_local as trade_local
+import blankly.exchanges.interfaces.paper_trade.utils as paper_trade
+import blankly.utils.utils as utils
+from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface
+from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
+from blankly.exchanges.interfaces.paper_trade.backtesting_wrapper import BacktestingWrapper
+from blankly.exchanges.orders.limit_order import LimitOrder
+from blankly.exchanges.orders.market_order import MarketOrder
+from blankly.utils.exceptions import APIException, InvalidOrder
 
 
-class PaperTradeInterface(CurrencyInterface, BacktestingWrapper):
-    def __init__(self, derived_interface):
+class PaperTradeInterface(ExchangeInterface, BacktestingWrapper):
+    def __init__(self, derived_interface: ABCExchangeInterface, initial_account_values: dict = None):
         self.paper_trade_orders = []
 
         self.get_products_cache = None
         self.get_fees_cache = None
-        self.get_market_limits_cache = None
+        self.get_order_filter_cache = {}
 
         self.__exchange_properties = None
 
@@ -52,22 +47,30 @@ def __init__(self, derived_interface):
 
         self.__thread = None
 
-        CurrencyInterface.__init__(self, "paper_trade", derived_interface)
+        # Set the type of the paper trade exchange to be the same as whichever interface its derived from
+        ExchangeInterface.__init__(self, derived_interface.get_exchange_type(), derived_interface)
         BacktestingWrapper.__init__(self)
 
         # Write in the accounts to our local account. This involves getting the values directly from the exchange
         accounts = self.calls.get_account()
-        value_pairs = {}
 
-        # Iterate & pair
-        for i in accounts:
-            value_pairs[i['currency']] = {
-                'available': i['available'],
-                'hold': i['hold']
-            }
+        # If it's given an initial account value dictionary, write all the account values that we passed in to the
+        # function to the account, then default everything else to zero.
+        if initial_account_values is not None:
+            for i in accounts.keys():
+                if i in initial_account_values.keys():
+                    accounts[i] = {
+                        'available': initial_account_values[i],
+                        'hold': 0.0
+                    }
+                else:
+                    accounts[i] = {
+                        'available': 0.0,
+                        'hold': 0.0
+                    }
 
         # Initialize the local account
-        trade_local.init_local_account(value_pairs)
+        trade_local.init_local_account(accounts)
 
     def init_exchange(self):
         try:
@@ -82,6 +85,7 @@ def init_exchange(self):
         }
 
     """ Needs to be overridden here """
+
     def start_paper_trade_watchdog(self):
         # TODO, this process could use variable update time/websocket usage, poll time and a variety of settings
         #  to create a robust trading system
@@ -94,6 +98,7 @@ def stop_paper_trade_watchdog(self):
         self.__run_watchdog = False
 
     """ Needs to be overridden here """
+
     def __paper_trade_watchdog(self):
         """
         Internal order watching system
@@ -104,6 +109,9 @@ def __paper_trade_watchdog(self):
                 break
             self.evaluate_limits()
 
+    def __get_decimals(self, number) -> int:
+        return abs(decimal.Decimal(str(number)).as_tuple().exponent)
+
     def override_local_account(self, value_dictionary: dict):
         """
         Push a new set of initial account values to the algorithm. All values not given in in the
@@ -132,10 +140,18 @@ def evaluate_limits(self):
         """
         used_currencies = []
         for i in self.paper_trade_orders:
-            if i["product_id"] not in used_currencies:
-                used_currencies.append(i['product_id'])
+            if i["symbol"] not in used_currencies:
+                used_currencies.append(i['symbol'])
         prices = {}
 
+        decimals_dict = {}
+        # get the market limits so that we can get accurate rounding
+        for i in used_currencies:
+            market_limits = self.get_order_filter(i)
+            decimals_dict[i] = {}
+            decimals_dict[i]['quantity_decimals'] = self.__get_decimals(market_limits['limit_order']['base_increment'])
+            decimals_dict[i]['quote_decimals'] = self.__get_decimals(market_limits['market_order']['quote_increment'])
+
         for i in used_currencies:
             prices[i] = self.get_price(i)
             if not self.backtesting:
@@ -164,13 +180,13 @@ def evaluate_limits(self):
             }
             """
             if index['type'] == 'limit' and index['status'] == 'pending':
-                current_price = prices[index['product_id']]
+                current_price = prices[index['symbol']]
 
                 if index['side'] == 'buy':
                     if index['price'] > current_price:
                         # Take everything off hold
-                        asset_id = index['product_id']
-                        quote = utils.get_quote_currency(asset_id)
+                        asset_id = index['symbol']
+                        quote = utils.get_quote_asset(asset_id)
 
                         available = trade_local.get_account(quote)['available']
                         # Put it back into available
@@ -181,20 +197,22 @@ def evaluate_limits(self):
                         trade_local.update_hold(quote, hold - (index['size'] * index['price']))
 
                         order, funds = self.evaluate_paper_trade(index, index['price'])
-                        trade_local.trade_local(currency_pair=index['product_id'],
+                        trade_local.trade_local(symbol=index['symbol'],
                                                 side='buy',
                                                 base_delta=float(order['filled_size']),  # Gain filled size after fees
-                                                quote_delta=funds * -1)  # Loose the original fund amount
+                                                quote_delta=funds * -1,  # Loose the original fund amount
+                                                base_resolution=decimals_dict[index['symbol']]['quantity_decimals'],
+                                                quote_resolution=decimals_dict[index['symbol']]['quote_decimals'])
                         order['status'] = 'done'
                         order['settled'] = 'true'
 
                         self.paper_trade_orders[i] = order
                 elif index['side'] == 'sell':
-                    if index['price'] < prices[index['product_id']]:
+                    if index['price'] < prices[index['symbol']]:
                         # Take everything off hold
 
-                        asset_id = index['product_id']
-                        base = utils.get_base_currency(asset_id)
+                        asset_id = index['symbol']
+                        base = utils.get_base_asset(asset_id)
 
                         available = trade_local.get_account(base)['available']
                         # Put it back into available
@@ -205,10 +223,13 @@ def evaluate_limits(self):
                         trade_local.update_hold(base, hold - index['size'])
 
                         order, funds = self.evaluate_paper_trade(index, index['price'])
-                        trade_local.trade_local(currency_pair=index['product_id'],
+                        trade_local.trade_local(symbol=index['symbol'],
                                                 side='sell',
                                                 base_delta=float(order['size'] * - 1),  # Loose size before any fees
-                                                quote_delta=float(order['executed_value']))  # Executed value after fees
+                                                quote_delta=float(order['executed_value']),  # Executed value after fees
+                                                base_resolution=decimals_dict[index['symbol']]['quantity_decimals'],
+                                                quote_resolution=decimals_dict[index['symbol']]['quote_decimals']
+                                                )
                         order['status'] = 'done'
                         order['settled'] = 'true'
 
@@ -232,56 +253,66 @@ def evaluate_paper_trade(self, order, current_price):
 
         return order, funds
 
-    def get_account(self, currency=None):
+    def get_account(self, symbol=None) -> dict:
         needed = self.needed['get_account']
 
         # TODO this can be optimized
         local_account = trade_local.get_accounts()
-        accounts = []
+        accounts = {}
         for key, value in local_account.items():
-            accounts.append({
-                'currency': key,
+            accounts[key] = {
                 'available': value['available'],
                 'hold': value['hold']
-            })
+            }
 
         # We have to sort through it if the accounts are none
-        if currency is not None:
-            for i in accounts:
-                if i["currency"] == currency:
-                    parsed_value = utils.isolate_specific(needed, i)
-                    return parsed_value
-            warnings.warn("Currency not found")
-        for i in range(len(accounts)):
-            accounts[i] = utils.isolate_specific(needed, accounts[i])
+        if symbol is not None:
+            if symbol in accounts.keys():
+                return local_account[symbol]
+            warnings.warn("Asset not found")
+
+        for k, v in local_account.items():
+            local_account[k] = utils.isolate_specific(needed, accounts[k])
         return accounts
 
-    def market_order(self, product_id, side, funds) -> MarketOrder:
+    def market_order(self, symbol, side, funds) -> MarketOrder:
         if not self.backtesting:
             print("Paper Trading...")
         needed = self.needed['market_order']
         order = {
             'funds': funds,
             'side': side,
-            'product_id': product_id,
+            'symbol': symbol,
             'type': 'market'
         }
         creation_time = self.time()
-        price = self.get_price(product_id)
+        price = self.get_price(symbol)
 
-        market_limits = self.get_market_limits(product_id)
-        if "min_market_funds" in market_limits['exchange_specific']:
-            min_funds = float(market_limits["exchange_specific"]["min_market_funds"])
-        else:
-            min_funds = float(market_limits["base_min_size"]) * price
+        market_limits = self.get_order_filter(symbol)
+
+        min_funds = market_limits['market_order'][side]["min_funds"]
+
+        max_funds = market_limits['market_order'][side]["max_funds"]
 
         if funds < min_funds:
-            raise InvalidOrder("Invalid Order: funds is too small. Minimum is: " + str(min_funds))
+            raise InvalidOrder("Funds is too small. Minimum is: " + str(min_funds))
+
+        if funds > max_funds:
+            raise InvalidOrder("Funds is too large. Maximum is: " + str(max_funds))
+
+        quote_increment = market_limits['market_order']['quote_increment']
+        quote_decimals = self.__get_decimals(quote_increment)
 
-        qty = funds / price
+        # Test if funds has more decimals than the increment. The increment is the maximum resolution of the quote.
+        if self.__get_decimals(funds) > quote_decimals:
+            raise InvalidOrder("Fund resolution is too high, minimum resolution is: " + str(quote_increment))
+
+        quantity_decimals = self.__get_decimals(market_limits['limit_order']['base_increment'])
+
+        qty = utils.trunc(funds / price, quantity_decimals)
 
         # Test the purchase
-        trade_local.test_trade(product_id, side, qty, price)
+        trade_local.test_trade(symbol, side, qty, price, quote_decimals, quantity_decimals)
         # Create coinbase pro-like id
         coinbase_pro_id = paper_trade.generate_coinbase_pro_id()
         # TODO the force typing here isn't strictly necessary because its run int the isolate_specific anyway
@@ -290,43 +321,53 @@ def market_order(self, product_id, side, funds) -> MarketOrder:
             'side': str(side),
             'type': 'market',
             'status': 'done',
-            'product_id': str(product_id),
-            'funds': str(funds - funds * float((self.__exchange_properties["maker_fee_rate"]))),
+            'symbol': str(symbol),
+            'funds': str(utils.trunc(funds - funds * float((self.__exchange_properties["maker_fee_rate"])),
+                                     quote_decimals)),
             'specified_funds': str(funds),
             'post_only': 'false',
             'created_at': str(creation_time),
             'done_at': str(self.time()),
             'done_reason': 'filled',
-            'fill_fees': str(funds * float((self.__exchange_properties["maker_fee_rate"]))),
-            'filled_size': str(funds - funds * float((self.__exchange_properties["maker_fee_rate"])) / price),
-            'executed_value': str(funds - funds * float((self.__exchange_properties["maker_fee_rate"]))),
+            'fill_fees': str(utils.trunc(funds * float((self.__exchange_properties["maker_fee_rate"])),
+                                         quote_decimals)),
+            'filled_size': str(utils.trunc(funds - funds *
+                                           float((self.__exchange_properties["maker_fee_rate"])) / price,
+                                           quantity_decimals)),
+            'executed_value': str(utils.trunc(funds - funds * float((self.__exchange_properties["maker_fee_rate"])),
+                                              quote_decimals)),
             'settled': 'true'
         }
         response = utils.isolate_specific(needed, response)
         self.paper_trade_orders.append(response)
         if side == "buy":
-            trade_local.trade_local(currency_pair=product_id,
+            trade_local.trade_local(symbol=symbol,
                                     side=side,
-                                    base_delta=qty - qty * float((self.__exchange_properties["maker_fee_rate"])),
+                                    base_delta=utils.trunc(qty -
+                                                           qty * float((self.__exchange_properties["maker_fee_rate"])),
+                                                           quantity_decimals),
                                     # Gain filled size after fees
-                                    quote_delta=funds * -1  # Loose the original fund amount
-                                    )
+                                    quote_delta=utils.trunc(funds * -1,
+                                                            quote_decimals),  # Loose the original fund amount
+                                    quote_resolution=quote_decimals, base_resolution=quantity_decimals)
         elif side == "sell":
-            trade_local.trade_local(currency_pair=product_id,
+            trade_local.trade_local(symbol=symbol,
                                     side=side,
-                                    base_delta=float(qty - 1),  # Loose size before any fees
-                                    quote_delta=funds - funds * float((self.__exchange_properties["maker_fee_rate"]))
-                                    # Gain executed value after fees
-                                    )
+                                    base_delta=utils.trunc(float(qty * -1), quantity_decimals),
+                                    # Loose size before any fees
+                                    quote_delta=utils.trunc(funds - funds *
+                                                            float((self.__exchange_properties["maker_fee_rate"])),
+                                                            quote_decimals),  # Gain executed value after fees
+                                    quote_resolution=quote_decimals, base_resolution=quantity_decimals)
         else:
             raise APIException("Invalid trade side: " + str(side))
         return MarketOrder(order, response, self)
 
-    def limit_order(self, product_id, side, price, size) -> LimitOrder:
+    def limit_order(self, symbol, side, price, size) -> LimitOrder:
         """
         Used for buying or selling limit orders
         Args:
-            product_id: currency to buy
+            symbol: currency to buy
             side: buy/sell
             price: price to set limit order
             size: amount of currency (like BTC) for the limit to be valued
@@ -355,24 +396,59 @@ def limit_order(self, product_id, side, price, size) -> LimitOrder:
             'size': size,
             'side': side,
             'price': price,
-            'product_id': product_id,
+            'symbol': symbol,
             'type': 'limit'
         }
         creation_time = self.time()
-        min_base = float(self.get_market_limits(product_id)["base_min_size"])
+
+        order_filter = self.get_order_filter(symbol)
+
+        min_base = float(order_filter['limit_order']["base_min_size"])
+        max_base = float(order_filter['limit_order']["base_max_size"])
+
         if size < min_base:
-            raise InvalidOrder("Invalid Order: Order quantity is too small. Minimum is: " + str(min_base))
+            raise InvalidOrder("Order quantity is too small. Minimum is: " + str(min_base))
+
+        if size > max_base:
+            raise InvalidOrder("Order quantity is too large. Maximum is: " + str(max_base))
+
+        min_price = float(order_filter['limit_order']["min_price"])
+        max_price = float(order_filter['limit_order']["max_price"])
+
+        if price < min_price:
+            raise InvalidOrder("Order quantity is too small. Minimum is: " + str(min_price))
+
+        if price > max_price:
+            raise InvalidOrder("Order quantity is too large. Maximum is: " + str(max_price))
+
+        # Check if the passed parameters are more accurate than either of the max base resolution or max price
+        # resolution
+        price_increment = order_filter['limit_order']['price_increment']
+        price_increment_decimals = self.__get_decimals(price_increment)
+
+        if self.__get_decimals(price) > price_increment_decimals:
+            raise InvalidOrder("Fund resolution is too high, minimum resolution is: " + str(price_increment))
+
+        base_increment = order_filter['limit_order']['base_increment']
+        base_decimals = self.__get_decimals(base_increment)
+
+        if self.__get_decimals(size) > base_decimals:
+            raise InvalidOrder("Fund resolution is too high, minimum resolution is: " + str(base_increment))
 
         # Test the trade
-        trade_local.test_trade(product_id, side, size, price)
+        trade_local.test_trade(symbol, side, size, price, quote_resolution=price_increment_decimals,
+                               base_resolution=base_decimals)
 
         # Create coinbase pro-like id
         coinbase_pro_id = paper_trade.generate_coinbase_pro_id()
+        # TODO the response fills in the exchange specific tags into the base dictionary. This is
+        #  useful but really shouldn't be included. We actually really need executed value as a
+        #  universally included key.
         response = {
             'id': str(coinbase_pro_id),
             'price': str(price),
             'size': str(size),
-            'product_id': str(product_id),
+            'symbol': str(symbol),
             'side': str(side),
             'stp': 'dc',
             'type': 'limit',
@@ -388,17 +464,17 @@ def limit_order(self, product_id, side, price, size) -> LimitOrder:
         response = utils.isolate_specific(needed, response)
         self.paper_trade_orders.append(response)
 
-        base = utils.get_base_currency(product_id)
-        quote = utils.get_quote_currency(product_id)
+        base = utils.get_base_asset(symbol)
+        quote = utils.get_quote_asset(symbol)
 
         if side == "buy":
             available = trade_local.get_account(quote)['available']
             # Loose the funds when buying
-            trade_local.update_available(quote, available - (size*price))
+            trade_local.update_available(quote, available - (size * price))
 
             # Gain the funds on hold when buying
             hold = trade_local.get_account(quote)['hold']
-            trade_local.update_hold(quote, hold + (size*price))
+            trade_local.update_hold(quote, hold + (size * price))
         elif side == "sell":
             available = trade_local.get_account(base)['available']
             # Loose the size when selling
@@ -409,7 +485,7 @@ def limit_order(self, product_id, side, price, size) -> LimitOrder:
             trade_local.update_hold(base, hold + size)
         return LimitOrder(order, response, self)
 
-    def cancel_order(self, currency_id, order_id) -> dict:
+    def cancel_order(self, symbol, order_id) -> dict:
         """
         This block could potentially work for both exchanges
         """
@@ -424,14 +500,14 @@ def cancel_order(self, currency_id, order_id) -> dict:
             del self.paper_trade_orders[order_index]
             return {"order_id": order_id}
 
-    def get_open_orders(self, product_id=None):
+    def get_open_orders(self, symbol=None):
         open_orders = []
         for i in self.paper_trade_orders:
             if i["status"] == "pending":
                 open_orders.append(i)
         return open_orders
 
-    def get_order(self, currency_id, order_id) -> dict:
+    def get_order(self, symbol, order_id) -> dict:
         for i in self.paper_trade_orders:
             if i["id"] == order_id:
                 return i
@@ -452,22 +528,20 @@ def get_fees(self):
         else:
             return self.calls.get_fees()
 
-    def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
+    def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
         if self.backtesting:
             raise APIException("Cannot download product history during a backtest")
         else:
-            return self.calls.get_product_history(product_id, epoch_start, epoch_stop, granularity)
+            return self.calls.get_product_history(symbol, epoch_start, epoch_stop, resolution)
 
-    def get_market_limits(self, product_id):
-        if self.backtesting:
-            if self.get_market_limits_cache is None:
-                self.get_market_limits_cache = self.calls.get_market_limits(product_id)
-            return self.get_market_limits_cache
-        else:
-            return self.calls.get_market_limits(product_id)
+    def get_order_filter(self, symbol):
+        # Don't re-query order filter if its cached
+        if symbol not in self.get_order_filter_cache:
+            self.get_order_filter_cache[symbol] = self.calls.get_order_filter(symbol)
+        return self.get_order_filter_cache[symbol]
 
-    def get_price(self, currency_pair) -> float:
+    def get_price(self, symbol) -> float:
         if self.backtesting:
-            return self.get_backtesting_price(currency_pair)
+            return self.get_backtesting_price(symbol)
         else:
-            return self.calls.get_price(currency_pair)
+            return self.calls.get_price(symbol)
diff --git a/Blankly/utils/paper_trading/utils.py b/blankly/exchanges/interfaces/paper_trade/utils.py
similarity index 100%
rename from Blankly/utils/paper_trading/utils.py
rename to blankly/exchanges/interfaces/paper_trade/utils.py
diff --git a/Blankly/interface/__init__.py b/blankly/exchanges/managers/__init__.py
similarity index 100%
rename from Blankly/interface/__init__.py
rename to blankly/exchanges/managers/__init__.py
diff --git a/Blankly/exchanges/general_stream_manager.py b/blankly/exchanges/managers/general_stream_manager.py
similarity index 51%
rename from Blankly/exchanges/general_stream_manager.py
rename to blankly/exchanges/managers/general_stream_manager.py
index c528eba1..3791a316 100644
--- a/Blankly/exchanges/general_stream_manager.py
+++ b/blankly/exchanges/managers/general_stream_manager.py
@@ -15,37 +15,35 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-import Blankly.utils.utils
-from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_Websocket import Tickers as Coinbase_Pro_Websocket
-from Blankly.exchanges.Binance.Binance_Websocket import Tickers as Binance_Websocket
-
-from Blankly.exchanges.websocket_manager import WebsocketManager
+import blankly.utils.utils
+from blankly.exchanges.interfaces.alpaca.alpaca_websocket import Tickers as Alpaca_Websocket
+from blankly.exchanges.interfaces.binance.binance_websocket import Tickers as Binance_Websocket
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket import Tickers as Coinbase_Pro_Websocket
+from blankly.exchanges.managers.websocket_manager import WebsocketManager
 
 
 class GeneralManager(WebsocketManager):
-    def __init__(self, default_exchange, default_currency):
+    def __init__(self, default_exchange, default_symbol):
         self.__default_exchange = default_exchange
-        self.__default_currency = default_currency
+        self.__default_currency = default_symbol
 
         self.__websockets = {}
 
-        super().__init__(self.__websockets, default_currency, default_exchange)
+        super().__init__(self.__websockets, default_symbol, default_exchange)
 
-    def create_general_connection(self, callback, channel, log=None, asset_id=None, override_exchange=None):
+    def create_general_connection(self, callback, channel, log=None, override_symbol=None, override_exchange=None):
         """
         Create a channel on any given stream on any exchange. This is not stored in the manager object, only returned.
         """
         asset_id_cache = self.__default_currency
         exchange_cache = self.__default_exchange
 
-        if asset_id is not None:
-            asset_id_cache = asset_id
+        if override_symbol is not None:
+            asset_id_cache = override_symbol
 
         if override_exchange is not None:
             exchange_cache = override_exchange
 
-        preferences = Blankly.utils.load_user_preferences()
-
         if channel not in self.__websockets.keys():
             self.__websockets[channel] = {}
 
@@ -55,10 +53,9 @@ def create_general_connection(self, callback, channel, log=None, asset_id=None,
         if asset_id_cache not in self.__websockets[channel][exchange_cache].keys():
             self.__websockets[channel][exchange_cache][asset_id_cache] = {}
 
-        use_sandbox = preferences['settings']['use_sandbox_websockets']
+        use_sandbox = self.preferences['settings']['use_sandbox_websockets']
 
         if exchange_cache == "coinbase_pro":
-            print(self.__websockets)
             if use_sandbox:
                 websocket = Coinbase_Pro_Websocket(asset_id_cache, channel, log,
                                                    WEBSOCKET_URL="wss://ws-feed-public.sandbox.pro.coinbase.com")
@@ -70,6 +67,8 @@ def create_general_connection(self, callback, channel, log=None, asset_id=None,
 
             return websocket
         elif exchange_cache == "binance":
+            # Lower this to subscribe
+            asset_id_cache = blankly.utils.to_exchange_coin_id(asset_id_cache, "binance").lower()
             if use_sandbox:
                 websocket = Binance_Websocket(asset_id_cache, channel, log,
                                               WEBSOCKET_URL="wss://testnet.binance.vision/ws")
@@ -77,6 +76,25 @@ def create_general_connection(self, callback, channel, log=None, asset_id=None,
                 websocket = Binance_Websocket(asset_id_cache, channel, log)
             websocket.append_callback(callback)
 
+            # Upper this to cache
+            asset_id_cache = asset_id_cache.upper()
+            self.__websockets[channel][exchange_cache][asset_id_cache] = websocket
+
+            return websocket
+
+        if exchange_cache == "alpaca":
+            stream = self.preferences['settings']['alpaca']['websocket_stream']
+
+            asset_id_cache = blankly.utils.to_exchange_coin_id(asset_id_cache, "alpaca")
+            if use_sandbox:
+                websocket = Alpaca_Websocket(asset_id_cache, channel, log,
+                                             WEBSOCKET_URL=
+                                             "wss://paper-api.alpaca.markets/stream/v2/{}/".format(stream))
+            else:
+                websocket = Alpaca_Websocket(asset_id_cache, channel, log,
+                                             WEBSOCKET_URL="wss://stream.data.alpaca.markets/v2/{}/".format(stream))
+            websocket.append_callback(callback)
+
             self.__websockets[channel][exchange_cache][asset_id_cache] = websocket
 
             return websocket
@@ -84,38 +102,39 @@ def create_general_connection(self, callback, channel, log=None, asset_id=None,
     """
     Create overriden method signatures
     """
-    def append_callback(self, callback_object, channel, override_currency=None, override_exchange=None):
+
+    def append_callback(self, callback_object, channel, override_symbol=None, override_exchange=None):
         self.websockets = self.__websockets[channel]
-        super().append_callback(callback_object, override_currency, override_exchange)
+        super().append_callback(callback_object, override_symbol, override_exchange)
 
-    def is_websocket_open(self, channel, override_currency=None, override_exchange=None):
+    def is_websocket_open(self, channel, override_symbol=None, override_exchange=None):
         self.websockets = self.__websockets[channel]
-        return super().is_websocket_open(override_currency, override_exchange)
+        return super().is_websocket_open(override_symbol, override_exchange)
 
-    def get_most_recent_time(self, channel, override_currency=None, override_exchange=None):
+    def get_most_recent_time(self, channel, override_symbol=None, override_exchange=None):
         self.websockets = self.__websockets[channel]
-        return super().get_most_recent_time(override_currency, override_exchange)
+        return super().get_most_recent_time(override_symbol, override_exchange)
 
-    def get_time_feed(self, channel, override_currency=None, override_exchange=None):
+    def get_time_feed(self, channel, override_symbol=None, override_exchange=None):
         self.websockets = self.__websockets[channel]
-        return super().get_time_feed(override_currency, override_exchange)
+        return super().get_time_feed(override_symbol, override_exchange)
 
-    def get_feed(self, channel, override_currency=None, override_exchange=None):
+    def get_feed(self, channel, override_symbol=None, override_exchange=None):
         self.websockets = self.__websockets[channel]
-        return super().get_feed(override_currency, override_exchange)
+        return super().get_feed(override_symbol, override_exchange)
 
-    def get_response(self, channel, override_currency=None, override_exchange=None):
+    def get_response(self, channel, override_symbol=None, override_exchange=None):
         self.websockets = self.__websockets[channel]
-        return super().get_response(override_currency, override_exchange)
+        return super().get_response(override_symbol, override_exchange)
 
-    def close_websocket(self, channel, override_currency=None, override_exchange=None):
+    def close_websocket(self, channel, override_symbol=None, override_exchange=None):
         self.websockets = self.__websockets[channel]
-        return super().close_websocket(override_currency, override_exchange)
+        return super().close_websocket(override_symbol, override_exchange)
 
-    def restart_ticker(self, channel, override_currency=None, override_exchange=None):
+    def restart_ticker(self, channel, override_symbol=None, override_exchange=None):
         self.websockets = self.__websockets[channel]
-        return super().restart_ticker(override_currency, override_exchange)
+        return super().restart_ticker(override_symbol, override_exchange)
 
-    def get_most_recent_tick(self, channel, override_currency=None, override_exchange=None):
+    def get_most_recent_tick(self, channel, override_symbol=None, override_exchange=None):
         self.websockets = self.__websockets[channel]
-        return super().get_most_recent_tick(override_currency, override_exchange)
+        return super().get_most_recent_tick(override_symbol, override_exchange)
diff --git a/blankly/exchanges/managers/orderbook_manager.py b/blankly/exchanges/managers/orderbook_manager.py
new file mode 100644
index 00000000..eb48b953
--- /dev/null
+++ b/blankly/exchanges/managers/orderbook_manager.py
@@ -0,0 +1,353 @@
+"""
+    Class to manage the orderbook, adding, removing and updating - as well as provide user interaction
+    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 <https://www.gnu.org/licenses/>.
+"""
+import traceback
+import warnings
+from typing import List
+
+import requests
+
+import blankly.exchanges.auth.auth_constructor
+import blankly.utils.utils
+from blankly.exchanges.interfaces.alpaca.alpaca_websocket import Tickers as Alpaca_Websocket
+from blankly.exchanges.interfaces.binance.binance_websocket import Tickers as Binance_Orderbook
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket import Tickers as Coinbase_Pro_Orderbook
+from blankly.exchanges.managers.websocket_manager import WebsocketManager
+
+
+def sort_list_tuples(list_with_tuples: list) -> List[tuple]:
+    return sorted(list_with_tuples, key=lambda x: x[0])
+
+
+def binance_snapshot(symbol, limit):
+    # TODO this should be using the REST set that Arun puts in
+    params = {
+        "symbol": symbol,
+        "limit": limit
+    }
+    response = requests.get("https://api.binance.com/api/v3/depth", params=params).json()
+    try:
+        buys_response = response['bids']
+    except KeyError:
+        raise KeyError(response)
+    sells_response = response['asks']
+    buys = []
+    sells = []
+    for i in buys_response:
+        buys.append((float(i[0]), float(i[1])))
+    for i in sells_response:
+        sells.append((float(i[0]), float(i[1])))
+
+    buys = sort_list_tuples(buys)
+    sells = sort_list_tuples(sells)
+    print("Orderbook snapshot acquired for: " + symbol.upper())
+    return buys, sells
+
+
+def remove_price(book: list, price: float) -> list:
+    for j in range(len(book)):
+        price_at_index = book[j][0]
+        if price_at_index > price:
+            break
+        if price_at_index == price:
+            book.pop(j)
+            break
+
+    return book
+
+
+class OrderbookManager(WebsocketManager):
+    def __init__(self, default_exchange, default_symbol):
+        """
+        Create a new orderbook manager
+        Args:
+            default_exchange: Add an exchange name for the manager to favor
+            default_symbol: Add a default currency for the manager to favor
+        """
+        self.__default_exchange = default_exchange
+        self.__default_currency = default_symbol
+
+        self.__orderbooks = {}
+        self.__orderbooks[default_exchange] = {}
+
+        self.__websockets = {}
+        self.__websockets[default_exchange] = {}
+
+        self.__websockets_callbacks = {}
+        self.__websockets_callbacks[default_exchange] = {}
+
+        self.__websockets_kwargs = {}
+        self.__websockets_kwargs[default_exchange] = {}
+
+        # Create the abstraction for adding many managers
+        super().__init__(self.__websockets, default_symbol, default_exchange)
+
+    def create_orderbook(self, callback,
+                         override_symbol=None,
+                         override_exchange=None,
+                         initially_stopped=False,
+                         **kwargs):
+        """
+        Create an orderbook for a given exchange
+        Args:
+            callback: Callback object for the function. Should be something like self.price_event
+            override_symbol: Override the default currency id
+            override_exchange: Override the default exchange
+            initially_stopped: Keep the websocket stopped when created
+            kwargs: Add any other parameters that should be passed into a callback function to identify
+                it or modify behavior
+        """
+
+        use_sandbox = self.preferences['settings']['use_sandbox_websockets']
+
+        exchange_name = self.__default_exchange
+        # Ensure the ticker dict has this overridden exchange
+        if override_exchange is not None:
+            if override_exchange not in self.__websockets.keys():
+                self.__websockets[override_exchange] = {}
+                self.__websockets_callbacks[override_exchange] = {}
+                self.__websockets_kwargs[override_exchange] = {}
+            # Write this value so it can be used later
+            exchange_name = override_exchange
+
+        if exchange_name == "coinbase_pro":
+            if override_symbol is None:
+                override_symbol = self.__default_currency
+
+            if use_sandbox:
+                websocket = Coinbase_Pro_Orderbook(override_symbol, "level2",
+                                                   pre_event_callback=self.coinbase_snapshot_update,
+                                                   initially_stopped=initially_stopped,
+                                                   WEBSOCKET_URL="wss://ws-feed-public.sandbox.pro.coinbase.com")
+            else:
+                websocket = Coinbase_Pro_Orderbook(override_symbol, "level2",
+                                                   pre_event_callback=self.coinbase_snapshot_update,
+                                                   initially_stopped=initially_stopped
+                                                   )
+            # This is where the sorting magic happens
+            websocket.append_callback(self.coinbase_update)
+
+            # Store this object
+            self.__websockets['coinbase_pro'][override_symbol] = websocket
+            self.__websockets_callbacks['coinbase_pro'][override_symbol] = [callback]
+            self.__websockets_kwargs['coinbase_pro'][override_symbol] = kwargs
+            self.__orderbooks['coinbase_pro'][override_symbol] = {
+                "bids": [],
+                "asks": []
+            }
+            return websocket
+        elif exchange_name == "binance":
+            if override_symbol is None:
+                override_symbol = self.__default_currency
+
+            # Lower the keys to subscribe
+            specific_currency_id = blankly.utils.to_exchange_coin_id(override_symbol, "binance").lower()
+
+            if use_sandbox:
+                websocket = Binance_Orderbook(specific_currency_id, "depth", initially_stopped=initially_stopped,
+                                              WEBSOCKET_URL="wss://testnet.binance.vision/ws")
+            else:
+                websocket = Binance_Orderbook(specific_currency_id, "depth", initially_stopped=initially_stopped)
+
+            websocket.append_callback(self.binance_update)
+
+            # binance returns the keys in all UPPER so the books should be created based on response
+            specific_currency_id = specific_currency_id.upper()
+            self.__websockets['binance'][specific_currency_id] = websocket
+            self.__websockets_callbacks['binance'][specific_currency_id] = [callback]
+            self.__websockets_kwargs['binance'][specific_currency_id] = kwargs
+
+            buys, sells = binance_snapshot(specific_currency_id, 1000)
+            self.__orderbooks['binance'][specific_currency_id] = {
+                "bids": buys,
+                "asks": sells
+            }
+
+        elif exchange_name == "alpaca":
+            warning_string = "Alpaca only allows the viewing of the bid/ask spread, not a total orderbook."
+            warnings.warn(warning_string)
+            if override_symbol is None:
+                override_symbol = self.__default_currency
+
+            stream = self.preferences['settings']['alpaca']['websocket_stream']
+            override_symbol = blankly.utils.to_exchange_coin_id(override_symbol, "alpaca")
+
+            if use_sandbox:
+                websocket = Alpaca_Websocket(override_symbol, 'quotes', initially_stopped=initially_stopped,
+                                             WEBSOCKET_URL=
+                                             "wss://paper-api.alpaca.markets/stream/v2/{}/".format(stream))
+            else:
+                websocket = Alpaca_Websocket(override_symbol, 'quotes', initially_stopped=initially_stopped,
+                                             WEBSOCKET_URL="wss://stream.data.alpaca.markets/v2/{}/".format(stream))
+
+            websocket.append_callback(self.alpaca_update)
+
+            self.__websockets['alpaca'][override_symbol] = websocket
+            self.__websockets_callbacks['alpaca'][override_symbol] = [callback]
+            self.__websockets_kwargs['alpaca'][override_symbol] = kwargs
+
+            self.__orderbooks['alpaca'][override_symbol] = {
+                "bids": [],
+                "asks": [],
+            }
+
+        else:
+            print(exchange_name + " ticker not supported, skipping creation")
+
+    def coinbase_snapshot_update(self, update):
+        print("Orderbook snapshot acquired for: " + update['product_id'])
+        # Clear whatever book we had
+        book = {
+            "bids": [],
+            "asks": []
+        }
+        # Get all bids
+        buys = update['bids']
+        # Convert these to float and write to our order dictionaries
+        for i in range(len(buys)):
+            buy = buys[i]
+            book['bids'].append((float(buy[0]), float(buy[1])))
+
+        sells = update['asks']
+        for i in range(len(sells)):
+            sell = sells[i]
+            book['asks'].append((float(sell[0]), float(sell[1])))
+
+        book["bids"] = sort_list_tuples(book["bids"])
+        book["asks"] = sort_list_tuples(book["asks"])
+
+        self.__orderbooks['coinbase_pro'][update['product_id']] = book
+
+    def coinbase_update(self, update):
+        # Side is first in list
+        side = update['changes'][0][0]
+
+        # Have to convert coinbase to use this
+        if side == 'buy':
+            side = 'bids'
+        elif side == 'sell':
+            side = 'asks'
+
+        # Price is second
+        price = float(update['changes'][0][1])
+        qty = float(update['changes'][0][2])
+        book = self.__orderbooks['coinbase_pro'][update['product_id']][side]  # type: list
+
+        # Quantity at that point is third
+        if qty == 0:
+            book = remove_price(book, price)
+        else:
+            book.append((price, qty))
+
+        book = sort_list_tuples(book)
+        self.__orderbooks['coinbase_pro'][update['product_id']][side] = book
+
+        # Iterate through the callback list
+        callbacks = self.__websockets_callbacks['coinbase_pro'][update['product_id']]
+        for i in callbacks:
+            i(self.__orderbooks['coinbase_pro'][update['product_id']],
+              **self.__websockets_kwargs['coinbase_pro'][update['product_id']])
+
+    def binance_update(self, update):
+        try:
+            # TODO this needs a snapshot to work correctly, which needs arun's rest code
+            # Get symbol first
+            symbol = update['s']
+
+            # Get symbol for orderbook
+            book_buys = self.__orderbooks['binance'][symbol]['bids']  # type: list
+            book_sells = self.__orderbooks['binance'][symbol]['asks']  # type: list
+
+            # Buys are b, count from low to high with reverse (which is the ::-1 thing)
+            new_buys = update['b'][::-1]  # type: list
+            for i in new_buys:
+                i[0] = float(i[0])
+                i[1] = float(i[1])
+                if i[1] == 0:
+                    book_buys = remove_price(book_buys, i[0])
+                else:
+                    book_buys.append((i[0], i[1]))
+
+            # Asks are sells, these are also counted from low to high
+            new_sells = update['a']  # type: list
+            for i in new_sells:
+                i[0] = float(i[0])
+                i[1] = float(i[1])
+                if i[1] == 0:
+                    book_sells = remove_price(book_sells, i[0])
+                else:
+                    book_sells.append((i[0], i[1]))
+
+            # Now sort them
+            book_buys = sort_list_tuples(book_buys)
+            book_sells = sort_list_tuples(book_sells)
+
+            self.__orderbooks['binance'][symbol]['bids'] = book_buys
+            self.__orderbooks['binance'][symbol]['asks'] = book_sells
+
+            # Pass in this new updated orderbook
+            callbacks = self.__websockets_callbacks['binance'][symbol]
+            for i in callbacks:
+                i(self.__orderbooks['binance'][symbol],
+                  **self.__websockets_kwargs['binance'][symbol])
+        except Exception:
+            traceback.print_exc()
+
+    def alpaca_update(self, update: dict):
+        # Alpaca only gives the spread, no orderbook depth (alpaca is very bad)
+        symbol = update['S']
+        self.__orderbooks['alpaca'][symbol]['bids'] = [(update['bp'], update['bs'])]
+        self.__orderbooks['alpaca'][symbol]['asks'] = [(update['ap'], update['as'])]
+
+        callbacks = self.__websockets_callbacks['alpaca'][symbol]
+        for i in callbacks:
+            i(self.__orderbooks['alpaca'][symbol],
+              **self.__websockets_kwargs['alpaca'][symbol])
+
+    def append_orderbook_callback(self, callback_object, override_symbol=None, override_exchange=None):
+        """
+        These are appended calls to a sorted orderbook. Functions added to this will be fired every time the orderbook
+        changes.
+        Args:
+            callback_object: Reference for the callback function. The price_event(self, tick)
+                function would be passed in as just self.price_event -- no parenthesis or arguments, just the reference
+            override_symbol: Ticker id, such as "BTC-USD" or exchange equivalents.
+            override_exchange: Forces the manager to use a different supported exchange.
+        """
+        if override_symbol is None:
+            override_symbol = self.__default_currency
+
+        if override_exchange is None:
+            override_exchange = self.__default_exchange
+
+        self.__websockets_callbacks[override_exchange][override_symbol].append(callback_object)
+
+    def get_most_recent_orderbook(self, override_symbol=None, override_exchange=None):
+        """
+        Get the most recent orderbook under a currency and exchange.
+
+        Args:
+            override_symbol: Ticker id, such as "BTC-USD" or exchange equivalents.
+            override_exchange: Forces the manager to use a different supported exchange.
+        """
+        if override_symbol is None:
+            override_symbol = self.__default_currency
+
+        if override_exchange is None:
+            override_exchange = self.__default_exchange
+
+        return self.__orderbooks[override_exchange][override_symbol]
diff --git a/blankly/exchanges/managers/ticker_manager.py b/blankly/exchanges/managers/ticker_manager.py
new file mode 100644
index 00000000..66760ff2
--- /dev/null
+++ b/blankly/exchanges/managers/ticker_manager.py
@@ -0,0 +1,125 @@
+"""
+    Script for managing the variety of tickers on different 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 <https://www.gnu.org/licenses/>.
+"""
+import blankly.utils.utils
+from blankly.exchanges.interfaces.alpaca.alpaca_websocket import Tickers as Alpaca_Ticker
+from blankly.exchanges.interfaces.binance.binance_websocket import Tickers as Binance_Ticker
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket import Tickers as Coinbase_Pro_Ticker
+from blankly.exchanges.managers.websocket_manager import WebsocketManager
+
+
+class TickerManager(WebsocketManager):
+    def __init__(self, default_exchange: str, default_symbol: str):
+        """
+        Create a new manager.
+        Args:
+            default_exchange: Add an exchange name for the manager to favor
+            default_symbol: Add a default currency for the manager to favor
+        """
+        self.__default_exchange = default_exchange
+        if default_exchange == "binance":
+            default_symbol = blankly.utils.to_exchange_coin_id(default_symbol, "binance").lower()
+        elif default_exchange == "alpaca":
+            default_symbol = blankly.utils.to_exchange_coin_id(default_symbol, "alpaca")
+
+        self.__default_symbol = default_symbol
+
+        self.__tickers = {}
+        self.__tickers[default_exchange] = {}
+
+        # Create abstraction for writing many different managers
+        super().__init__(self.__tickers, default_symbol, default_exchange)
+
+    """ 
+    Manager Functions 
+    """
+
+    def create_ticker(self, callback, log: str = None, override_symbol: str = None, override_exchange: str = None):
+        """
+        Create a ticker on a given exchange.
+        Args:
+            callback: Callback object for the function. Should be something like self.price_event
+            log: Fill this with a path to log the price updates.
+            override_symbol: The currency to create a ticker for.
+            override_exchange: Override the default exchange.
+        Returns:
+            Direct ticker object
+        """
+
+        sandbox_mode = self.preferences['settings']['use_sandbox_websockets']
+
+        exchange_name = self.__default_exchange
+        # Ensure the ticker dict has this overridden exchange
+        if override_exchange is not None:
+            if override_exchange not in self.__tickers.keys():
+                self.__tickers[override_exchange] = {}
+            # Write this value so it can be used later
+            exchange_name = override_exchange
+
+        if exchange_name == "coinbase_pro":
+            if override_symbol is None:
+                override_symbol = self.__default_symbol
+
+            if sandbox_mode:
+                ticker = Coinbase_Pro_Ticker(override_symbol, "ticker", log=log,
+                                             WEBSOCKET_URL="wss://ws-feed-public.sandbox.pro.coinbase.com")
+            else:
+                ticker = Coinbase_Pro_Ticker(override_symbol, "ticker", log=log)
+
+            ticker.append_callback(callback)
+            # Store this object
+            self.__tickers['coinbase_pro'][override_symbol] = ticker
+            return ticker
+        elif exchange_name == "binance":
+            if override_symbol is None:
+                override_symbol = self.__default_symbol
+
+            override_symbol = blankly.utils.to_exchange_coin_id(override_symbol, "binance").lower()
+            if sandbox_mode:
+                ticker = Binance_Ticker(override_symbol,
+                                        "trades",
+                                        log=log,
+                                        WEBSOCKET_URL="wss://testnet.binance.vision/ws")
+            else:
+                ticker = Binance_Ticker(override_symbol,
+                                        "trades",
+                                        log=log)
+            ticker.append_callback(callback)
+            self.__tickers['binance'][override_symbol] = ticker
+            return ticker
+        elif exchange_name == "alpaca":
+            stream = self.preferences['settings']['alpaca']['websocket_stream']
+            if override_symbol is None:
+                override_symbol = self.__default_symbol
+
+            override_symbol = blankly.utils.to_exchange_coin_id(override_symbol, "alpaca")
+            if sandbox_mode:
+                ticker = Alpaca_Ticker(override_symbol,
+                                       "trades",
+                                       log=log,
+                                       WEBSOCKET_URL="wss://paper-api.alpaca.markets/stream/v2/{}/".format(stream))
+            else:
+                ticker = Alpaca_Ticker(override_symbol,
+                                       "trades",
+                                       log=log,
+                                       WEBSOCKET_URL="wss://stream.data.alpaca.markets/v2/{}/".format(stream))
+            ticker.append_callback(callback)
+            self.__tickers['alpaca'][override_symbol] = ticker
+            return ticker
+
+        else:
+            print(exchange_name + " ticker not supported, skipping creation")
diff --git a/Blankly/exchanges/websocket_manager.py b/blankly/exchanges/managers/websocket_manager.py
similarity index 63%
rename from Blankly/exchanges/websocket_manager.py
rename to blankly/exchanges/managers/websocket_manager.py
index d3ceebbe..4265efbc 100644
--- a/Blankly/exchanges/websocket_manager.py
+++ b/blankly/exchanges/managers/websocket_manager.py
@@ -15,16 +15,18 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-import Blankly.utils.utils
-from Blankly.exchanges.IExchange_Websocket import IExchangeWebsocket
+import blankly.utils.utils
+from blankly.exchanges.abc_exchange_websocket import ABCExchangeWebsocket
 
 
-class WebsocketManager(IExchangeWebsocket):
-    def __init__(self, websockets, default_currency, default_exchange):
+class WebsocketManager(ABCExchangeWebsocket):
+    def __init__(self, websockets, default_symbol, default_exchange):
         self.websockets = websockets
-        self.__default_currency = default_currency
+        self.__default_symbol = default_symbol
         self.__default_exchange = default_exchange
 
+        self.preferences = blankly.utils.load_user_preferences()
+
     def close_all_websockets(self):
         """
         Iterate through orderbooks and make sure they're closed
@@ -38,36 +40,36 @@ def __recursive_close(self, d):
             else:
                 d[k].close_websocket()
 
-    def __evaluate_overrides(self, override_currency, override_exchange):
+    def __evaluate_overrides(self, override_symbol, override_exchange):
         """
         Switches to inputted exchanges, used in the public methods
         """
         # Issues could arise if the user specifies a different exchange but not a different currency. It would run
         # that default on the new exchange. Not great behavior unless they're clever
-        if override_currency is not None:
-            currency_id = override_currency
+        if override_symbol is not None:
+            currency_id = override_symbol
         else:
-            currency_id = self.__default_currency
+            currency_id = self.__default_symbol
         if override_exchange is not None:
             exchange = override_exchange
         else:
             exchange = self.__default_exchange
 
         if exchange == "binance":
-            currency_id = Blankly.utils.to_exchange_coin_id(currency_id, "binance")
+            currency_id = blankly.utils.to_exchange_coin_id(currency_id, "binance")
         return self.websockets[exchange][currency_id]
 
-    def get_ticker(self, currency_id, override_exchange=None):
+    def get_ticker(self, symbol, override_exchange=None):
         """
         Retrieve the ticker attached to a currency
         Args:
-            currency_id: The currency ID of ticker (required) such as "BTC-USD"
+            symbol: The currency ID of ticker (required) such as "BTC-USD"
             override_exchange: Override the default to get tickers for different exchanges
         """
         if override_exchange is None:
-            return self.websockets[self.__default_exchange][currency_id]
+            return self.websockets[self.__default_exchange][symbol]
         else:
-            return self.websockets[override_exchange][currency_id]
+            return self.websockets[override_exchange][symbol]
 
     def get_all_tickers(self) -> dict:
         """
@@ -80,7 +82,8 @@ def get_all_tickers(self) -> dict:
     """
     Ticker functions/overridden
     """
-    def append_callback(self, callback_object, override_currency=None, override_exchange=None):
+
+    def append_callback(self, callback_object, override_symbol=None, override_exchange=None):
         """
         This bypasses all processing that a manager class may do before returning to the user's main.
         For example with an orderbook feed, this will return the ticks instead of a sorted orderbook
@@ -89,74 +92,74 @@ def append_callback(self, callback_object, override_currency=None, override_exch
         Args:
             callback_object: Reference for the callback function. The price_event(self, tick)
                 function would be passed in as just self.price_event  -- no parenthesis or arguments, just the object
-            override_currency: Ticker id, such as "BTC-USD" or exchange equivalents.
+            override_symbol: Ticker id, such as "BTC-USD" or exchange equivalents.
             override_exchange: Forces the manager to use a different supported exchange.
         """
-        websocket = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_symbol, override_exchange)
 
         websocket.append_callback(callback_object)
 
-    def is_websocket_open(self, override_currency=None, override_exchange=None) -> bool:
+    def is_websocket_open(self, override_symbol=None, override_exchange=None) -> bool:
         """
         Check if the websocket attached to a currency is open
         """
-        websocket = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_symbol, override_exchange)
 
         return websocket.is_websocket_open()
 
-    def get_most_recent_time(self, override_currency=None, override_exchange=None):
+    def get_most_recent_time(self, override_symbol=None, override_exchange=None):
         """
         Get the most recent time associated with the most recent tick
         """
-        websocket = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_symbol, override_exchange)
 
         return websocket.get_most_recent_time()
 
-    def get_time_feed(self, override_currency=None, override_exchange=None):
+    def get_time_feed(self, override_symbol=None, override_exchange=None):
         """
         Get a time array associated with the ticker feed.
         """
-        websocket = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_symbol, override_exchange)
 
         return websocket.get_time_feed()
 
-    def get_feed(self, override_currency=None, override_exchange=None):
+    def get_feed(self, override_symbol=None, override_exchange=None):
         """
         Get the full ticker array. This can be extremely large.
         """
-        websocket = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_symbol, override_exchange)
 
         return websocket.get_feed()
 
-    def get_response(self, override_currency=None, override_exchange=None):
+    def get_response(self, override_symbol=None, override_exchange=None):
         """
         Get the exchange's response to the request to subscribe to a feed
         """
-        websocket = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_symbol, override_exchange)
 
         return websocket.get_response()
 
-    def close_websocket(self, override_currency=None, override_exchange=None):
+    def close_websocket(self, override_symbol=None, override_exchange=None):
         """
         Close a websocket thread
         """
-        websocket = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_symbol, override_exchange)
 
         websocket.close_websocket()
 
-    def restart_ticker(self, override_currency=None, override_exchange=None):
+    def restart_ticker(self, override_symbol=None, override_exchange=None):
         """
         Restart a websocket feed after asking it to stop
         """
-        websocket = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_symbol, override_exchange)
 
         websocket.restart_ticker()
 
-    def get_most_recent_tick(self, override_currency=None, override_exchange=None):
+    def get_most_recent_tick(self, override_symbol=None, override_exchange=None):
         """
         Get the most recent tick received
         """
-        websocket = self.__evaluate_overrides(override_currency, override_exchange)
-        # TODO fix the returned value below, really this needs a class like in Binance that can create a callback to
+        websocket = self.__evaluate_overrides(override_symbol, override_exchange)
+        # TODO fix the returned value below, really this needs a class like in binance that can create a callback to
         #  allow a pointer to be subbed in for whichever exchange/currency/websocket type is overridden
         return websocket.get_most_recent_tick()
diff --git a/Blankly/utils/__init__.py b/blankly/exchanges/orders/__init__.py
similarity index 100%
rename from Blankly/utils/__init__.py
rename to blankly/exchanges/orders/__init__.py
diff --git a/Blankly/utils/purchases/limit_order.py b/blankly/exchanges/orders/limit_order.py
similarity index 83%
rename from Blankly/utils/purchases/limit_order.py
rename to blankly/exchanges/orders/limit_order.py
index 804022fa..5f186e24 100644
--- a/Blankly/utils/purchases/limit_order.py
+++ b/blankly/exchanges/orders/limit_order.py
@@ -15,7 +15,7 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-from Blankly.utils.purchases.order import Order
+from blankly.exchanges.orders.order import Order
 
 
 class LimitOrder(Order):
@@ -45,7 +45,7 @@ def __init__(self, order, response, interface):
             "settled": false
         }
 
-        Binance Response:
+        binance Response:
         {
             "symbol": "BTCUSDT",
             "orderId": 28,
@@ -62,7 +62,7 @@ def __init__(self, order, response, interface):
 
         Guaranteed:
         needed = [
-            ["product_id", str],
+            ["symbol", str],
             ["id", str],
             ["created_at", float],
             ["price", float],
@@ -87,12 +87,34 @@ def get_price(self) -> float:
         """
         return self.__response["price"]
 
+    def get_time_in_force(self) -> str:
+        """
+        Get the exchange's set time_in_force value.
+        """
+        return self.__response["time_in_force"]
+
     def get_quantity(self) -> float:
         """
         Get the quantity of order, Ex: .004 bitcoin
         """
         return self.__response["size"]
 
+    def __str__(self):
+        return_string = super().__str__()
+
+        return_string = self.add_new_line(return_string, "Limit Order Parameters: ")
+
+        return_string = self.add_new_line(return_string, "Time In Force: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_time_in_force())
+
+        return_string = self.add_new_line(return_string, "Price: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_price())
+
+        return_string = self.add_new_line(return_string, "Quantity: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_quantity())
+
+        return return_string
+
     """ This is something that should be implemented in the future """
     # """ Returns true if the buy order can be sold at a profit or the sell order didn't loose money """
     # def is_profitable(self, show=False):
@@ -116,7 +138,7 @@ def get_quantity(self) -> float:
     #             return False
     #
     # def get_fee(self, show=False):
-    #     fee = (self.__valueAtTime * self.__amountCurrency) * Blankly.Constants.PRETEND_FEE_RATE
+    #     fee = (self.__valueAtTime * self.__amountCurrency) * blankly.Constants.PRETEND_FEE_RATE
     #     if show:
     #         print(fee)
     #     return fee
diff --git a/Blankly/utils/purchases/market_order.py b/blankly/exchanges/orders/market_order.py
similarity index 89%
rename from Blankly/utils/purchases/market_order.py
rename to blankly/exchanges/orders/market_order.py
index 211d9dc3..ec2bbaeb 100644
--- a/Blankly/utils/purchases/market_order.py
+++ b/blankly/exchanges/orders/market_order.py
@@ -15,7 +15,7 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-from Blankly.utils.purchases.order import Order
+from blankly.exchanges.orders.order import Order
 
 
 class MarketOrder(Order):
@@ -44,12 +44,12 @@ def __init__(self, order, response, interface):
             'settled': False
         }
 
-        Binance Response:
+        binance Response:
         TODO add this
 
         Guaranteed:
         needed = [
-            ["product_id", str],
+            ["symbol", str],
             ["id", str],
             ["created_at", float],
             ["funds", float],
@@ -71,6 +71,16 @@ def get_funds(self) -> float:
         """
         return self.__response["funds"]
 
+    def __str__(self):
+        return_string = super().__str__()
+
+        return_string = self.add_new_line(return_string, "Market Order Parameters: ")
+
+        return_string = self.add_new_line(return_string, "Funds: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_funds())
+
+        return return_string
+
     """ This is something that should be implemented in the future """
     # """ Returns true if the buy order can be sold at a profit or the sell order didn't loose money """
     # def is_profitable(self, show=False):
@@ -94,7 +104,7 @@ def get_funds(self) -> float:
     #             return False
     #
     # def get_fee(self, show=False):
-    #     fee = (self.__valueAtTime * self.__amountCurrency) * Blankly.Constants.PRETEND_FEE_RATE
+    #     fee = (self.__valueAtTime * self.__amountCurrency) * blankly.Constants.PRETEND_FEE_RATE
     #     if show:
     #         print(fee)
     #     return fee
diff --git a/Blankly/utils/purchases/order.py b/blankly/exchanges/orders/order.py
similarity index 59%
rename from Blankly/utils/purchases/order.py
rename to blankly/exchanges/orders/order.py
index a936e156..2afa98ab 100644
--- a/Blankly/utils/purchases/order.py
+++ b/blankly/exchanges/orders/order.py
@@ -15,6 +15,7 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
+from blankly.utils.utils import pretty_print_JSON
 
 
 class Order:
@@ -22,7 +23,7 @@ def __init__(self, response, order, interface):
         """
         Limit Order:
         needed = [
-            ["product_id", str],    <- similar
+            ["symbol", str],    <- similar
             ["id", str],    <- similar
             ["created_at", float],  <- similar
             ["price", float],
@@ -35,7 +36,7 @@ def __init__(self, response, order, interface):
 
         Market Order:
         needed = [
-            ["product_id", str],
+            ["symbol", str],
             ["id", str],
             ["created_at", float],
             ["funds", float],
@@ -48,6 +49,40 @@ def __init__(self, response, order, interface):
         self.__order = order
         self.Interface = interface
 
+    def add_new_line(self, input_string, new_line, newline=True):
+        input_string += str(new_line)
+        if newline:
+            input_string += "\n"
+        return input_string
+
+    def __str__(self):
+        return_string = ""
+        return_string = self.add_new_line(return_string, "General Order Parameters: ")
+
+        return_string = self.add_new_line(return_string, "Response: ")
+        return_string = self.add_new_line(return_string, pretty_print_JSON(self.get_response(), actually_print=False))
+
+        return_string = self.add_new_line(return_string, "Full Status: ")
+        return_string = self.add_new_line(return_string, pretty_print_JSON(self.get_status(full=True),
+                                                                           actually_print=False))
+
+        return_string = self.add_new_line(return_string, "ID: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_id())
+
+        return_string = self.add_new_line(return_string, "Symbol: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_asset_id())
+
+        return_string = self.add_new_line(return_string, "Purchase Time: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_purchase_time())
+
+        return_string = self.add_new_line(return_string, "Type: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_type())
+
+        return_string = self.add_new_line(return_string, "Side: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_side())
+
+        return return_string
+
     def get_response(self) -> dict:
         """
         Get the original but parsed response from the exchange.
@@ -60,11 +95,11 @@ def get_id(self) -> str:
         """
         return self.__response["id"]
 
-    def get_product_id(self) -> str:
+    def get_asset_id(self) -> str:
         """
         Get the asset id of the exchange
         """
-        return self.__response["product_id"]
+        return self.__response["symbol"]
 
     def get_purchase_time(self) -> float:
         """
@@ -81,15 +116,9 @@ def get_status(self, full=False) -> dict:
             field as (example) {"status:" "NEW"}
         """
         if full:
-            return self.Interface.get_order(self.__order["product_id"], self.get_id())
+            return self.Interface.get_order(self.__order["symbol"], self.get_id())
         else:
-            return {"status": self.Interface.get_order(self.__order["product_id"], self.get_id())["status"]}
-
-    def get_time_in_force(self) -> str:
-        """
-        Get the exchange's set time_in_force value.
-        """
-        return self.__response["time_in_force"]
+            return {"status": self.Interface.get_order(self.__order["symbol"], self.get_id())["status"]}
 
     def get_type(self) -> str:
         """
diff --git a/Blankly/utils/purchases/stop_limit.py b/blankly/exchanges/orders/stop_limit.py
similarity index 71%
rename from Blankly/utils/purchases/stop_limit.py
rename to blankly/exchanges/orders/stop_limit.py
index dac9dcc0..0f00b10b 100644
--- a/Blankly/utils/purchases/stop_limit.py
+++ b/blankly/exchanges/orders/stop_limit.py
@@ -15,7 +15,7 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-from Blankly.utils.purchases.order import Order
+from blankly.exchanges.orders.order import Order
 
 
 class StopLimit(Order):
@@ -47,12 +47,12 @@ def __init__(self, order, response, interface):
             'status': 'pending',
             'settled': False
         }
-        Binance Response:
+        binance Response:
         TODO add this
 
         Guaranteed:
         needed = [
-                ["product_id", str],
+                ["symbol", str],
                 ["id", str],  <-- Handled by order
                 ["created_at", float],  <-- Handled by order
                 ["stop_price", float],
@@ -95,3 +95,31 @@ def get_size(self) -> float:
         Get the amount of base currency set in the triggered stop limit order
         """
         return self.__response["size"]
+
+    def get_time_in_force(self) -> str:
+        """
+        Get the exchange's set time_in_force value.
+        """
+        return self.__response["time_in_force"]
+
+    def __str__(self):
+        return_string = super().__str__()
+
+        return_string = self.add_new_line(return_string, "Stop Order Parameters: ")
+
+        return_string = self.add_new_line(return_string, "Time In Force: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_time_in_force())
+
+        return_string = self.add_new_line(return_string, "Stop Price: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_stop_price())
+
+        return_string = self.add_new_line(return_string, "Stop Limit Price: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_limit_price())
+
+        return_string = self.add_new_line(return_string, "Stop Type: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_stop_type())
+
+        return_string = self.add_new_line(return_string, "Get Size: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_size())
+
+        return return_string
diff --git a/blankly/indicators/__init__.py b/blankly/indicators/__init__.py
new file mode 100644
index 00000000..c0f6190d
--- /dev/null
+++ b/blankly/indicators/__init__.py
@@ -0,0 +1,5 @@
+from blankly.indicators.indicators import *
+from blankly.indicators.moving_averages import *
+from blankly.indicators.oscillators import *
+from blankly.indicators.statistics import *
+from blankly.indicators.utils import *
diff --git a/blankly/indicators/indicators.py b/blankly/indicators/indicators.py
new file mode 100644
index 00000000..abcd1a25
--- /dev/null
+++ b/blankly/indicators/indicators.py
@@ -0,0 +1,75 @@
+"""
+    Tulipy wrappers
+    Copyright (C) 2021 Brandon Fan
+
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+import pandas as pd
+import tulipy as ti
+
+from blankly.indicators.utils import check_series, convert_to_numpy
+
+
+def bbands(data, period=14, stddev=2):
+    data = convert_to_numpy(data)
+    return ti.bbands(data, period, stddev)
+
+
+def wad(high_data, low_data, close_data, use_series=False):
+    if check_series(high_data) or check_series(low_data) or check_series(close_data):
+        use_series = True
+    high_data = convert_to_numpy(high_data)
+    low_data = convert_to_numpy(low_data)
+    close_data = convert_to_numpy(close_data)
+    wad = ti.wad(high_data, low_data, close_data)
+    return pd.Series(wad) if use_series else wad
+
+
+def wilders(data, period=50, use_series=False):
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    wilders = ti.wilders(data, period)
+    return pd.Series(wilders) if use_series else wilders
+
+
+def willr(high_data, low_data, close_data, period=50, use_series=False):
+    if check_series(high_data) or check_series(low_data) or check_series(close_data):
+        use_series = True
+    high_data = convert_to_numpy(high_data)
+    low_data = convert_to_numpy(low_data)
+    close_data = convert_to_numpy(close_data)
+    willr = ti.willr(high_data, low_data, close_data, period)
+    return pd.Series(willr) if use_series else willr
+
+
+def true_range(high_data, low_data, close_data, use_series=False):
+    if check_series(high_data) or check_series(low_data) or check_series(close_data):
+        use_series = True
+    high_data = convert_to_numpy(high_data)
+    low_data = convert_to_numpy(low_data)
+    close_data = convert_to_numpy(close_data)
+    tr = ti.tr(high_data, low_data, close_data)
+    return pd.Series(tr) if use_series else tr
+
+
+def average_true_range(high_data, low_data, close_data, period=50, use_series=False):
+    if check_series(high_data) or check_series(low_data) or check_series(close_data):
+        use_series = True
+    high_data = convert_to_numpy(high_data)
+    low_data = convert_to_numpy(low_data)
+    close_data = convert_to_numpy(close_data)
+    atr = ti.atr(high_data, low_data, close_data, period=period)
+    return pd.Series(atr) if use_series else atr
diff --git a/Blankly/indicators/moving_averages.py b/blankly/indicators/moving_averages.py
similarity index 51%
rename from Blankly/indicators/moving_averages.py
rename to blankly/indicators/moving_averages.py
index b0354d97..1633eb54 100644
--- a/Blankly/indicators/moving_averages.py
+++ b/blankly/indicators/moving_averages.py
@@ -1,9 +1,28 @@
-from Blankly.indicators.utils import convert_to_numpy
-from Blankly.indicators.utils import check_series
+"""
+    Moving average function utils
+    Copyright (C) 2021 Brandon Fan
+
+    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 <https://www.gnu.org/licenses/>.
+"""
+
 from typing import Any
+
 import pandas as pd
 import tulipy as ti
 
+from blankly.indicators.utils import check_series, convert_to_numpy
+
 
 def ema(data: Any, period: int = 50, use_series=False) -> Any:
     if check_series(data):
@@ -13,20 +32,18 @@ def ema(data: Any, period: int = 50, use_series=False) -> Any:
     return pd.Series(ema) if use_series else ema
 
 
-def vwma(data: Any, volume_data: Any, period: int = 50) -> Any:
-    use_series = False
+def vwma(data: Any, volume_data: Any, period: int = 50, use_series=False) -> Any:
     if check_series(data):
         use_series = True
 
     data = convert_to_numpy(data)
-    volume_data = convert_to_numpy(volume_data)
+    volume_data = convert_to_numpy(volume_data).astype(float)
 
     vwma = ti.vwma(data, volume_data, period=period)
     return pd.Series(vwma) if use_series else vwma
 
 
-def wma(data: Any, period: int = 50) -> Any:
-    use_series = False
+def wma(data: Any, period: int = 50, use_series=False) -> Any:
     if check_series(data):
         use_series = True
     data = convert_to_numpy(data)
@@ -34,8 +51,7 @@ def wma(data: Any, period: int = 50) -> Any:
     return pd.Series(wma) if use_series else wma
 
 
-def zlema(data: Any, period: int = 50) -> Any:
-    use_series = False
+def zlema(data: Any, period: int = 50, use_series=False) -> Any:
     if check_series(data):
         use_series = True
     data = convert_to_numpy(data)
@@ -43,14 +59,13 @@ def zlema(data: Any, period: int = 50) -> Any:
     return pd.Series(zlema) if use_series else zlema
 
 
-def sma(data: Any, period: int = 50) -> Any:
+def sma(data: Any, period: int = 50, use_series=False) -> Any:
     """
     Finding the moving average of a dataset
     Args:
         data: (list) A list containing the data you want to find the moving average of
         period: (int) How far each average set should be
     """
-    use_series = False
     if check_series(data):
         use_series = True
     data = convert_to_numpy(data)
@@ -58,8 +73,7 @@ def sma(data: Any, period: int = 50) -> Any:
     return pd.Series(sma) if use_series else sma
 
 
-def hma(data: Any, period: int = 50) -> Any:
-    use_series = False
+def hma(data: Any, period: int = 50, use_series=False) -> Any:
     if check_series(data):
         use_series = True
     data = convert_to_numpy(data)
@@ -67,8 +81,7 @@ def hma(data: Any, period: int = 50) -> Any:
     return pd.Series(hma) if use_series else hma
 
 
-def kaufman_adaptive_ma(data: Any, period: int = 50) -> Any:
-    use_series = False
+def kaufman_adaptive_ma(data: Any, period: int = 50, use_series=False) -> Any:
     if check_series(data):
         use_series = True
     data = convert_to_numpy(data)
@@ -76,8 +89,7 @@ def kaufman_adaptive_ma(data: Any, period: int = 50) -> Any:
     return pd.Series(kama) if use_series else kama
 
 
-def trima(data: Any, period: int = 50) -> Any:
-    use_series = False
+def trima(data: Any, period: int = 50, use_series=False) -> Any:
     if check_series(data):
         use_series = True
     data = convert_to_numpy(data)
@@ -85,10 +97,12 @@ def trima(data: Any, period: int = 50) -> Any:
     return pd.Series(trima) if use_series else trima
 
 
-def macd(data: Any, short_period: int = 12, long_period: int = 26, signal_period: int = 9) -> Any:
-    use_series = False
+def macd(data: Any, short_period: int = 12, long_period: int = 26, signal_period: int = 9, use_series=False) -> Any:
     if check_series(data):
         use_series = True
-    convert_to_numpy(data)
-    macd = ti.macd(data, short_period, long_period, signal_period)
-    return pd.Series(macd) if use_series else macd
+    data = convert_to_numpy(data)
+    macd, macd_signal, macd_histogram = ti.macd(data, short_period, long_period, signal_period)
+    if use_series:
+        df = pd.DataFrame({'macd': macd, 'macd_signal': macd_signal, 'macd_histogram': macd_histogram})
+        return df
+    return macd, macd_signal, macd_histogram
diff --git a/Blankly/indicators/oscillators.py b/blankly/indicators/oscillators.py
similarity index 69%
rename from Blankly/indicators/oscillators.py
rename to blankly/indicators/oscillators.py
index 82dcc206..7dc48f21 100644
--- a/Blankly/indicators/oscillators.py
+++ b/blankly/indicators/oscillators.py
@@ -1,14 +1,32 @@
-from Blankly.indicators.utils import convert_to_numpy
-from Blankly.indicators.utils import check_series
+"""
+    Oscillator wrappers
+    Copyright (C) 2021 Brandon Fan
+
+    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 <https://www.gnu.org/licenses/>.
+"""
+
 from typing import Any
+
 import numpy as np
 import pandas as pd
 import tulipy as ti
 
+from blankly.indicators.utils import check_series, convert_to_numpy
+
 
-def rsi(data: Any, period: int = 14, round_rsi: bool = True) -> np.array:
+def rsi(data: Any, period: int = 14, round_rsi: bool = False, use_series=False) -> np.array:
     """ Implements RSI Indicator """
-    use_series = False
     if check_series(data):
         use_series = True
     data = convert_to_numpy(data)
@@ -75,8 +93,9 @@ def stochastic_rsi(data, period=14, smooth_pct_k=3, smooth_pct_d=3):
 
     # Calculate StochRSI
     rsi_values = pd.Series(rsi_values)
-    stochrsi = (rsi - rsi.rolling(period).min()) / (rsi.rolling(period).max() - rsi.rolling(period).min())
+    stochrsi = (rsi_values - rsi_values.rolling(period).min()) / (
+                rsi_values.rolling(period).max() - rsi_values.rolling(period).min())
     stochrsi_K = stochrsi.rolling(smooth_pct_k).mean()
     stochrsi_D = stochrsi_K.rolling(smooth_pct_d).mean()
 
-    return round(rsi_values, 2).tolist(), round(stochrsi_K * 100, 2).tolist(), round(stochrsi_D * 100, 2).tolist()
+    return round(rsi_values, 2), round(stochrsi_K * 100, 2), round(stochrsi_D * 100, 2)
diff --git a/Blankly/indicators/statistics.py b/blankly/indicators/statistics.py
similarity index 50%
rename from Blankly/indicators/statistics.py
rename to blankly/indicators/statistics.py
index b1a67b6d..1c0cb86a 100644
--- a/Blankly/indicators/statistics.py
+++ b/blankly/indicators/statistics.py
@@ -1,14 +1,33 @@
+"""
+    Statistics functions
+    Copyright (C) 2021 Brandon Fan
+
+    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 <https://www.gnu.org/licenses/>.
+"""
+
 from typing import Any
-from Blankly.indicators.utils import convert_to_numpy
-from Blankly.indicators.utils import check_series
-import tulipy as ti
+
 import pandas as pd
+import tulipy as ti
+
+from blankly.indicators.utils import check_series, convert_to_numpy
 
 
-def stdev_period(data, period=14, use_series=False) -> Any:
+def stddev_period(data, period=14, use_series=False) -> Any:
     if check_series(data):
         use_series = True
-    data = convert_to_numpy(data)    
+    data = convert_to_numpy(data)
     stddev = ti.stddev(data, period)
     return pd.Series(stddev) if use_series else stddev
 
@@ -43,3 +62,11 @@ def max_period(data, period, use_series=False) -> Any:
     data = convert_to_numpy(data)
     maximum = ti.max(data, period)
     return pd.Series(maximum) if use_series else maximum
+
+
+def sum_period(data, period, use_series=False) -> Any:
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    maximum = ti.sum(data, period)
+    return pd.Series(maximum) if use_series else maximum
diff --git a/blankly/indicators/utils.py b/blankly/indicators/utils.py
new file mode 100644
index 00000000..d7a08df4
--- /dev/null
+++ b/blankly/indicators/utils.py
@@ -0,0 +1,38 @@
+"""
+    Indicator utils and repeatedly used functions
+    Copyright (C) 2021 Brandon Fan
+
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+from typing import Any
+
+import numpy as np
+import pandas as pd
+
+
+def to_historical_returns(data: Any):
+    return pd.Series(data).diff().tolist()
+
+
+def convert_to_numpy(data: Any):
+    if type(data) == list:
+        return np.asarray(data)
+    elif type(data) == pd.Series:
+        return data.to_numpy()
+    return data
+
+
+def check_series(data: Any):
+    return isinstance(data, pd.Series)
diff --git a/blankly/metrics/__init__.py b/blankly/metrics/__init__.py
new file mode 100644
index 00000000..5fd9d2dc
--- /dev/null
+++ b/blankly/metrics/__init__.py
@@ -0,0 +1,1 @@
+from blankly.metrics.portfolio import *
diff --git a/Blankly/metrics/portfolio.py b/blankly/metrics/portfolio.py
similarity index 58%
rename from Blankly/metrics/portfolio.py
rename to blankly/metrics/portfolio.py
index 1fbc28fe..673ffe80 100644
--- a/Blankly/metrics/portfolio.py
+++ b/blankly/metrics/portfolio.py
@@ -1,13 +1,36 @@
-import pandas as pd
+"""
+    General indicators & metrics for blankly
+    Copyright (C) 2021  Brandon Fan
+
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+import warnings
+
 import numpy as np
+import pandas as pd
 
 
 def cagr(start_value, end_value, years):
-    return (end_value / start_value) ** (1 / years)
+    if end_value < start_value:
+        warnings.warn('End Value less than Start Value makes CAGR meaningless, returning 0')
+        return 0.0
+    return (end_value / start_value) ** (1 / years) - 1
 
 
 def cum_returns(start_value, end_value):
-    return (start_value - end_value) / start_value
+    return (end_value - start_value) / start_value
 
 
 def sortino(returns, n=252, risk_free_rate=None):
@@ -16,7 +39,7 @@ def sortino(returns, n=252, risk_free_rate=None):
         mean = returns.mean() - risk_free_rate
     else:
         mean = returns.mean()
-    std_neg = returns[returns < 0].std() 
+    std_neg = returns[returns < 0].std()
     return mean / std_neg * np.sqrt(n)
 
 
@@ -26,7 +49,7 @@ def sharpe(returns, n=252, risk_free_rate=None):
         mean = returns.mean() - risk_free_rate
     else:
         mean = returns.mean()
-    std = returns.std() 
+    std = returns.std()
     return mean / std * np.sqrt(n)
 
 
@@ -65,8 +88,7 @@ def cvar(initial_value, returns, alpha):
 
 def max_drawdown(returns):
     return_series = pd.Series(returns)
-    comp_ret = (return_series+1).cumprod()
+    comp_ret = (return_series + 1).cumprod()
     peak = comp_ret.expanding(min_periods=1).max()
-    dd = (comp_ret/peak)-1
+    dd = (comp_ret / peak) - 1
     return dd.min()
-
diff --git a/blankly/strategy/__init__.py b/blankly/strategy/__init__.py
new file mode 100644
index 00000000..55328f5a
--- /dev/null
+++ b/blankly/strategy/__init__.py
@@ -0,0 +1,2 @@
+from blankly.strategy.strategy_base import *
+from blankly.strategy.strategy_state import *
diff --git a/Blankly/blankly_bot.py b/blankly/strategy/blankly_bot.py
similarity index 83%
rename from Blankly/blankly_bot.py
rename to blankly/strategy/blankly_bot.py
index 55199586..a703e28b 100644
--- a/Blankly/blankly_bot.py
+++ b/blankly/strategy/blankly_bot.py
@@ -1,5 +1,5 @@
 """
-    Inherited object to allow library interaction with a Blankly bot
+    Inherited object to allow library interaction with a blankly bot
     Copyright (C) 2021  Emerson Dove
 
     This program is free software: you can redistribute it and/or modify
@@ -16,28 +16,29 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-from multiprocessing import Process, Manager
-import Blankly
 import copy
 import warnings
+from multiprocessing import Manager, Process
 
-from Blankly.exchanges.orderbook_manager import OrderbookManger
-from Blankly.exchanges.ticker_manager import TickerManager
-from Blankly.interface.abc_currency_interface import ICurrencyInterface
-from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_API import API as Coinbase_Pro_API
 from binance.client import Client as Binance_API
 
+import blankly
+from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_api import API as CoinbaseProAPI
+from blankly.exchanges.managers.orderbook_manager import OrderbookManager
+from blankly.exchanges.managers.ticker_manager import TickerManager
+
 
 class BlanklyBot:
-    currency_pair: str
+    symbol: str
     user_preferences: dict
     exchange_type: str
     initial_state: dict
     # Define the given types for the user
-    Orderbook_Manager: OrderbookManger
+    Orderbook_Manager: OrderbookManager
     Ticker_Manager: TickerManager
-    Interface: ICurrencyInterface
-    coinbase_pro_direct: Coinbase_Pro_API
+    Interface: ABCExchangeInterface
+    coinbase_pro_direct: CoinbaseProAPI
     binance_direct: Binance_API
 
     def __init__(self):
@@ -50,7 +51,7 @@ def __init__(self):
         self.exchange_type = ""
         self.initial_state = {}
         self.user_preferences = {}
-        self.currency_pair = ""
+        self.symbol = ""
         self.direct_calls = None
         self.process = Process(target=self.setup_process)
 
@@ -71,7 +72,7 @@ def setup(self, exchange_type, currency_pair, user_preferences, initial_state, i
         self.user_preferences = user_preferences
         self.Interface = copy.deepcopy(interface)
         # Coin id is the currency and which market its on
-        self.currency_pair = currency_pair
+        self.symbol = currency_pair
         self.direct_calls = interface.get_calls()
         self.coinbase_pro_direct = self.direct_calls
         self.binance_direct = self.direct_calls
@@ -96,13 +97,14 @@ def setup_process(self, args):
         """
         Create any objects that need to be process-specific in the other process
         """
-        self.Ticker_Manager = Blankly.TickerManager(self.exchange_type, self.currency_pair)
-        self.Orderbook_Manager = Blankly.OrderbookManager(self.exchange_type, self.currency_pair)
+        self.Ticker_Manager = blankly.TickerManager(self.exchange_type, self.symbol)
+        self.Orderbook_Manager = blankly.OrderbookManager(self.exchange_type, self.symbol)
         self.main(args)
 
     """
     State getters and setters for external understanding of the operation
     """
+
     def get_state(self):
         try:
             return self.__state.copy()
diff --git a/Blankly/strategy/order.py b/blankly/strategy/order.py
similarity index 84%
rename from Blankly/strategy/order.py
rename to blankly/strategy/order.py
index 7379dc77..3275d081 100644
--- a/Blankly/strategy/order.py
+++ b/blankly/strategy/order.py
@@ -19,12 +19,14 @@
 
 class Order:
     """Order Type"""
+
     def __init__(self, order_type, side, amount, price=None):
         if order_type not in ['market', 'limit']:
-            raise ValueError("Order types can only be of type ['market', 'limit'], but was given: {}".format(order_type))
+            raise ValueError(
+                "Order types can only be of type ['market', 'limit'], but was given: {}".format(order_type))
         self.type = order_type
         if side not in ['buy', 'sell']:
-                raise ValueError("Order side can only be of type ['buy', 'sell'], but was given: {}".format(order_type))
+            raise ValueError("Order side can only be of type ['buy', 'sell'], but was given: {}".format(order_type))
         self.side = side
         if self.amount <= 0:
             raise ValueError("Size must be greater than zero, but was given: {}".format(size))
@@ -36,4 +38,3 @@ def __init__(self, order_type, side, amount, price=None):
             raise ValueError("Price must be greater than zero but was given: {}".format(price))
 
         self.price = price
-
diff --git a/Blankly/strategy/cookbook/recipes.py b/blankly/strategy/recipes/recipes.py
similarity index 79%
rename from Blankly/strategy/cookbook/recipes.py
rename to blankly/strategy/recipes/recipes.py
index a0e3f819..d60e174a 100644
--- a/Blankly/strategy/cookbook/recipes.py
+++ b/blankly/strategy/recipes/recipes.py
@@ -1,14 +1,14 @@
-import Blankly
+import blankly
 
 
-def golden_cross(ticker, interface: Blankly.Interface, state: StrategyState):
+def golden_cross(ticker, interface: blankly.Interface, state: StrategyState):
     resolution: str = state.resolution  # get the resolution that this price event is stored at
     variables = state.__variables  # each price event has it's own local variable state
 
     account = interface.account  # get account holdings
 
-    historical_prices = Blankly.historical(ticker, 50, resolution=resolution)
-    sma50 = Blankly.analysis.calculate_sma(historical_prices, window=50)[
+    historical_prices = blankly.historical(ticker, 50, resolution=resolution)
+    sma50 = blankly.analysis.calculate_sma(historical_prices, window=50)[
         -1]  # last 50 day sma value is the value we want
 
     if price > sma50 and variables['order_submitted']:
@@ -20,14 +20,14 @@ def golden_cross(ticker, interface: Blankly.Interface, state: StrategyState):
         interface.market_order(ticker, 'sell', stocks_available)
 
 
-def rsi(price, ticker, interface: Blankly.Interface, state: StrategyState):
+def rsi(price, ticker, interface: blankly.Interface, state: StrategyState):
     resolution: str = state.resolution  # get the resolution that this price event is stored at
     variables = state.__variables  # each price event has it's own local variable state
 
     account = interface.account  # get account holdings
 
-    historical_prices = Blankly.historical(ticker, 50, resolution=resolution)
-    rsi = Blankly.analysis.calculate_rsi(historical_prices)[-1]  # last 50 day sma value is the value we want
+    historical_prices = blankly.historical(ticker, 50, resolution=resolution)
+    rsi = blankly.analysis.calculate_rsi(historical_prices)[-1]  # last 50 day sma value is the value we want
 
     if rsi > 70 and variables['order_submitted']:
         variables['order_submitted'] = True
@@ -35,4 +35,4 @@ def rsi(price, ticker, interface: Blankly.Interface, state: StrategyState):
     elif rsi < 30:  # only sell if there's an open position
         variables['order_submitted'] = False
         stocks_available = account.holdings[ticker].available * 0.5  # sell 50% of available sures
-        interface.market_order(ticker, 'sell', stocks_available)
\ No newline at end of file
+        interface.market_order(ticker, 'sell', stocks_available)
diff --git a/blankly/strategy/strategy_base.py b/blankly/strategy/strategy_base.py
new file mode 100644
index 00000000..d7866ed1
--- /dev/null
+++ b/blankly/strategy/strategy_base.py
@@ -0,0 +1,353 @@
+"""
+    Abstraction for creating event driven user strategies
+    Copyright (C) 2021  Emerson Dove, Brandon Fan
+
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+import datetime
+import time
+import typing
+import warnings
+
+import pandas as pd
+
+import blankly
+from blankly.exchanges.exchange import Exchange
+from blankly.exchanges.interfaces.paper_trade.backtest_controller import BackTestController
+from blankly.exchanges.interfaces.paper_trade.backtest_result import BacktestResult
+from blankly.strategy.strategy_state import StrategyState
+from blankly.utils.time_builder import time_interval_to_seconds
+from blankly.utils.utils import AttributeDict
+
+
+class Strategy:
+    def __init__(self, exchange: Exchange, currency_pair='BTC-USD'):
+        self.__exchange = exchange
+        self.Ticker_Manager = blankly.TickerManager(self.__exchange.get_type(), currency_pair)
+        self.Orderbook_Manager = blankly.OrderbookManager(self.__exchange.get_type(), currency_pair)
+
+        self.__scheduling_pair = []  # Object to hold a currency and the resolution its pulled at: ["BTC-USD", 60]
+        self.Interface = exchange.get_interface()
+
+        # Create a cache for the current interface, and a wrapped paper trade object for user backtesting
+        self.__interface_cache = self.Interface
+        self.__paper_trade_exchange = blankly.PaperTrade(self.__exchange)
+        self.__schedulers = []
+        self.__variables = {}
+        self.__hashes = []
+        self.__assigned_websockets = []
+
+    @property
+    def variables(self):
+        return self.__variables
+
+    def modify_variable(self, callable, key, value):
+        hashed = hash(callable)
+        self.__variables[hashed][key] = value
+
+    def add_price_event(self, callback: typing.Callable, symbol: str, resolution: typing.Union[str, float],
+                        init: typing.Callable = None, synced: bool = False, **kwargs):
+        """
+        Add Price Event
+        Args:
+            callback: The price event callback that will be added to the current ticker and run at the proper resolution
+            symbol: Currency pair to create the price event for
+            resolution: The resolution that the callback will be run - in seconds
+            init: Callback function to allow a setup for the strategy variable. This
+                can be used for accumulating price data
+            synced: Sync the function to
+        """
+        self.__custom_price_event(callback, symbol, resolution, init, synced, kwargs=kwargs)
+
+    def add_bar_event(self, callback: typing.Callable, symbol: str, resolution: typing.Union[str, float],
+                      init: typing.Callable = None, **kwargs):
+        """
+        Add Price Event
+        Args:
+            callback: The price event callback that will be added to the current ticker and run at the proper resolution
+            symbol: Currency pair to create the price event for
+            resolution: The resolution that the callback will be run - in seconds
+            init: Callback function to allow a setup for the strategy variable. This
+                can be used for accumulating price data
+        """
+        self.__custom_price_event(callback, symbol, resolution, init, synced=True, bar=True, kwargs=kwargs)
+
+    def __custom_price_event(self, callback: typing.Callable, symbol: str, resolution: typing.Union[str, float],
+                             init: typing.Callable = None, synced: bool = False, bar: bool = False, **kwargs):
+        """
+        Add Price Event
+        Args:
+            callback: The price event callback that will be added to the current ticker and run at the proper resolution
+            symbol: Currency pair to create the price event for
+            resolution: The resolution that the callback will be run - in seconds
+            init: Callback function to allow a setup for the strategy variable. This
+                can be used for accumulating price data
+            synced: Sync the function to
+            bar: Get the OHLCV data for a valid exchange interval
+        """
+        resolution = time_interval_to_seconds(resolution)
+
+        if bar:
+            self.__scheduling_pair.append([symbol, resolution, 'bar'])
+        else:
+            self.__scheduling_pair.append([symbol, resolution, 'price_event'])
+        callback_hash = hash((callback, hash((symbol, resolution))))
+        if callback_hash in self.__hashes:
+            raise ValueError("A callback of the same type and resolution has already been made for "
+                             "the ticker: {}".format(symbol))
+        else:
+            self.__hashes.append(callback_hash)
+        self.__variables[callback_hash] = AttributeDict({})
+        state = StrategyState(self, self.__variables[callback_hash], symbol, resolution=resolution)
+
+        # run init
+        if init:
+            init(symbol, state)
+
+        if resolution < 10:
+            # since it's less than 10 sec, we will just use the websocket feed - exchanges don't like fast calls
+            self.Ticker_Manager.create_ticker(self.__idle_event, override_symbol=symbol)
+            self.__schedulers.append(
+                blankly.Scheduler(self.__price_event_websocket, resolution,
+                                  initially_stopped=True,
+                                  callback=callback,
+                                  resolution=resolution,
+                                  variables=self.__variables[callback_hash],
+                                  state_object=state,
+                                  synced=synced,
+                                  ohlc=bar,
+                                  symbol=symbol, **kwargs)
+            )
+        else:
+            # Use the API
+            self.__schedulers.append(
+                blankly.Scheduler(self.__price_event_rest, resolution,
+                                  initially_stopped=True,
+                                  callback=callback,
+                                  resolution=resolution,
+                                  variables=self.__variables[callback_hash],
+                                  state_object=state,
+                                  synced=synced,
+                                  ohlc=bar,
+                                  symbol=symbol, **kwargs)
+            )
+
+    def __idle_event(self):
+        """
+        Function to skip & ignore callbacks
+        """
+        pass
+
+    def __price_event_rest(self, **kwargs):
+        callback = kwargs['callback']
+        symbol = kwargs['symbol']
+        resolution = kwargs['resolution']
+        variables = kwargs['variables']
+        ohlc = kwargs['ohlc']
+        state = kwargs['state_object']  # type: StrategyState
+
+        state.variables = variables
+        state.resolution = resolution
+
+        if ohlc:
+            ohlcv_time = kwargs['ohlcv_time']
+            while True:
+                data = self.Interface.history(symbol, 1, resolution).iloc[-1].to_dict()
+                if data['time'] + resolution == ohlcv_time:
+                    break
+                time.sleep(.5)
+            data['price'] = self.Interface.get_price(symbol)
+        else:
+            data = self.Interface.get_price(symbol)
+
+        callback(data, symbol, state)
+
+    def __price_event_websocket(self, **kwargs):
+        callback = kwargs['callback']
+        symbol = kwargs['symbol']
+        resolution = kwargs['resolution']
+        variables = kwargs['variables']
+        state = kwargs['state_object']  # type: StrategyState
+
+        price = self.Ticker_Manager.get_most_recent_tick(override_symbol=symbol)
+
+        state.variables = variables
+        state.resolution = resolution
+
+        callback(price, symbol, state)
+
+    def __orderbook_event(self, tick, symbol, user_callback, state_object):
+        user_callback(tick, symbol, state_object)
+
+    def add_orderbook_event(self, callback: typing.Callable, symbol: str, init: typing.Callable = None,
+                            **kwargs):
+        """
+        Add Orderbook Event
+        Args:
+            callback: The price event callback that will be added to the current ticker and run at the proper resolution
+            symbol: Currency pair to create the orderbook for
+            init: Callback function to allow a setup for the strategy variable. This
+                can be used for accumulating price data
+        """
+        self.__scheduling_pair.append([symbol, 'live'])
+        callback_hash = hash((callback, symbol))
+        if callback_hash in self.__hashes:
+            raise ValueError("A callback of the same type and resolution has already been made for the ticker: "
+                             "{}".format(symbol))
+        else:
+            self.__hashes.append(callback_hash)
+        self.__variables[callback_hash] = AttributeDict({})
+        state = StrategyState(self, self.__variables[callback], symbol=symbol)
+        if init:
+            init(symbol, state)
+
+        variables = self.__variables[callback_hash]
+
+        # since it's less than 10 sec, we will just use the websocket feed - exchanges don't like fast calls
+        self.Orderbook_Manager.create_orderbook(self.__orderbook_event, initially_stopped=True,
+                                                override_symbol=symbol,
+                                                symbol=symbol,
+                                                user_callback=callback,
+                                                variables=variables,
+                                                state_object=state,
+                                                **kwargs
+                                                )
+
+        exchange_type = self.__exchange.get_type()
+        self.__assigned_websockets.append([symbol, exchange_type])
+
+    def start(self):
+        for i in self.__schedulers:
+            i.start()
+
+        for i in self.__assigned_websockets:
+            self.Orderbook_Manager.restart_ticker(i[0], i[1])
+
+    def backtest(self,
+                 initial_values: dict = None,
+                 to: str = None,
+                 start_date: str = None,
+                 end_date: str = None,
+                 save: bool = False,
+                 settings_path: str = None,
+                 callbacks: list = None,
+                 **kwargs
+                 ) -> BacktestResult:
+        """
+        Turn this strategy into a backtest.
+
+        Args:
+            ** We expect either an initial_value (in USD) or a dictionary of initial values, we also expect
+            either `to` to be set or `start_date` and `end_date` **
+
+            initial_values (dict): Dictionary of initial value sizes (i.e { 'BTC': 3, 'USD': 5650}).
+                Using this will override the values that are currently on your exchange.
+            to (str): Declare an amount of time before now to backtest from: ex: '5y' or '10h'
+            start_date (str): Override argument "to" by specifying a start date such as "03/06/2018"
+            end_date (str): End the backtest at a date such as "03/06/2018"
+            save (bool): Save the price data references to the data required for the backtest as well as
+                overriden settings.
+            settings_path (str): Path to the backtest.json file.
+            callbacks (list of callables): Custom functions that will be run at the end of the backtest
+
+            Keyword Arguments:
+                **Use these to override parameters in the backtest.json file**
+
+                use_price: str = 'close',
+                    Set which price column to use.
+
+                smooth_prices: bool = False,
+                    Create linear connections between downloaded prices
+
+                GUI_output: bool = True,
+                    Enable/disable GUI webpage display after backtest
+
+                show_tickers_with_zero_delta: bool = False,
+                    Exclude tickers that have no change to account value in the GUI
+
+                save_initial_account_value: bool = True,
+                    Put an extra frame which contains the initial account values before any trade
+
+                show_progress_during_backtest: bool = True,
+                    Show a progress bar as the backtest runs
+
+                cache_location: str = './price_caches'
+                    Set a location for the price cache csv's to be written to
+
+                resample_account_value_for_metrics: str or bool = '1d' or False
+                    Because backtest data can be input at a variety of resolutions, account value often needs to be
+                        recalculated at consistent intervals for use in metrics & indicators.
+                        This setting allows the specification of that consistent interval.
+                        The value can be set to `False` to skip any recalculation.
+
+                quote_account_value_in: str = 'USD'
+                    Manually set what valuation should be used when calculating account value.
+                        Multiple types of quote currency (ex: USD and EUR) are not supported because
+                        there is no datasource for quoting pairs such as EUR-USD until forex integration.
+        """
+
+        start = None
+        end = None
+
+        if to is not None:
+            start = time.time() - time_interval_to_seconds(to)
+            end = time.time()
+
+        if start_date is not None:
+            start_date = pd.to_datetime(start_date)
+            epoch = datetime.datetime.utcfromtimestamp(0)
+            start = (start_date - epoch).total_seconds()
+
+        if end_date is not None:
+            end_date = pd.to_datetime(end_date)
+            epoch = datetime.datetime.utcfromtimestamp(0)
+            end = (end_date - epoch).total_seconds()
+
+        self.Interface = self.__paper_trade_exchange.get_interface()
+        backtesting_controller = BackTestController(self.__paper_trade_exchange,
+                                                    backtest_settings_path=settings_path,
+                                                    callbacks=callbacks
+                                                    )
+
+        # Write any kwargs as settings to the settings - save if enabled.
+        for k, v in kwargs.items():
+            backtesting_controller.write_setting(k, v, save)
+
+        if initial_values is not None:
+            backtesting_controller.write_initial_price_values(initial_values)
+
+        # Write each scheduling pair as a price event - save if enabled.
+        if start is not None and end is not None:
+            for i in self.__scheduling_pair:
+                backtesting_controller.add_prices(i[0], start, end, i[1], save=save)
+        else:
+            warnings.warn("User-specified start and end time not given. Defaulting to using only cached data.")
+
+        # Append each of the events the class defines into the backtest
+        for i in self.__schedulers:
+            kwargs = i.get_kwargs()
+            backtesting_controller.append_backtest_price_event(callback=kwargs['callback'],
+                                                               asset_id=kwargs['symbol'],
+                                                               time_interval=i.get_interval(),
+                                                               state_object=kwargs['state_object'],
+                                                               ohlc=kwargs['ohlc']
+                                                               )
+
+        # Run the backtest & return results
+        results = backtesting_controller.run()
+
+        # Clean up
+        self.Interface = self.__interface_cache
+        return results
diff --git a/Blankly/strategy/strategy_state.py b/blankly/strategy/strategy_state.py
similarity index 62%
rename from Blankly/strategy/strategy_state.py
rename to blankly/strategy/strategy_state.py
index 39f71210..dbfd2c47 100644
--- a/Blankly/strategy/strategy_state.py
+++ b/blankly/strategy/strategy_state.py
@@ -16,15 +16,25 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
+from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface as Interface
+from blankly.utils.utils import AttributeDict, get_base_asset, get_quote_asset
 
-from Blankly.utils.utils import AttributeDict
-from Blankly import Interface
 
 class StrategyState:
+    interface: Interface
+    variables: AttributeDict
+    resolution: float
+
     """Strategy State"""
-    def __init__(self, strategy, interface: Interface, variables: AttributeDict, resolution: str = None):
+
+    def __init__(self, strategy, variables: AttributeDict, symbol, resolution: float = None):
         self.strategy = strategy
-        self.interface = interface
         self.variables = variables
         self.resolution = resolution
+        self.symbol = symbol
+        self.base_asset = get_base_asset(symbol)
+        self.quote_asset = get_quote_asset(symbol)
 
+    @property
+    def interface(self) -> Interface:
+        return self.strategy.Interface
diff --git a/blankly/strategy/strategy_usage_example.py b/blankly/strategy/strategy_usage_example.py
new file mode 100644
index 00000000..8229761b
--- /dev/null
+++ b/blankly/strategy/strategy_usage_example.py
@@ -0,0 +1,54 @@
+import blankly
+from blankly.strategy import Strategy, StrategyState
+
+
+def golden_cross(price, ticker, state: StrategyState):
+    resolution: str = state.resolution  # get the resolution that this price event is stored at
+    variables = state.variables  # each price event has it's own local variable state
+    interface: blankly.Interface = state.interface
+
+    account = interface.account  # get account holdings
+
+    historical_prices = blankly.historical(ticker, 50, resolution=resolution)
+    sma50 = blankly.analysis.calculate_sma(historical_prices, window=50)[
+        -1]  # last 50 day sma value is the value we want
+
+    if price > sma50 and variables['order_submitted']:
+        variables['order_submitted'] = True
+        interface.market_order(ticker, 'buy', account.cash * 0.25)
+    elif price < sma50:  # only sell if there's an open position
+        variables['order_submitted'] = False
+        stocks_available = account.holdings[ticker].available * 0.5  # sell 50% of available sures
+        interface.market_order(ticker, 'sell', stocks_available)
+
+
+def rsi(price, ticker, interface: blankly.Interface, state: StrategyState):
+    resolution: str = state.resolution  # get the resolution that this price event is stored at
+    variables = state.variables  # each price event has it's own local variable state
+
+    account = interface.account  # get account holdings
+
+    historical_prices = blankly.historical(ticker, 50, resolution=resolution)
+    rsi = blankly.analysis.calculate_rsi(historical_prices)[-1]  # last 50 day sma value is the value we want
+
+    if rsi > 70 and variables['order_submitted']:
+        variables['order_submitted'] = True
+        interface.market_order(ticker, 'buy', account.cash * 0.25)
+    elif rsi < 30:  # only sell if there's an open position
+        variables['order_submitted'] = False
+        stocks_available = account.holdings[ticker].available * 0.5  # sell 50% of available sures
+        interface.market_order(ticker, 'sell', stocks_available)
+
+
+if __name__ == "__main__":
+    coinbase_pro = blankly.CoinbasePro()
+    alpaca = blankly.Alpaca()
+
+    coinbase_strategy = Strategy(coinbase_pro)
+    alpaca_strategy = Strategy(alpaca)
+
+    coinbase_strategy.add_price_event(golden_cross, symbol='BTC-USD', resolution='15m')
+    coinbase_strategy.add_price_event(rsi, symbol='XLM-USD', resolution='15m')
+
+    alpaca_strategy.add_price_event(golden_cross, symbol='MSFT', resolution='15m')
+    alpaca_strategy.add_price_event(rsi, symbol='AAPL', resolution='15m')
diff --git a/Blankly/utils/paper_trading/__init__.py b/blankly/utils/__init__.py
similarity index 100%
rename from Blankly/utils/paper_trading/__init__.py
rename to blankly/utils/__init__.py
diff --git a/Blankly/calc.py b/blankly/utils/calc.py
similarity index 99%
rename from Blankly/calc.py
rename to blankly/utils/calc.py
index ac824cfb..f7f52217 100644
--- a/Blankly/calc.py
+++ b/blankly/utils/calc.py
@@ -101,4 +101,4 @@ def calc(s):
             a = numStk.pop()
             numStk.append(getBin(op, a, b))
 
-    return numStk.pop()
\ No newline at end of file
+    return numStk.pop()
diff --git a/Blankly/utils/exceptions.py b/blankly/utils/exceptions.py
similarity index 90%
rename from Blankly/utils/exceptions.py
rename to blankly/utils/exceptions.py
index b41a075d..a1cfabbd 100644
--- a/Blankly/utils/exceptions.py
+++ b/blankly/utils/exceptions.py
@@ -1,5 +1,5 @@
 """
-    Exceptions definitions for Blankly
+    Exceptions definitions for blankly
     Copyright (c) 2021 Emerson Dove
 
     Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -20,3 +20,7 @@ class InvalidOrder(Exception):
 
 class APIException(Exception):
     pass
+
+
+class AuthError(Exception):
+    pass
diff --git a/Blankly/utils/scheduler.py b/blankly/utils/scheduler.py
similarity index 86%
rename from Blankly/utils/scheduler.py
rename to blankly/utils/scheduler.py
index 7f656918..a413ac96 100644
--- a/Blankly/utils/scheduler.py
+++ b/blankly/utils/scheduler.py
@@ -15,22 +15,24 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-import warnings
-
-from Blankly.utils.time_builder import time_interval_to_seconds
 import threading
 import time
 import traceback
+import warnings
+from datetime import datetime as dt
+
+from blankly.utils.time_builder import time_interval_to_seconds
+from blankly.utils.utils import ceil_date
 
 
 class Scheduler:
-    def __init__(self, function, interval, initially_stopped=False, **kwargs):
+    def __init__(self, function, interval, initially_stopped=False, synced=True, **kwargs):
         """
         Wrapper for functions that run at a set interval
         Args:
             function: Function reference to create the scheduler on ex: self.price_event
             interval: int of delay between calls in seconds, or a string that takes units s, m, h, d w, M, y (second,
-            minute, hour, day, week, month, year) after a magnitude. Examples: "4s", "6h", "10d".
+            minute, hour, day, week, month, year) after a magnitude. examples: "4s", "6h", "10d".
         """
         if isinstance(interval, str):
             interval = time_interval_to_seconds(interval)
@@ -38,6 +40,7 @@ def __init__(self, function, interval, initially_stopped=False, **kwargs):
         # Stop flag
         self.__stop = False
         self.__thread = None
+        self.synced = synced
 
         self.__interval = interval
         self.__kwargs = kwargs
@@ -100,17 +103,21 @@ def __threading_wait(self, func, interval, kwargs):
         This function is used with the scheduler decorator
         """
         base_time = time.time()
+        if self.synced:
+            base_time = ceil_date(dt.now(), seconds=interval).timestamp()
+            offset = base_time - time.time()
+            time.sleep(offset)
+            kwargs['ohlcv_time'] = base_time
         while True:
             if self.__stop:
                 break
             try:
-                if kwargs == {}:
-                    func()
-                else:
-                    func(**kwargs)
+                func(**kwargs)
             except Exception:
                 traceback.print_exc()
-            base_time = base_time + interval
+            base_time += interval
+            if self.synced:
+                kwargs['ohlcv_time'] += interval
 
             # The downside of this is that it keeps the thread running while waiting to stop
             # It's dependent on delay if its more efficient to just check more
diff --git a/blankly/utils/time.py b/blankly/utils/time.py
new file mode 100644
index 00000000..ef2afb6a
--- /dev/null
+++ b/blankly/utils/time.py
@@ -0,0 +1,25 @@
+"""
+    Timezone & time utils
+    Copyright (C) 2021 Brandon Fan
+
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+from datetime import datetime as dt
+
+
+def is_datetime_naive(datetime_obj: dt):
+    if datetime_obj.tzinfo is None or datetime_obj.tzinfo.utcoffset(datetime_obj) is None:
+        return True
+    return False
diff --git a/Blankly/utils/time_builder.py b/blankly/utils/time_builder.py
similarity index 93%
rename from Blankly/utils/time_builder.py
rename to blankly/utils/time_builder.py
index 11019bdf..d80b23d5 100644
--- a/Blankly/utils/time_builder.py
+++ b/blankly/utils/time_builder.py
@@ -16,6 +16,8 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
+from typing import Union
+
 
 def build_second() -> int:
     return 1
@@ -57,13 +59,15 @@ def build_millennium() -> int:
     return build_century() * 10
 
 
-def time_interval_to_seconds(interval) -> float:
+def time_interval_to_seconds(interval: Union[str, float]) -> float:
     """
     Extract the number of seconds in an interval string
     """
     if isinstance(interval, float) or isinstance(interval, int):
         return float(interval)
     # Extract intervals
+    if 'mo' in interval:
+        interval = interval.replace('mo', 'M')
     try:
         magnitude = int(interval[:-1])
     except ValueError:
diff --git a/Blankly/utils/utils.py b/blankly/utils/utils.py
similarity index 65%
rename from Blankly/utils/utils.py
rename to blankly/utils/utils.py
index a9f39a0e..1b9ee3d0 100644
--- a/Blankly/utils/utils.py
+++ b/blankly/utils/utils.py
@@ -16,15 +16,22 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-import datetime as DT
-import dateutil.parser as DP
+import datetime
 import json
-import numpy
-import warnings
 import sys
-
+import warnings
+from datetime import datetime as dt, timezone
+from math import ceil, trunc as math_trunc
+from typing import Union
+
+import dateutil.parser as dp
+import numpy as np
+import pandas as pd
+import pandas_market_calendars as mcal
 from sklearn.linear_model import LinearRegression
 
+from blankly.utils.time_builder import time_interval_to_seconds
+
 
 # Recursively check if the user has all the preferences, inform when defaults are missing
 def __compare_dicts(default_settings, user_settings):
@@ -52,7 +59,7 @@ def __compare_dicts(default_settings, user_settings):
 settings_path_override = None
 
 backtest_cache = None
-backteset_path_override = None
+backtest_path_override = None
 
 # Copy of settings to compare defaults vs overrides
 default_global_settings = {
@@ -60,8 +67,19 @@ def __compare_dicts(default_settings, user_settings):
         "account_update_time": 5000,
         "use_sandbox": False,
         "use_sandbox_websockets": False,
-        "binance_tld": "us",
-        "websocket_buffer_size": 100000,
+        "websocket_buffer_size": 10000,
+
+        "coinbase_pro": {
+            "cash": "USD"
+        },
+        "binance": {
+            "cash": "USDT",
+            "binance_tld": "us"
+        },
+        "alpaca": {
+            "websocket_stream": "iex",
+            "cash": "USD"
+        }
     }
 }
 
@@ -72,7 +90,9 @@ def __compare_dicts(default_settings, user_settings):
     "show_tickers_with_zero_delta": False,
     "save_initial_account_value": False,
     "show_progress_during_backtest": False,
-    "cache_location": "./price_caches"
+    "cache_location": "./price_caches",
+    "resample_account_value_for_metrics": "1d",
+    "quote_account_value_in": "USD"
 }
 
 
@@ -85,13 +105,13 @@ def load_json_file(override_path=None):
 
 def load_backtest_preferences(override_path=None) -> dict:
     global backtest_cache
-    global backteset_path_override
+    global backtest_path_override
 
     # Allow this override to be evaluated anywhere
-    if override_path is None and backteset_path_override is not None:
-        override_path = backteset_path_override
+    if override_path is None and backtest_path_override is not None:
+        override_path = backtest_path_override
     elif override_path is not None:
-        backteset_path_override = override_path
+        backtest_path_override = override_path
 
     if backtest_cache is None:
         try:
@@ -134,7 +154,7 @@ def load_user_preferences(override_path=None) -> dict:
     if settings_cache is None:
         try:
             if override_path is None:
-                preferences = load_json_file('settings.json')
+                preferences = load_json_file('./settings.json')
             else:
                 preferences = load_json_file(override_path)
         except FileNotFoundError:
@@ -147,29 +167,40 @@ def load_user_preferences(override_path=None) -> dict:
         return settings_cache
 
 
-def pretty_print_JSON(json_object):
+def pretty_print_JSON(json_object, actually_print=True):
     """
     Json pretty printer for general string usage
     """
     out = json.dumps(json_object, indent=2)
-    print(out)
+    if actually_print:
+        print(out)
     return out
 
 
 def epoch_from_ISO8601(ISO8601) -> float:
-    return DP.parse(ISO8601).timestamp()
+    return dp.parse(ISO8601).timestamp()
+
+
+def convert_input_to_epoch(value: Union[str, dt]) -> float:
+    if isinstance(value, str):
+        return epoch_from_ISO8601(value)
+    elif isinstance(value, dt):
+        return value.timestamp()
+    elif isinstance(value, float):
+        return value
+    raise ValueError("Incorrect value input given, expected string or value but got: {}".format(type(value)))
 
 
 def ISO8601_from_epoch(epoch) -> str:
-    return DT.datetime.utcfromtimestamp(epoch).isoformat() + 'Z'
+    return dt.utcfromtimestamp(epoch).isoformat() + 'Z'
 
 
 def get_price_derivative(ticker, point_number):
     """
     Performs regression n points back
     """
-    feed = numpy.array(ticker.get_ticker_feed()).reshape(-1, 1)
-    times = numpy.array(ticker.get_time_feed()).reshape(-1, 1)
+    feed = np.array(ticker.get_ticker_feed()).reshape(-1, 1)
+    times = np.array(ticker.get_time_feed()).reshape(-1, 1)
     if point_number > len(feed):
         point_number = len(feed)
 
@@ -178,7 +209,7 @@ def get_price_derivative(ticker, point_number):
     prices = []
     for i in range(point_number):
         prices.append(feed[i][0]["price"])
-    prices = numpy.array(prices).reshape(-1, 1)
+    prices = np.array(prices).reshape(-1, 1)
 
     regressor = LinearRegression()
     regressor.fit(times, prices)
@@ -207,7 +238,7 @@ def fit_parabola(ticker, point_number):
     for i in range(len(prices)):
         times[i] = times[i] - latest_time
 
-    return numpy.polyfit(times, prices, 2, full=True)
+    return np.polyfit(times, prices, 2, full=True)
 
 
 def to_blankly_coin_id(asset_id, exchange, quote_guess=None) -> str:
@@ -237,18 +268,25 @@ def __check_ending(full_string, checked_ending) -> bool:
 def to_exchange_coin_id(blankly_coin_id, exchange):
     if exchange == "binance":
         return blankly_coin_id.replace('-', '')
+    if exchange == "alpaca":
+        return get_base_asset(blankly_coin_id)
     if exchange == "coinbase_pro":
         return blankly_coin_id
 
 
-def get_base_currency(blankly_coin_id):
+def get_base_asset(blankly_coin_id):
     # Gets the BTC of the BTC-USD
     return blankly_coin_id.split('-')[0]
 
 
-def get_quote_currency(blankly_coin_id):
+def get_quote_asset(blankly_coin_id):
     # Gets the USD of the BTC-USD
-    return blankly_coin_id.split('-')[1]
+    split = blankly_coin_id.split('-')
+    if len(split) > 1:
+        return split[1]
+    else:
+        # This could go wrong
+        return 'USD'
 
 
 def rename_to(keys_array, renaming_dictionary):
@@ -284,7 +322,7 @@ def isolate_specific(needed, compare_dictionary):
     # Make a copy of the dictionary so we don't modify it if you loop over
     compare_dictionary = {**compare_dictionary}
     # Create a row vector for the keys
-    column = [column[0] for column in needed]    # ex: ['currency', 'available', 'hold']
+    column = [column[0] for column in needed]  # ex: ['currency', 'available', 'hold']
     # Create an area to hold the specific data
     if 'exchange_specific' in compare_dictionary:
         exchange_specific = compare_dictionary['exchange_specific']
@@ -300,7 +338,10 @@ def isolate_specific(needed, compare_dictionary):
             # If it is, there is a state value for it
             if k == val:
                 # Push type to value
-                compare_dictionary[k] = needed[index][1](v)
+                if v is not None:
+                    compare_dictionary[k] = needed[index][1](v)
+                else:
+                    compare_dictionary[k] = v
                 required = True
                 break
         # Must not be found
@@ -332,12 +373,13 @@ def convert_epochs(epoch):
     return epoch
 
 
-def compare_dictionaries(dict1, dict2) -> bool:
+def compare_dictionaries(dict1, dict2, force_exchange_specific=True) -> bool:
     """
     Compare two output dictionaries to check if they have the same keys (excluding "exchange_specific")
     Args:
         dict1 (dict): First dictionary to compare
         dict2 (dict): Second dictionary to compare
+        force_exchange_specific (bool): Check if the the exchange_specific tag is in the dictionary
     Returns:
         bool: Are the non specific tags the same?
     """
@@ -345,15 +387,27 @@ def compare_dictionaries(dict1, dict2) -> bool:
     # Make copies of the dictionaries
     dict1 = {**dict1}
     dict2 = {**dict2}
-    if "exchange_specific" not in dict1:
-        raise KeyError("Exchange specific tag not in: " + str(dict1))
 
-    if "exchange_specific" not in dict2:
-        raise KeyError("Exchange specific tag not in: " + str(dict2))
+    if force_exchange_specific:
+        if "exchange_specific" not in dict1:
+            raise KeyError("Exchange specific tag not in: " + str(dict1))
 
-    # Safely remove keys now
-    del dict1["exchange_specific"]
-    del dict2["exchange_specific"]
+        if "exchange_specific" not in dict2:
+            raise KeyError("Exchange specific tag not in: " + str(dict2))
+
+        # Safely remove keys now
+        del dict1["exchange_specific"]
+        del dict2["exchange_specific"]
+    else:
+        # Remove the tag if its there anyway
+        try:
+            del dict1["exchange_specific"]
+        except KeyError:
+            pass
+        try:
+            del dict2["exchange_specific"]
+        except KeyError:
+            pass
 
     valid_keys = []
 
@@ -394,6 +448,7 @@ def compare_dictionaries(dict1, dict2) -> bool:
 
 
 def update_progress(progress):
+    # From this great post: https://stackoverflow.com/a/15860757/8087739
     # update_progress() : Displays or updates a console progress bar
     # Accepts a float between 0 and 1. Any int will be converted to a float.
     # A value under 0 represents a 'halt'.
@@ -418,9 +473,90 @@ def update_progress(progress):
     sys.stdout.flush()
 
 
+def split_df(df, n):
+    return df.groupby(np.arange(len(df)) // n)
+
+
+def get_ohlcv(candles, n):
+    if len(candles) < n:
+        raise ValueError("Not enough candles provided, required at least {} candles, "
+                         "but only received {}".format(n, len(candles)))
+    new_candles = pd.DataFrame()
+    df = split_df(candles, n)
+    new_candles['high'] = df['high'].max().reset_index(drop=True)
+    new_candles['low'] = df['low'].min().reset_index(drop=True)
+    new_candles['volume'] = df['volume'].sum().reset_index(drop=True)
+    new_candles['volume'] = new_candles['volume'].apply(lambda x: float(x))
+    new_candles['close'] = candles['close'].iloc[::n].reset_index(drop=True)
+    new_candles['open'] = candles['open'].iloc[::n].reset_index(drop=True)
+    new_candles['time'] = candles.index.to_series().iloc[::n].reset_index(drop=True)
+    new_candles['time'] = new_candles['time'].apply(lambda x: np.int64(x.timestamp()))
+    print(new_candles)
+    return new_candles
+
+
+# TODO: change this
+def get_ohlcv_2(candles, n):
+    if len(candles) < n:
+        raise ValueError("Not enough candles provided, required at least {} candles, "
+                         "but only received {}".format(n, len(candles)))
+    new_candles = pd.DataFrame()
+    df = split_df(candles, n)
+    new_candles['high'] = df['high'].max().reset_index(drop=True)
+    new_candles['low'] = df['low'].min().reset_index(drop=True)
+    new_candles['volume'] = df['volume'].sum().reset_index(drop=True)
+    new_candles['volume'] = new_candles['volume'].apply(lambda x: float(x))
+    new_candles['close'] = candles['close'].iloc[0::n].reset_index(drop=True)
+    new_candles['open'] = candles['open'].iloc[0::n].reset_index(drop=True)
+    new_candles['time'] = candles['time'].iloc[0::n].reset_index(drop=True).astype('int64')
+    new_candles['time'] = new_candles['time'].apply(lambda x: np.int64(x))
+    return new_candles
+
+
+def ceil_date(date, **kwargs):
+    secs = datetime.timedelta(**kwargs).total_seconds()
+    return dt.fromtimestamp(date.timestamp() + secs - date.timestamp() % secs)
+
+
+def get_estimated_start_from_limit(limit, end_epoch, resolution_str, resolution_multiplier):
+    OVERESTIMATE_CONSTANT = 1.5
+
+    nyse = mcal.get_calendar('NYSE')
+    required_length = ceil(limit * OVERESTIMATE_CONSTANT)
+    resolution = time_interval_to_seconds(resolution_str)
+
+    if resolution == 60 and limit < (1440 / resolution_multiplier):
+        return end_epoch - 4 * 86400  # worst case is three day weekend at 9:30am open
+
+    if resolution == 3600 and limit < (24 / resolution_multiplier):
+        return end_epoch - 4 * 86400  # worst case is three day weekend at 9:30am open
+
+    temp_start = end_epoch - limit * resolution * resolution_multiplier
+    end_date = dt.fromtimestamp(end_epoch, tz=timezone.utc)
+    start_date = dt.fromtimestamp(temp_start, tz=timezone.utc)
+
+    schedule = nyse.schedule(start_date=start_date, end_date=end_date)
+    date_range = mcal.date_range(schedule, frequency='1D')
+
+    count = 1
+    while len(date_range) < required_length:
+        temp_start -= 3600 * OVERESTIMATE_CONSTANT * count
+        start_date = dt.fromtimestamp(temp_start)
+        schedule = nyse.schedule(start_date=start_date, end_date=end_date)
+        date_range = mcal.date_range(schedule, frequency='1D')
+        count += 1
+
+    return temp_start
+
+
 class AttributeDict(dict):
     def __getattr__(self, attr):
         return self[attr]
 
     def __setattr__(self, attr, value):
         self[attr] = value
+
+
+def trunc(number: float, decimals: int) -> float:
+    stepper = 10.0 ** decimals
+    return math_trunc(stepper * number) / stepper
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 00000000..7b8af165
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,4 @@
+## Setting up is easy:
+1. *Rename or create a new file of the format* `keys_example.json` as `keys.json`. ***This is done to keep it out of version control (putting keys in git history can be bad).***
+2. Add your exchange keys to the new (non-version controlled) `keys.json` file
+3. Run `basic_example.py`. You can change exchanges by changing the line `Blankly.CoinbasePro()` to what you're using, such as `Blankly.Binance()`
\ No newline at end of file
diff --git a/Examples/basic_example.py b/examples/basic_example.py
similarity index 58%
rename from Examples/basic_example.py
rename to examples/basic_example.py
index aab3e7da..0c3b0136 100644
--- a/Examples/basic_example.py
+++ b/examples/basic_example.py
@@ -1,7 +1,7 @@
-import Blankly
+import blankly
 
 
-def price_event(price, currency_pair):
+def price_event(price, currency_pair, state):
     """
     This function will run every time we check for a new price - defined below
     """
@@ -10,18 +10,18 @@ def price_event(price, currency_pair):
 
 if __name__ == "__main__":
     # Authenticate coinbase pro strategy
-    coinbase_pro = Blankly.Coinbase_Pro()
+    coinbase_pro = blankly.CoinbasePro()
 
     # Define our interface in case we want to make our own API calls
     interface = coinbase_pro.get_interface()
 
     # Use our strategy helper on coinbase pro
-    coinbase_strategy = Blankly.Strategy(coinbase_pro)
+    coinbase_strategy = blankly.Strategy(coinbase_pro)
 
     # Run the price event function every time we check for a new price - by default that is 15 seconds
-    coinbase_strategy.add_price_event(price_event, currency_pair='BTC-USD', resolution='15s')
-    coinbase_strategy.add_price_event(price_event, currency_pair='LINK-USD', resolution='15s')
-    coinbase_strategy.add_price_event(price_event, currency_pair='ETH-BTC', resolution='15s')
+    coinbase_strategy.add_price_event(price_event, symbol='BTC-USD', resolution='15s')
+    coinbase_strategy.add_price_event(price_event, symbol='LINK-USD', resolution='15s')
+    coinbase_strategy.add_price_event(price_event, symbol='ETH-BTC', resolution='15s')
 
     # Start the strategy. This will begin each of the price event ticks
     coinbase_strategy.start()
diff --git a/Examples/keys_example.json b/examples/keys_example.json
similarity index 85%
rename from Examples/keys_example.json
rename to examples/keys_example.json
index 676bf119..9902c65b 100644
--- a/Examples/keys_example.json
+++ b/examples/keys_example.json
@@ -14,8 +14,8 @@
     },
     "alpaca": {
         "another cool portfolio": {
-            "API_KEY": "**************************************************************",
-            "API_SECRET": "*************************************************************"
+            "API_KEY": "********************",
+            "API_SECRET": "****************************************"
         }
     }
 }
\ No newline at end of file
diff --git a/Examples/modeling_example.py b/examples/modeling_example.py
similarity index 76%
rename from Examples/modeling_example.py
rename to examples/modeling_example.py
index 9510c928..b6a6c7c7 100644
--- a/Examples/modeling_example.py
+++ b/examples/modeling_example.py
@@ -1,4 +1,4 @@
-import Blankly
+import blankly
 import time
 from bokeh.plotting import figure, output_file, show
 
@@ -7,28 +7,27 @@
     market = 'BTC-USD'
     p = figure(plot_width=800, plot_height=900)
     # Construct our authenticated exchange
-    portfolio = Blankly.Coinbase_Pro()
+    portfolio = blankly.CoinbasePro()
     # Get the interface for API calls
     interface = portfolio.get_interface()
 
     # Download market data
     print("Downloading...")
-    history = interface.get_product_history(market, time.time() - Blankly.time_builder.build_day() * 300,
-                                            time.time(),
-                                            Blankly.time_builder.build_day())
+    history = interface.get_product_history(market, time.time() - blankly.time_builder.build_day() * 300, time.time(),
+                                            blankly.time_builder.build_day())
     # Create X-axis
     x = range(len(history['close']))
 
     # Shift the graph to match
     shift = [shift + 99 for shift in x]
     # Plot 100 day moving average
-    p.line(shift, Blankly.indicators.sma(history["close"], 100), line_width=2, line_color='pink',
+    p.line(shift, blankly.indicators.sma(history["close"], 100), line_width=2, line_color='pink',
            legend_label="100d SMA")
 
     # Shift the graph to match
     shift = [shift+49 for shift in x]
     # Plot 50 day moving average
-    p.line(shift, Blankly.indicators.sma(history["close"], 50), line_width=2, line_color='yellow',
+    p.line(shift, blankly.indicators.sma(history["close"], 50), line_width=2, line_color='yellow',
            legend_label="50d SMA")
 
     # Plot close price
diff --git a/Examples/multicore_bot.py b/examples/multicore_bot.py
similarity index 93%
rename from Examples/multicore_bot.py
rename to examples/multicore_bot.py
index dffb6515..a6c376f0 100644
--- a/Examples/multicore_bot.py
+++ b/examples/multicore_bot.py
@@ -1,9 +1,9 @@
 import time
-import Blankly
+import blankly
 # Have feedback? Use this: https://forms.gle/4oAjG9MKRTYKX2hP9
 
 
-class Bot(Blankly.BlanklyBot):
+class Bot(blankly.BlanklyBot):
     def main(self, args):
         """
         Main function to write any general analysis or bot management logic
@@ -48,7 +48,7 @@ def price_event(self, tick):
 
     # This creates an authenticated exchange and chooses the first portfolio in the keys.json file
     # You can use portfolio_name="my cool portfolio" if want a certain one
-    portfolio = Blankly.Coinbase_Pro()  # You could also use Blankly.Binance()
+    portfolio = blankly.CoinbasePro()  # You could also use blankly.binance()
 
     # Create the bot and add it to run as a coinbase_pro bitcoin model.
     bot = Bot()
@@ -62,5 +62,5 @@ def price_event(self, tick):
     while True:
         # Print the state from the model we just started every second
         state = portfolio.get_full_state("BTC-USD")
-        Blankly.utils.pretty_print_JSON(state)
+        blankly.utils.pretty_print_JSON(state)
         time.sleep(1)
diff --git a/examples/settings.json b/examples/settings.json
new file mode 100644
index 00000000..e131b7a5
--- /dev/null
+++ b/examples/settings.json
@@ -0,0 +1,20 @@
+{
+  "settings": {
+    "account_update_time": 5000,
+    "use_sandbox": true,
+    "use_sandbox_websockets": false,
+    "websocket_buffer_size": 10000,
+
+    "coinbase_pro": {
+      "cash": "USD"
+    },
+    "binance": {
+      "cash": "USDT",
+      "binance_tld": "us"
+    },
+    "alpaca": {
+      "websocket_stream": "iex",
+      "cash": "USD"
+    }
+  }
+}
\ No newline at end of file
diff --git a/setup.py b/setup.py
index d3bd8453..e30d282e 100644
--- a/setup.py
+++ b/setup.py
@@ -11,33 +11,35 @@
 """
 
 setup(
-    name='Blankly',  # How you named your package folder (MyLib)
+    name='blankly',  # How you named your package folder (MyLib)
     packages=find_packages(),
-    # packages=['Blankly'],  # Potentially should be the same thing as name
+    # packages=['blankly'],  # Potentially should be the same thing as name
     version='v0.1.12-alpha',
     license='lgpl-3.0',  # Licenses: https://help.github.com/articles/licensing-a-repository
-    description='Cryptocurrency bot development platform',  # Give a short description about your library
+    description='Rapidly build trading bots that run on any exchange',  # Give a short description about your library
     long_description=long_description,
     long_description_content_type='text/markdown',
-    author='Emerson Dove',
+    author='blankly',
+    entry_points={'console_scripts': ['blankly = blankly.command.cli:main']},
     author_email='contact@blankly.finance',
     url='https://github.com/Blankly-Finance/Blankly',  # Could be github or website
     # download_url='https://github.com/EmersonDove/Blankly/archive/v0.1.1-alpha.tar.gz',
-    keywords=['Crypto', 'Exchanges', 'Bot'],  # Keywords
+    keywords=['Crypto', 'Stocks', 'Quantitative Finance', 'Exchanges', 'Bot'],  # Keywords
     install_requires=[
+        'alpaca-trade-api >= 1.2.3',
+        'bokeh',
+        'dateparser',
+        'newtulipy',
         'numpy',
-        'sklearn',
-        'scikit-learn',
-        'zerorpc',
-        'requests',
-        'websocket-client',
         'pandas',
-        'python-binance',
-        'tulipy',
-        'alpaca-trade-api',
+        'pandas_market_calendars',
         'pytest',
-        'bokeh',
-        'pytest-mock'
+        'pytest-mock',
+        'python-binance',
+        'requests',
+        'scikit-learn',
+        'websocket-client',
+        'zerorpc'
     ],
     classifiers=[
         # Possible: "3 - Alpha", "4 - Beta" or "5 - Production/Stable"


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index b0510d96..372b5f10 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,22 +1,8 @@
-# This workflow will install Python dependencies, run tests and lint with a single version of Python
-# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+# This runs all blankly Module Tests
 
-# Avoiding these tests currently because they mess with clone history.
-
-# name: Blankly Module Tests
-
-on:
-  push:
-    branches:
-      - main
-
-  pull_request:
-    branches:
-      - main
-      - development
-
-  workflow_dispatch:
+name: blankly Module Tests
 
+on: [push, pull_request, workflow_dispatch]
 
 jobs:
   build:
@@ -37,11 +23,10 @@ jobs:
 
     - name: Inject Keys
       run: |
-        KEYS_FILE=${{ secrets.SANDBOX_KEYS }}
-
-        echo $keys_file >> ./tests/config/keys.json
+        echo "$KEYS_FILE" > ./tests/config/keys.json
       shell: bash
-
+      env: 
+        KEYS_FILE: ${{ secrets.SANDBOX_KEYS }}
     - name: Run test file
       run: |
         pytest
diff --git a/Blankly/exchanges/Paper_Trade/backtest_controller.py b/Blankly/exchanges/Paper_Trade/backtest_controller.py
deleted file mode 100644
index e20e37ae..00000000
--- a/Blankly/exchanges/Paper_Trade/backtest_controller.py
+++ /dev/null
@@ -1,294 +0,0 @@
-"""
-    Backtesting controller for paper trading interface
-    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 <https://www.gnu.org/licenses/>.
-"""
-
-
-from Blankly.exchanges.Paper_Trade.Paper_Trade import PaperTrade
-from Blankly.exchanges.Paper_Trade.Paper_Trade_Interface import PaperTradeInterface
-from Blankly.utils.time_builder import time_interval_to_seconds
-from Blankly.utils.utils import load_backtest_preferences, write_backtest_preferences, update_progress
-
-import typing
-import pandas as pd
-from datetime import datetime
-import traceback
-from bokeh.plotting import figure, show
-from bokeh.layouts import column as bokeh_columns
-from bokeh.palettes import Category10_10
-import os
-
-
-def to_string_key(separated_list):
-    output = ""
-    for i in range(len(separated_list) - 1):
-        output += str(separated_list[i])
-        output += ","
-    output += str(separated_list[-1:][0])
-    return output
-
-
-class BackTestController:
-    def __init__(self, paper_trade_exchange: PaperTrade, backtest_settings_path=None):
-        self.preferences = load_backtest_preferences(backtest_settings_path)
-        self.backtest_settings_path = backtest_settings_path
-        if not paper_trade_exchange.get_type() == "paper_trade":
-            raise ValueError("Backtest controller was not constructed with a paper trade exchange object.")
-        self.interface = paper_trade_exchange.get_interface()  # type: PaperTradeInterface
-
-        self.price_events = []
-
-        self.current_time = None
-        self.initial_time = None
-
-        self.prices = []  # [epoch, "BTC-USD", price]
-
-        self.price_dictionary = {}
-
-        self.sync_prices()
-
-        self.queue_backtest_write = False
-
-    def sync_prices(self, save=True) -> dict:
-        cache_folder = self.preferences['settings']["cache_location"]
-        # Make sure the cache folder exists and read files
-        try:
-            files = os.listdir(cache_folder)
-        except FileNotFoundError:
-            files = []
-            os.mkdir(cache_folder)
-
-        available_files = []
-        for i in range(len(files)):
-            # example file name: 'BTC-USD.1622400000.1622510793.60.csv'
-            # Remove the .csv from each of the files: BTC-USD.1622400000.1622510793.60
-            identifier = files[i][:-4].split(",")
-            identifier[1] = float(identifier[1])
-            identifier[2] = float(identifier[2])
-            identifier[3] = float(identifier[3])
-            available_files.append(identifier)
-
-        assets = self.preferences['price_data']['assets']  # type: dict
-        for i in assets:
-            identifier = [i[0], i[1], i[2], i[3]]
-            string_identifier = to_string_key(identifier)
-            if identifier in available_files:
-                # Read the csv here
-                if tuple(identifier) not in self.price_dictionary.keys():
-                    print("Including: " + string_identifier + ".csv in backtest.")
-                    self.price_dictionary[tuple(identifier)] = pd.read_csv(os.path.join(cache_folder,
-                                                                           string_identifier + ".csv")
-                                                                           )
-            else:
-                if tuple(identifier) not in self.price_dictionary.keys():
-                    print("No exact cache exists for " + str(identifier[0]) + " from " + str(identifier[1]) + " to " +
-                          str(identifier[2]) + " at " + str(identifier[3]) + "s resolution. Downloading...")
-                    download = self.interface.get_product_history(identifier[0],
-                                                                  identifier[1],
-                                                                  identifier[2],
-                                                                  identifier[3])
-                    self.price_dictionary[tuple(identifier)] = download
-                    if save:
-                        download.to_csv(os.path.join(cache_folder, string_identifier + ".csv"), index=False)
-
-        # Merge all the same asset ids into the same dictionary spots
-        unique_assets = {}
-        for k, v in self.price_dictionary.items():
-            if k[0] in unique_assets:
-                unique_assets[k[0]] = pd.concat([unique_assets[k[0]], v], ignore_index=True)
-            else:
-                unique_assets[k[0]] = v
-
-        return unique_assets
-
-    def add_prices(self, asset_id, start_time, end_time, resolution, save=False):
-        # Create its unique identifier
-        identifier = [asset_id, start_time, end_time, resolution]
-
-        # If it's not loaded then write it to the file
-        if tuple(identifier) not in self.price_dictionary.keys():
-            self.preferences['price_data']['assets'].append(identifier)
-            if save:
-                self.queue_backtest_write = True
-        else:
-            print("already identified")
-
-        # Ensure everything is up to date
-        self.sync_prices(save)
-
-    def append_backtest_price_event(self, callback: typing.Callable, asset_id, time_interval):
-        if isinstance(time_interval, str):
-            time_interval = time_interval_to_seconds(time_interval)
-        self.price_events.append([callback, asset_id, time_interval])
-
-    def write_setting(self, key, value, save=False):
-        """
-        Write a setting to the .json preferences
-
-        Args:
-            key: Key under settings
-            value: Value to set that settings key to
-            save: Write this new setting to the file
-        """
-        self.preferences['settings'][key] = value
-        if save:
-            self.queue_backtest_write = True
-
-        self.sync_prices(save)
-
-    def write_initial_price_values(self, account_dictionary):
-        """
-        Write in a new price dictionary for the paper trade exchange.
-        """
-        self.interface.override_local_account(account_dictionary)
-
-    def format_account_data(self, local_time) -> dict:
-        available_dict = {}
-        # Grab the account status
-        account_status = self.interface.get_account()
-        for i in account_status:
-            # Funds on hold are still added
-            available_dict[i['currency']] = i['available'] + i['hold']
-        # Make sure to add the time key in
-        available_dict['time'] = local_time
-
-        return available_dict
-
-    def run(self):
-        """
-        Setup
-        """
-        # Write our queued edits to the file
-        if self.queue_backtest_write:
-            write_backtest_preferences(self.preferences, self.backtest_settings_path)
-
-        prices = self.sync_prices(False)
-
-        # Organize each price into this structure: [epoch, "BTC-USD", price]
-        use_price = self.preferences['settings']['use_price']
-        for k, v in prices.items():
-            frame = v  # type: pd.DataFrame
-
-            # Be sure to push these initial prices to the strategy
-            self.interface.receive_price(k, v[use_price].iloc[0])
-
-            for index, row in frame.iterrows():
-                # TODO iterrows() is allegedly pretty slow
-                self.prices.append([row.time, k, row[use_price]])
-
-        self.prices = sorted(self.prices, key=lambda x: x[0])
-
-        self.current_time = self.prices[0][0]
-        self.initial_time = self.current_time
-
-        # Add a section to the price events which controls the next time they run & change to array of dicts
-        for i in range(len(self.price_events)):
-            self.price_events[i] = {
-                'function': self.price_events[i][0],
-                'asset_id': self.price_events[i][1],
-                'interval': self.price_events[i][2],
-                'next_run': self.initial_time
-            }
-
-        if prices == {} or self.price_events == []:
-            raise ValueError("Either no price data or backtest events given. "
-                             "Use .append_backtest_price_data or "
-                             "append_backtest_price_event to create the backtest model.")
-        """
-        Begin backtesting
-        """
-        self.interface.set_backtesting(True)
-        account = self.interface.get_account()
-        column_keys = ['time']
-        for i in account:
-            column_keys.append(i['currency'])
-        cycle_status = pd.DataFrame(columns=column_keys)
-
-        # Append dictionaries to this to make the pandas dataframe
-        price_data = []
-
-        # Add an initial account row here
-        if self.preferences['settings']['save_initial_account_value']:
-            available_dict = self.format_account_data(self.initial_time)
-            price_data.append(available_dict)
-
-        show_progress = self.preferences['settings']['show_progress_during_backtest']
-
-        print("\nBacktesting...")
-        price_number = len(self.prices)
-        try:
-            for i in range(price_number):
-                price_array = self.prices[i]
-                if show_progress:
-                    update_progress(i/price_number)
-                self.interface.receive_price(price_array[1], price_array[2])
-                self.current_time = price_array[0]
-                self.interface.receive_time(self.current_time)
-                self.interface.evaluate_limits()
-
-                for function_dict in self.price_events:
-                    while function_dict['next_run'] <= self.current_time:
-                        local_time = function_dict['next_run']
-                        # This is the actual callback to the user space
-                        function_dict['function'](self.interface.get_price(function_dict['asset_id']),
-                                                  function_dict['asset_id'])
-
-                        # Delay the next run until after the interval
-                        function_dict['next_run'] += function_dict['interval']
-
-                        available_dict = self.format_account_data(local_time)
-                        price_data.append(available_dict)
-        except Exception:
-            traceback.print_exc()
-
-        # Push the accounts to the dataframe
-        cycle_status = cycle_status.append(price_data, ignore_index=True)
-
-        if self.preferences['settings']['GUI_output']:
-            figures = []
-            global_x_range = None
-
-            show_zero_delta = self.preferences['settings']['show_tickers_with_zero_delta']
-
-            color = Category10_10.__iter__()
-            for column in cycle_status:
-                if column != 'time' and (not (cycle_status[column][0] == cycle_status[column].iloc[-1]) or
-                                         show_zero_delta):
-
-                    p = figure(plot_width=900, plot_height=200, x_axis_type='datetime')
-                    time = [datetime.fromtimestamp(ts) for ts in cycle_status['time']]
-                    p.step(time, cycle_status[column].tolist(),
-                           line_width=2,
-                           color=next(color),
-                           legend_label=column,
-                           mode="before",
-                           )
-                    # Format graph
-                    p.legend.location = "top_left"
-                    p.legend.title = column
-                    p.legend.title_text_font_style = "bold"
-                    p.legend.title_text_font_size = "20px"
-                    if global_x_range is None:
-                        global_x_range = p.x_range
-                    else:
-                        p.x_range = global_x_range
-
-                    figures.append(p)
-
-            show(bokeh_columns(figures))
-
-        self.interface.set_backtesting(False)
-        return cycle_status
diff --git a/Blankly/utils/ci_utils/tests_controller.py b/Blankly/utils/ci_utils/tests_controller.py
deleted file mode 100644
index a5ee8ce8..00000000
--- a/Blankly/utils/ci_utils/tests_controller.py
+++ /dev/null
@@ -1,10 +0,0 @@
-import unittest
-from root.test_auth_constructor import TestAuthConstructor
-
-from root.exchanges.test_interface_homogeneity import InterfaceHomogeneity
-from root.exchanges.Coinbase_Pro.test_Coinbase_Pro_Interface import CoinbaseInterface
-from root.exchanges.Binance.test_Binance_Interface import BinanceInterface
-
-
-if __name__ == "__main__":
-    unittest.main()
diff --git a/blankly/exchanges/interfaces/paper_trade/backtest_controller.py b/blankly/exchanges/interfaces/paper_trade/backtest_controller.py
new file mode 100644
index 00000000..6c51a87f
--- /dev/null
+++ b/blankly/exchanges/interfaces/paper_trade/backtest_controller.py
@@ -0,0 +1,549 @@
+"""
+    Backtesting controller for paper trading interface
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+import os
+import traceback
+import typing
+from datetime import datetime as dt
+
+import pandas as pd
+from bokeh.layouts import column as bokeh_columns
+from bokeh.models import HoverTool
+from bokeh.palettes import Category10_10
+from bokeh.plotting import ColumnDataSource, figure, show
+
+import blankly.exchanges.interfaces.paper_trade.metrics as metrics
+from blankly.exchanges.interfaces.paper_trade.backtest_result import BacktestResult
+from blankly.exchanges.interfaces.paper_trade.paper_trade import PaperTrade
+from blankly.exchanges.interfaces.paper_trade.paper_trade_interface import PaperTradeInterface
+from blankly.utils.time_builder import time_interval_to_seconds
+from blankly.utils.utils import load_backtest_preferences, update_progress, write_backtest_preferences
+
+
+def to_string_key(separated_list):
+    output = ""
+    for i in range(len(separated_list) - 1):
+        output += str(separated_list[i])
+        output += ","
+    output += str(separated_list[-1:][0])
+    return output
+
+
+class BackTestController:
+    def __init__(self, paper_trade_exchange: PaperTrade, backtest_settings_path: str = None, callbacks: list = None):
+        self.preferences = load_backtest_preferences(backtest_settings_path)
+        self.backtest_settings_path = backtest_settings_path
+        if not paper_trade_exchange.get_type() == "paper_trade":
+            raise ValueError("Backtest controller was not constructed with a paper trade exchange object.")
+        self.interface = paper_trade_exchange.get_interface()  # type: PaperTradeInterface
+
+        self.price_events = []
+
+        if callbacks is None:
+            callbacks = []
+        self.callbacks = callbacks
+
+        self.current_time = None
+        self.initial_time = None
+
+        self.prices = []  # [epoch, "BTC-USD", price]
+
+        self.price_dictionary = {}
+
+        self.pd_prices = None
+
+        self.sync_prices()
+
+        self.use_price = None
+
+        self.queue_backtest_write = False
+
+        self.quote_currency = self.preferences['settings']['quote_account_value_in']
+
+        self.initial_account = {}
+
+    def sync_prices(self, save=True) -> dict:
+        cache_folder = self.preferences['settings']["cache_location"]
+        # Make sure the cache folder exists and read files
+        try:
+            files = os.listdir(cache_folder)
+        except FileNotFoundError:
+            files = []
+            os.mkdir(cache_folder)
+
+        available_files = []
+        for i in range(len(files)):
+            # example file name: 'BTC-USD.1622400000.1622510793.60.csv'
+            # Remove the .csv from each of the files: BTC-USD.1622400000.1622510793.60
+            identifier = files[i][:-4].split(",")
+            identifier[1] = float(identifier[1])
+            identifier[2] = float(identifier[2])
+            identifier[3] = float(identifier[3])
+            available_files.append(identifier)
+
+        assets = self.preferences['price_data']['assets']  # type: dict
+        for i in assets:
+            identifier = [i[0], i[1], i[2], i[3]]
+            string_identifier = to_string_key(identifier)
+            if identifier in available_files:
+                # Read the csv here
+                if tuple(identifier) not in self.price_dictionary.keys():
+                    print("Including: " + string_identifier + ".csv in backtest.")
+                    self.price_dictionary[tuple(identifier)] = pd.read_csv(os.path.join(cache_folder,
+                                                                                        string_identifier + ".csv")
+                                                                           )
+            else:
+                if tuple(identifier) not in self.price_dictionary.keys():
+                    print("No exact cache exists for " + str(identifier[0]) + " from " + str(identifier[1]) + " to " +
+                          str(identifier[2]) + " at " + str(identifier[3]) + "s resolution. Downloading...")
+                    download = self.interface.get_product_history(identifier[0], identifier[1], identifier[2],
+                                                                  identifier[3])
+                    self.price_dictionary[tuple(identifier)] = download
+                    if save:
+                        download.to_csv(os.path.join(cache_folder, string_identifier + ".csv"), index=False)
+
+        # Merge all the same asset ids into the same dictionary spots
+        unique_assets = {}
+        for k, v in self.price_dictionary.items():
+            if k[0] in unique_assets:
+                unique_assets[k[0]] = pd.concat([unique_assets[k[0]], v], ignore_index=True)
+            else:
+                unique_assets[k[0]] = v
+
+        return unique_assets
+
+    def add_prices(self, asset_id, start_time, end_time, resolution, save=False):
+        # Create its unique identifier
+        identifier = [asset_id, start_time, end_time, resolution]
+
+        # If it's not loaded then write it to the file
+        if tuple(identifier) not in self.price_dictionary.keys():
+            self.preferences['price_data']['assets'].append(identifier)
+            if save:
+                self.queue_backtest_write = True
+        else:
+            print("already identified")
+
+        # Ensure everything is up to date
+        self.sync_prices(save)
+
+    def append_backtest_price_event(self, callback: typing.Callable, asset_id, time_interval, state_object, ohlc):
+        if isinstance(time_interval, str):
+            time_interval = time_interval_to_seconds(time_interval)
+        self.price_events.append([callback, asset_id, time_interval, state_object, ohlc])
+
+    def __determine_price(self, asset_id, epoch):
+
+        # Geeksforgeeks binary search implementation
+        def binary_search(arr, low, high, x):
+            # Check base case
+            if high >= low:
+
+                mid = (high + low) // 2
+
+                # Modify this to get our sort-of-right behavior
+                try:
+                    if arr[mid] <= x <= arr[mid + 1]:
+                        return mid
+                    elif arr[mid] > x:
+                        return binary_search(arr, low, mid - 1, x)
+                except IndexError:
+                    return len(arr) - 1
+
+                # If element is smaller than mid, then it can only
+                # be present in left subarray
+
+                # Else the element can only be present in right subarray
+                else:
+                    return binary_search(arr, mid + 1, high, x)
+
+            else:
+                # Must be before this case
+                return 0
+
+        try:
+            prices = self.pd_prices[asset_id][self.use_price]  # type: pd.Series
+            times = self.pd_prices[asset_id]['time']  # type: pd.Series
+
+            # Iterate and find the correct quote price
+            index = binary_search(times, 0, times.size, epoch)
+            return prices[index]
+        except KeyError:
+            return 0
+
+    def write_setting(self, key, value, save=False):
+        """
+        Write a setting to the .json preferences
+
+        Args:
+            key: Key under settings
+            value: Value to set that settings key to
+            save: Write this new setting to the file
+        """
+        self.preferences['settings'][key] = value
+        if save:
+            self.queue_backtest_write = True
+
+        self.sync_prices(save)
+
+    def write_initial_price_values(self, account_dictionary):
+        """
+        Write in a new price dictionary for the paper trade exchange.
+        """
+        self.interface.override_local_account(account_dictionary)
+
+    def format_account_data(self, local_time) -> typing.Tuple[typing.Dict[typing.Union[str, typing.Any],
+                                                                          typing.Union[int, typing.Any]],
+                                                              typing.Dict[typing.Union[str, typing.Any],
+                                                                          typing.Union[int, typing.Any]]]:
+        true_available = {}
+        # Grab the account status
+        true_account = self.interface.get_account()
+
+        # Create an account total value
+        value_total = 0
+
+        no_trade_available = {}
+        # No trade account total
+        no_trade_value = 0
+
+        for i in true_account.keys():
+            # Funds on hold are still added
+            true_available[i] = true_account[i]['available'] + true_account[i]['hold']
+            no_trade_available[i] = self.initial_account[i]['available'] + self.initial_account[i]['hold']
+            currency_pair = i
+
+            # Convert to quote (this could be optimized a bit)
+            currency_pair += '-'
+            currency_pair += self.quote_currency
+
+            # Get price at time
+            price = self.__determine_price(currency_pair, local_time)
+            value_total += price * true_available[i]
+            no_trade_value += price * no_trade_available[i]
+
+        # Make sure to add the time key in
+        true_available['time'] = local_time
+        no_trade_available['time'] = local_time
+
+        value_total += true_account[self.quote_currency]['available'] + true_account[self.quote_currency]['hold']
+
+        no_trade_value += self.initial_account[self.quote_currency]['available'] + \
+                          self.initial_account[self.quote_currency]['hold']
+
+        true_available['Account Value (' + self.quote_currency + ')'] = value_total
+
+        no_trade_available['Account Value (No Trades)'] = no_trade_value
+
+        return true_available, no_trade_available
+
+    def run(self) -> BacktestResult:
+        """
+        Setup
+        """
+
+        # Create this initial so that we can compare how our strategy performs
+        self.initial_account = self.interface.get_account()
+
+        # Write our queued edits to the file
+        if self.queue_backtest_write:
+            write_backtest_preferences(self.preferences, self.backtest_settings_path)
+
+        prices = self.sync_prices(False)
+
+        # Organize each price into this structure: [epoch, "BTC-USD", price, open, high, low, close, volume]
+        use_price = self.preferences['settings']['use_price']
+        self.use_price = use_price
+
+        # Sort each column by the use price
+        for column in prices.keys():
+            prices[column] = prices[column].sort_values('time')
+
+        self.pd_prices = {**prices}
+
+        for k, v in prices.items():
+            frame = v  # type: pd.DataFrame
+
+            # Be sure to push these initial prices to the strategy
+            self.interface.receive_price(k, v[use_price].iloc[0])
+
+            for index, row in frame.iterrows():
+                # TODO iterrows() is allegedly pretty slow
+                self.prices.append([row.time, k, row[use_price],
+                                    row['open'],  # (index) 3
+                                    row['high'],  # 4
+                                    row['low'],  # 5
+                                    row['close'],  # 6
+                                    row['volume']])  # 7
+
+        self.current_time = self.prices[0][0]
+        self.initial_time = self.current_time
+
+        # Add a section to the price events which controls the next time they run & change to array of dicts
+        for i in range(len(self.price_events)):
+            self.price_events[i] = {
+                'function': self.price_events[i][0],
+                'asset_id': self.price_events[i][1],
+                'interval': self.price_events[i][2],
+                'state_object': self.price_events[i][3],
+                'next_run': self.initial_time,
+                'ohlc': self.price_events[i][4]
+            }
+
+        if prices == {} or self.price_events == []:
+            raise ValueError("Either no price data or backtest events given. "
+                             "Use .append_backtest_price_data or "
+                             "append_backtest_price_event to create the backtest model.")
+        """
+        Begin backtesting
+        """
+        self.interface.set_backtesting(True)
+        account = self.interface.get_account()
+        column_keys = list(account.keys())
+        column_keys.append('time')
+
+        # column_keys = ['time']
+        # for i in account.keys():
+        #     column_keys.append(i['currency'])
+
+        cycle_status = pd.DataFrame(columns=column_keys)
+
+        no_trade_cycle_status = pd.DataFrame(columns=column_keys)
+
+        # Append dictionaries to this to make the pandas dataframe
+        price_data = []
+
+        # Append dictionaries to this to make the no trade dataframe
+        no_trade = []
+
+        # Add an initial account row here
+        if self.preferences['settings']['save_initial_account_value']:
+            available_dict = self.format_account_data(self.initial_time)
+            price_data.append(available_dict)
+
+        show_progress = self.preferences['settings']['show_progress_during_backtest']
+
+        print("\nBacktesting...")
+        price_number = len(self.prices)
+        try:
+            for i in range(price_number):
+                price_array = self.prices[i]
+                if show_progress:
+                    update_progress(i / price_number)
+                self.interface.receive_price(price_array[1], price_array[2])
+                self.current_time = price_array[0]
+                self.interface.receive_time(self.current_time)
+                self.interface.evaluate_limits()
+
+                for function_dict in self.price_events:
+                    while function_dict['next_run'] <= self.current_time:
+                        local_time = function_dict['next_run']
+                        # This is the actual callback to the user space
+                        if function_dict['ohlc']:
+                            # This pulls all the price data out of the price array defined on line 260
+                            function_dict['function']({'open': price_array[3],
+                                                       'high': price_array[4],
+                                                       'low': price_array[5],
+                                                       'close': price_array[6],
+                                                       'volume': price_array[7]},
+
+                                                      function_dict['asset_id'],
+                                                      function_dict['state_object'])
+                        else:
+                            function_dict['function'](self.interface.get_price(function_dict['asset_id']),
+                                                      function_dict['asset_id'], function_dict['state_object'])
+
+                        # Delay the next run until after the interval
+                        function_dict['next_run'] += function_dict['interval']
+
+                        available_dict, no_trade_dict = self.format_account_data(local_time)
+                        price_data.append(available_dict)
+
+                        no_trade.append(no_trade_dict)
+        except Exception:
+            traceback.print_exc()
+
+        # Push the accounts to the dataframe
+        cycle_status = cycle_status.append(price_data, ignore_index=True)
+
+        no_trade_cycle_status = no_trade_cycle_status.append(no_trade, ignore_index=True)
+
+        print("Creating report...")
+
+        figures = []
+        # for i in self.prices:
+        #     result_index = cycle_status['time'].sub(i[0]).abs().idxmin()
+        #     for i in cycle_status.iloc[result_index]:
+
+        hover = HoverTool(
+            tooltips=[
+                ('value', '@value')
+            ],
+
+            # formatters={
+            #     'time': 'datetime',  # use 'datetime' formatter for 'date' field
+            #     '@{value}': 'printf',   # use 'printf' formatter for '@{adj close}' field
+            # },
+
+            # display a tooltip whenever the cursor is vertically in line with a glyph
+            mode='vline'
+        )
+
+        # Back up the epoch list so that it can be used later for re-sampling
+        epoch_backup = cycle_status['time'].tolist()
+
+        if self.preferences['settings']['GUI_output']:
+            global_x_range = None
+
+            show_zero_delta = self.preferences['settings']['show_tickers_with_zero_delta']
+
+            color = Category10_10.__iter__()
+
+            time = [dt.fromtimestamp(ts) for ts in cycle_status['time']]
+
+            for column in cycle_status:
+                if column != 'time' and (not (cycle_status[column][0] == cycle_status[column].iloc[-1]) or
+                                         show_zero_delta):
+
+                    p = figure(plot_width=900, plot_height=200, x_axis_type='datetime')
+                    source = ColumnDataSource(data=dict(
+                        time=time,
+                        value=cycle_status[column].tolist()
+                    ))
+                    p.step('time', 'value',
+                           source=source,
+                           line_width=2,
+                           color=next(color),
+                           legend_label=column,
+                           mode="before",
+                           )
+
+                    # Replica of whats above to add the no-trade line to the backtest
+                    if column == 'Account Value (' + self.quote_currency + ')':
+                        source = ColumnDataSource(data=dict(
+                            time=time,
+                            value=no_trade_cycle_status['Account Value (No Trades)'].tolist()
+                        ))
+                        p.step('time', 'value',
+                               source=source,
+                               line_width=2,
+                               color=next(color),
+                               legend_label='Account Value (No Trades)',
+                               mode="before")
+
+                    p.add_tools(hover)
+
+                    # Format graph
+                    p.legend.location = "top_left"
+                    p.legend.title = column
+                    p.legend.title_text_font_style = "bold"
+                    p.legend.title_text_font_size = "20px"
+                    if global_x_range is None:
+                        global_x_range = p.x_range
+                    else:
+                        p.x_range = global_x_range
+
+                    figures.append(p)
+
+            show(bokeh_columns(figures))
+
+        def is_number(s):
+            try:
+                float(s)
+                return True
+            except ValueError:
+                return False
+
+        dataframes = {
+            'history': cycle_status
+        }
+        metrics_indicators = {}
+        user_callbacks = {}
+
+        # Check if it needs resampling
+        resample_setting = self.preferences['settings']['resample_account_value_for_metrics']
+        if isinstance(resample_setting, str) or is_number(resample_setting):
+            # Backing arrays. We can't append directly to the dataframe so array has to also be made
+            resampled_returns = pd.DataFrame(columns=['time', 'value'])
+            resampled_backing_array = []
+
+            # Find the interval second value
+            interval_value = time_interval_to_seconds(resample_setting)
+
+            # Assign start and stop limits
+            epoch_start = epoch_backup[0]
+            epoch_max = epoch_backup[len(epoch_backup) - 1]
+
+            # Going to push this in as a single column version of our price data so that __determine_price can handle it
+            self.pd_prices['Account Value (' + self.quote_currency + ')'] = pd.DataFrame()
+            self.pd_prices['Account Value (' + self.quote_currency + ')'][use_price] = \
+                cycle_status['Account Value (' + self.quote_currency + ')']
+
+            self.pd_prices['Account Value (' + self.quote_currency + ')']['time'] = cycle_status['time']
+
+            while epoch_start <= epoch_max:
+                # Append this dict to the array
+                resampled_backing_array.append({
+                    'time': epoch_start,
+                    'value': self.__determine_price('Account Value (' + self.quote_currency + ')', epoch_start)
+                })
+
+                # Increase the epoch value
+                epoch_start += interval_value
+
+            # Put this in the dataframe
+            resampled_returns = resampled_returns.append(resampled_backing_array, ignore_index=True)
+
+            # This is the resampled version
+            dataframes['resampled_account_value'] = resampled_returns
+
+            # Now we need to copy it and find the differences
+            returns = resampled_returns.copy(deep=True)
+
+            # Default diff parameters should do it
+            returns['value'] = returns['value'].diff()
+
+            # Now write it to our dictionary
+            dataframes['returns'] = returns
+
+            # -----=====*****=====----- I thought I stopped doing these comments when I actually learned to code
+            metrics_indicators['cagr'] = metrics.cagr(dataframes)
+            try:
+                metrics_indicators['cum_returns'] = metrics.cum_returns(dataframes)
+            except ZeroDivisionError:
+                raise ZeroDivisionError("Division by zero when calculating cum returns. "
+                                        "Are there valid account datapoints?")
+            metrics_indicators['sortino'] = metrics.sortino(dataframes)
+            metrics_indicators['sharpe'] = metrics.sharpe(dataframes)
+            metrics_indicators['calmar'] = metrics.calmar(dataframes)
+            metrics_indicators['volatility'] = metrics.volatility(dataframes)
+            metrics_indicators['variance'] = metrics.variance(dataframes)
+            # return_dict['beta'] = metrics.beta(return_dict)
+            metrics_indicators['var'] = metrics.var(dataframes)
+            metrics_indicators['cvar'] = metrics.cvar(dataframes)
+            # return_dict['max_drawdown'] = metrics.cagr(return_dict)
+            # -----=====*****=====-----
+
+        # Run this last so that the user can override what they want
+        for callback in self.callbacks:
+            user_callbacks[callback.__name__] = callback(dataframes)
+
+        result_object = BacktestResult(dataframes, metrics_indicators, user_callbacks)
+
+        self.interface.set_backtesting(False)
+        return result_object
diff --git a/blankly/exchanges/interfaces/paper_trade/backtest_result.py b/blankly/exchanges/interfaces/paper_trade/backtest_result.py
new file mode 100644
index 00000000..6aff272d
--- /dev/null
+++ b/blankly/exchanges/interfaces/paper_trade/backtest_result.py
@@ -0,0 +1,69 @@
+"""
+    Object for cleanly storing & printing complex backtest results.
+    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 <https://www.gnu.org/licenses/>.
+"""
+
+from pandas import DataFrame
+
+
+class BacktestResult:
+    def __init__(self, dataframes: dict, metrics: dict, user_callbacks: dict):
+        self.dataframes = dataframes
+        self.metrics = metrics
+        self.user_callbacks = user_callbacks
+
+    def get_account_history(self) -> DataFrame:
+        return self.dataframes['history']
+
+    def get_returns(self) -> DataFrame:
+        return self.dataframes['returns']
+
+    def get_resampled_account(self) -> DataFrame:
+        return self.dataframes['resampled_account_value']
+
+    def get_user_callback_results(self) -> dict:
+        return self.user_callbacks
+
+    def get_metrics(self) -> dict:
+        return self.metrics
+
+    def __str__(self):
+        return_string = "\n"
+        return_string += "Historical Dataframes: \n"
+
+        return_string += "Account History: \n"
+        return_string += self.dataframes['history'].__str__()
+        return_string += "\n"
+
+        return_string += "Account Returns: \n"
+        return_string += self.dataframes['returns'].__str__()
+        return_string += "\n"
+
+        return_string += "Resampled Account Value: \n"
+        return_string += self.dataframes['resampled_account_value'].__str__()
+        return_string += "\n"
+
+        return_string += "blankly Metrics: \n"
+        for i in self.metrics.keys():
+            return_string += i + ": " + str(self.metrics[i]) + "\n"
+
+        if self.user_callbacks != {}:
+            return_string += "\n"
+            return_string += "User Callbacks: \n"
+            for i in self.user_callbacks.keys():
+                return_string += i + ": " + str(self.user_callbacks[i]) + "\n"
+
+        return return_string
diff --git a/Blankly/exchanges/Paper_Trade/backtesting_wrapper.py b/blankly/exchanges/interfaces/paper_trade/backtesting_wrapper.py
similarity index 99%
rename from Blankly/exchanges/Paper_Trade/backtesting_wrapper.py
rename to blankly/exchanges/interfaces/paper_trade/backtesting_wrapper.py
index 95080eab..9ddccccb 100644
--- a/Blankly/exchanges/Paper_Trade/backtesting_wrapper.py
+++ b/blankly/exchanges/interfaces/paper_trade/backtesting_wrapper.py
@@ -38,6 +38,7 @@ def receive_price(self, asset_id, new_price):
     """
     Override functions for manipulating backtesting
     """
+
     def get_backtesting_price(self, asset_id):
         try:
             return self.frame['prices'][asset_id]
diff --git a/Examples/backtest.json b/examples/backtest.json
similarity index 79%
rename from Examples/backtest.json
rename to examples/backtest.json
index e74e0d17..8d16e7dd 100644
--- a/Examples/backtest.json
+++ b/examples/backtest.json
@@ -22,6 +22,8 @@
     "show_tickers_with_zero_delta": false,
     "save_initial_account_value": false,
     "show_progress_during_backtest": false,
-    "cache_location": "./price_caches"
+    "cache_location": "./price_caches",
+    "resample_account_value_for_metrics": "1d",
+    "quote_account_value_in": "USD"
   }
 }
\ No newline at end of file
diff --git a/Examples/backtesting_example.py b/examples/backtesting_example.py
similarity index 76%
rename from Examples/backtesting_example.py
rename to examples/backtesting_example.py
index 2a7efdde..afb60fdb 100644
--- a/Examples/backtesting_example.py
+++ b/examples/backtesting_example.py
@@ -1,16 +1,16 @@
-import Blankly
+import blankly
 
 
-def price_event(price, product_id):
+def price_event(price, product_id, state):
     # Run this every "hour"
     interface = strategy.Interface
 
     usd_amount = interface.get_account('USD')['available']
 
-    # Try to make our account value match 1/100 of the price.
-    price = price/100
+    # Try to make our account value match the price.
+    # Note we have to round the delta because the exchange has a set quote resolution
+    delta = round(price-usd_amount, 2)
 
-    delta = price-usd_amount
     if delta > 10:
         strategy.Interface.market_order(product_id, 'sell', delta)
     if delta < -10:
@@ -23,10 +23,10 @@ def price_event(price, product_id):
     """
     print("Authenticating...")
     # Create an authenticated coinbase pro object
-    coinbase_pro = Blankly.Coinbase_Pro()
+    coinbase_pro = blankly.CoinbasePro()
 
     # Create a strategy object
-    strategy = Blankly.Strategy(coinbase_pro)
+    strategy = blankly.Strategy(coinbase_pro)
 
     """
     Backtest
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/tests/config/keys_demo.json b/tests/config/keys_demo.json
index 0c092ef5..70054afe 100644
--- a/tests/config/keys_demo.json
+++ b/tests/config/keys_demo.json
@@ -1,13 +1,13 @@
 {
     "coinbase_pro": {
-        "alpaca test portfolio": {
+        "coinbase pro test portfolio": {
             "API_KEY": "******************************",
             "API_SECRET": "**************************************************************************************",
             "API_PASS": "**********"
         }
     },
     "binance": {
-        "alpaca test portfolio": {
+        "binance test portfolio": {
             "API_KEY": "**************************************************************",
             "API_SECRET": "*************************************************************"
         }
diff --git a/tests/config/settings.json b/tests/config/settings.json
index cb6cc007..e131b7a5 100644
--- a/tests/config/settings.json
+++ b/tests/config/settings.json
@@ -3,7 +3,18 @@
     "account_update_time": 5000,
     "use_sandbox": true,
     "use_sandbox_websockets": false,
-    "binance_tld": "us",
-    "websocket_buffer_size": 10000
+    "websocket_buffer_size": 10000,
+
+    "coinbase_pro": {
+      "cash": "USD"
+    },
+    "binance": {
+      "cash": "USDT",
+      "binance_tld": "us"
+    },
+    "alpaca": {
+      "websocket_stream": "iex",
+      "cash": "USD"
+    }
   }
 }
\ No newline at end of file
diff --git a/tests/config/test_data.p b/tests/config/test_data.p
new file mode 100644
index 00000000..3921527f
Binary files /dev/null and b/tests/config/test_data.p differ
diff --git a/tests/exchanges/Coinbase_Pro/test_Coinbase_Pro_Interface.py b/tests/exchanges/Coinbase_Pro/test_Coinbase_Pro_Interface.py
deleted file mode 100644
index 4a3d2411..00000000
--- a/tests/exchanges/Coinbase_Pro/test_Coinbase_Pro_Interface.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""
-    Unit tests for Coinbase Pro.
-    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 <https://www.gnu.org/licenses/>.
-"""
-import Blankly
-import unittest
-
-
-class CoinbaseInterface(unittest.TestCase):
-    @classmethod
-    def setUpClass(cls) -> None:
-        cls.Coinbase_Pro = Blankly.Coinbase_Pro(portfolio_name="Sandbox Portfolio",
-                                                keys_path='./tests/config/keys.json',
-                                                settings_path="./tests/config/settings.json")
-        cls.Coinbase_Pro_Interface = cls.Coinbase_Pro.get_interface()
-
-    def test_get_exchange_type(self):
-        coinbase_pro = self.Coinbase_Pro_Interface.get_exchange_type()
-        self.assertEqual(coinbase_pro, "coinbase_pro")
-
-    # def test_market_order(self):
-    #     cbp_status = self.Coinbase_Pro_Interface.get_paper_trading_status()
-    #
-    #     self.assertTrue(cbp_status)
diff --git a/tests/exchanges/alpaca/alpaca_api_interface_test.py b/tests/exchanges/alpaca/alpaca_api_interface_test.py
deleted file mode 100644
index 0fb5cd05..00000000
--- a/tests/exchanges/alpaca/alpaca_api_interface_test.py
+++ /dev/null
@@ -1,158 +0,0 @@
-from Blankly.auth.Alpaca.auth import alpaca_auth
-from Blankly.interface.currency_factory import InterfaceFactory
-import pytest
-from pytest_mock import MockerFixture
-
-from tests.helpers.comparisons import is_sub_dict
-from pathlib import Path
-
-
-class mock_alpaca_direct_calls:
-    def __init__(self, *args, **kwargs):
-        pass
-
-    def get_account(self):
-        mock_account_response = {
-            "account_blocked": False,
-            "account_number": "010203ABCD",
-            "buying_power": "262113.632",
-            "cash": "1500",
-            "created_at": "2019-06-12T22:47:07.99658Z",
-            "currency": "USD",
-            "daytrade_count": 0,
-            "daytrading_buying_power": "262113.632",
-            "equity": "103820.56",
-            "id": "e6fe16f3-64a4-4921-8928-cadf02f92f98",
-            "initial_margin": "63480.38",
-            "last_equity": "103529.24",
-            "last_maintenance_margin": "38000.832",
-            "long_market_value": "126960.76",
-            "maintenance_margin": "38088.228",
-            "multiplier": "4",
-            "pattern_day_trader": False,
-            "portfolio_value": "103820.56",
-            "regt_buying_power": "80680.36",
-            "short_market_value": "0",
-            "shorting_enabled": True,
-            "sma": "0",
-            "status": "ACTIVE",
-            "trade_suspended_by_user": False,
-            "trading_blocked": False,
-            "transfers_blocked": False
-        }
-        return mock_account_response
-
-    def list_positions(self):
-        mock_positions_response = [{
-            "asset_id": "904837e3-3b76-47ec-b432-046db621571b",
-            "symbol": "AAPL",
-            "exchange": "NASDAQ",
-            "asset_class": "us_equity",
-            "avg_entry_price": "100.0",
-            "qty": "5",
-            "side": "long",
-            "market_value": "600.0",
-            "cost_basis": "500.0",
-            "unrealized_pl": "100.0",
-            "unrealized_plpc": "0.20",
-            "unrealized_intraday_pl": "10.0",
-            "unrealized_intraday_plpc": "0.0084",
-            "current_price": "120.0",
-            "lastday_price": "119.0",
-            "change_today": "0.0084"
-        }]
-        return mock_positions_response
-
-    def list_assets(self, *args, **kwargs):
-        mock_products_response = [{
-            "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
-        }]
-        return mock_products_response
-
-
-class mock_alpaca_interface:
-    def __init__(self, *args, **kwargs):
-        pass
-
-    def get_account(self):
-        return "hello"
-
-
-@pytest.fixture
-def alpaca_mock_interface(mocker: MockerFixture) -> None:
-    keys_file_path = Path("tests/config/keys.json").resolve()
-    settings_file_path = Path("tests/config/settings.json").resolve()
-
-    auth_obj = alpaca_auth(keys_file_path, "alpaca test portfolio")
-    mocker.patch("alpaca_trade_api.REST", new=mock_alpaca_direct_calls)
-    alpaca_interface = InterfaceFactory.create_interface("alpaca", auth_obj, settings_file_path)
-
-    return alpaca_interface
-
-# TODO: Need to create a functional testing package
-# @pytest.fixture
-# def alpaca_interface(mocker: MockerFixture) -> None:
-#     keys_file_path = Path("tests/config/keys.json").resolve()
-#     settings_file_path = Path("tests/config/settings.json").resolve()
-#
-#     auth_obj = alpaca_auth(keys_file_path, "alpaca test portfolio")
-#     alpaca_interface = InterfaceFactory.create_interface("alpaca", auth_obj, settings_file_path)
-#     return alpaca_interface
-#
-#
-# def test_alpaca_interface_functional(alpaca_interface) -> None:
-#
-#     assert alpaca_interface.get_exchange_type() == 'alpaca'
-
-
-def test_get_products(alpaca_mock_interface) -> None:
-    return_val = alpaca_mock_interface.get_products()
-
-    expected_answer = {
-        "currency_id": "904837e3-3b76-47ec-b432-046db621571b",
-        "base_currency": "AAPL",
-        "quote_currency": "usd",
-        "base_min_size": -1,
-        "base_max_size": -1,
-        "base_increment": -1,
-    }
-    for k, v in expected_answer.items():
-        assert return_val[0][k] == v
-
-    assert "exchange_specific" in return_val[0]
-
-
-def test_get_account(alpaca_mock_interface) -> None:
-    return_val = alpaca_mock_interface.get_account()
-
-    expected_answer = [
-        {
-            "currency": "USD",
-            "available": 1500,
-            "hold": -1,
-        },
-        {
-            "currency": "AAPL",
-            "available": 5,
-            "hold": -1,
-        }
-    ]
-
-    for answer in expected_answer:
-        found = False
-        for result in return_val:
-            if is_sub_dict(answer, result):
-                found = True
-
-        assert found, "expected return element not found: %r" % answer
-
-    assert "exchange_specific" in return_val[0]
diff --git a/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py b/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py
new file mode 100644
index 00000000..c937c262
--- /dev/null
+++ b/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py
@@ -0,0 +1,236 @@
+from pathlib import Path
+
+import pytest
+from pytest_mock import MockerFixture
+
+from blankly.exchanges.interfaces.alpaca.alpaca_auth import AlpacaAuth
+from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory
+
+
+class mock_alpaca_direct_calls:
+    def __init__(self, *args, **kwargs):
+        pass
+
+    def get_account(self):
+        mock_account_response = {
+            "account_blocked": False,
+            "account_number": "010203ABCD",
+            "buying_power": "262113.632",
+            "cash": "1500",
+            "created_at": "2019-06-12T22:47:07.99658Z",
+            "currency": "USD",
+            "daytrade_count": 0,
+            "daytrading_buying_power": "262113.632",
+            "equity": "103820.56",
+            "id": "e6fe16f3-64a4-4921-8928-cadf02f92f98",
+            "initial_margin": "63480.38",
+            "last_equity": "103529.24",
+            "last_maintenance_margin": "38000.832",
+            "long_market_value": "126960.76",
+            "maintenance_margin": "38088.228",
+            "multiplier": "4",
+            "pattern_day_trader": False,
+            "portfolio_value": "103820.56",
+            "regt_buying_power": "80680.36",
+            "short_market_value": "0",
+            "shorting_enabled": True,
+            "sma": "0",
+            "status": "ACTIVE",
+            "trade_suspended_by_user": False,
+            "trading_blocked": False,
+            "transfers_blocked": False
+        }
+        return mock_account_response
+
+    def list_orders(self, *args, **kwargs):
+        mock_orders_response = [{
+            "id": "61e69015-8549-4bfd-b9c3-01e75843f47d",
+            "client_order_id": "eb9e2aaa-f71a-4f51-b5b4-52a6c565dad4",
+            "created_at": "2021-03-16T18:38:01.942282Z",
+            "updated_at": "2021-03-16T18:38:01.942282Z",
+            "submitted_at": "2021-03-16T18:38:01.937734Z",
+            "filled_at": None,
+            "expired_at": None,
+            "canceled_at": None,
+            "failed_at": None,
+            "replaced_at": None,
+            "replaced_by": None,
+            "replaces": None,
+            "asset_id": "b0b6dd9d-8b9b-48a9-ba46-b9d54906e415",
+            "symbol": "AAPL",
+            "asset_class": "us_equity",
+            "notional": None,
+            "qty": 1,
+            "filled_qty": "0",
+            "filled_avg_price": None,
+            "order_class": "",
+            "order_type": "market",
+            "type": "market",
+            "side": "sell",
+            "time_in_force": "day",
+            "limit_price": None,
+            "stop_price": None,
+            "status": "accepted",
+            "extended_hours": False,
+            "legs": None,
+            "trail_percent": None,
+            "trail_price": None,
+            "hwm": None
+        }]
+
+        return mock_orders_response
+
+    def get_snapshots(self, symbols):
+        mock_snapshots = [{
+            "symbol": "AAPL",
+            "latestTrade": {
+                "t": "2021-05-11T20:00:00.435997104Z",
+                "x": "Q",
+                "p": 125.91,
+                "s": 5589631,
+                "c": [
+                    "@",
+                    "M"
+                ],
+                "i": 179430,
+                "z": "C"
+            },
+            "latestQuote": {
+                "t": "2021-05-11T22:05:02.307304704Z",
+                "ax": "P",
+                "ap": 125.68,
+                "as": 12,
+                "bx": "P",
+                "bp": 125.6,
+                "bs": 4,
+                "c": [
+                    "R"
+                ]
+            },
+            "minuteBar": {
+                "t": "2021-05-11T22:02:00Z",
+                "o": 125.66,
+                "h": 125.66,
+                "l": 125.66,
+                "c": 125.66,
+                "v": 396
+            },
+            "dailyBar": {
+                "t": "2021-05-11T04:00:00Z",
+                "o": 123.5,
+                "h": 126.27,
+                "l": 122.77,
+                "c": 125.91,
+                "v": 125863164
+            },
+            "prevDailyBar": {
+                "t": "2021-05-10T04:00:00Z",
+                "o": 129.41,
+                "h": 129.54,
+                "l": 126.81,
+                "c": 126.85,
+                "v": 79569305
+            }
+        }]
+
+        return mock_snapshots
+
+    def list_positions(self):
+        mock_positions_response = [{
+            "asset_id": "904837e3-3b76-47ec-b432-046db621571b",
+            "symbol": "AAPL",
+            "exchange": "NASDAQ",
+            "asset_class": "us_equity",
+            "avg_entry_price": "100.0",
+            "qty": "5",
+            "side": "long",
+            "market_value": "600.0",
+            "cost_basis": "500.0",
+            "unrealized_pl": "100.0",
+            "unrealized_plpc": "0.20",
+            "unrealized_intraday_pl": "10.0",
+            "unrealized_intraday_plpc": "0.0084",
+            "current_price": "120.0",
+            "lastday_price": "119.0",
+            "change_today": "0.0084"
+        }]
+        return mock_positions_response
+
+    def list_assets(self, *args, **kwargs):
+        mock_products_response = [{
+            "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
+        }]
+        return mock_products_response
+
+
+class mock_alpaca_interface:
+    def __init__(self, *args, **kwargs):
+        pass
+
+    def get_account(self):
+        return "hello"
+
+
+@pytest.fixture
+def alpaca_mock_interface(mocker: MockerFixture) -> None:
+    keys_file_path = Path("tests/config/keys.json").resolve()
+    settings_file_path = Path("tests/config/settings.json").resolve()
+
+    auth_obj = AlpacaAuth(keys_file_path, "alpaca test portfolio")
+    mocker.patch("alpaca_trade_api.REST", new=mock_alpaca_direct_calls)
+    _, alpaca_interface = DirectCallsFactory.create("alpaca", auth_obj, settings_file_path)
+
+    return alpaca_interface
+
+
+# TODO: Need to create a functional testing package
+# @pytest.fixture
+# def alpaca_interface(mocker: MockerFixture) -> None:
+#     keys_file_path = Path("tests/config/keys.json").resolve()
+#     settings_file_path = Path("tests/config/settings.json").resolve()
+#
+#     auth_obj = alpaca_auth(keys_file_path, "alpaca test portfolio")
+#     alpaca_interface = DirectCallsFactory.create_interface("alpaca", auth_obj, settings_file_path)
+#     return alpaca_interface
+#
+#
+# def test_alpaca_interface_functional(alpaca_interface) -> None:
+#
+#     assert alpaca_interface.get_exchange_type() == 'alpaca'
+
+
+# TODO: make this test better
+def test_get_account(alpaca_mock_interface) -> None:
+    return_val = alpaca_mock_interface.get_account()
+
+    expected_answer = {
+        "AAPL": {
+            "available": 4,
+            "hold": 1,
+        }
+    }
+
+    assert 'AAPL' in return_val
+    assert return_val['AAPL']['available'] == 4
+    assert return_val['AAPL']['hold'] == 1
+
+
+def test_get_cash(alpaca_mock_interface) -> None:
+    expected_answer = 262113.632
+    return_val = alpaca_mock_interface.cash
+    assert return_val == expected_answer
+
+
+def test_get_fees(alpaca_mock_interface) -> None:
+    fee_response = alpaca_mock_interface.get_fees()
+    assert fee_response['maker_fee_rate'] == 0
+    assert fee_response['taker_fee_rate'] == 0
diff --git a/tests/exchanges/interfaces/alpaca/test_alpaca_interface_functional.py b/tests/exchanges/interfaces/alpaca/test_alpaca_interface_functional.py
new file mode 100644
index 00000000..0578558d
--- /dev/null
+++ b/tests/exchanges/interfaces/alpaca/test_alpaca_interface_functional.py
@@ -0,0 +1,141 @@
+import datetime
+import time
+from datetime import datetime as dt
+from pathlib import Path
+
+import alpaca_trade_api
+import dateparser
+import dateparser as dp
+import pytest
+import pytz
+
+from blankly.exchanges.interfaces.alpaca.alpaca_auth import AlpacaAuth
+from blankly.exchanges.interfaces.alpaca.alpaca_interface import AlpacaInterface
+from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory
+
+timeZ_Ny = pytz.timezone('America/New_York')
+MARKET_OPEN = datetime.time(hour=9, minute=0, second=0, tzinfo=timeZ_Ny)
+MARKET_CLOSE = datetime.time(hour=17, minute=0, second=0, tzinfo=timeZ_Ny)
+
+
+@pytest.fixture
+def alpaca_interface():
+    keys_file_path = Path("tests/config/keys.json").resolve()
+    settings_file_path = Path("tests/config/settings.json").resolve()
+
+    auth_obj = AlpacaAuth(str(keys_file_path), "alpaca test portfolio")
+    _, alpaca_interface = DirectCallsFactory.create("alpaca", auth_obj, str(settings_file_path))
+    return alpaca_interface
+
+
+def test_get_exchange(alpaca_interface: AlpacaInterface) -> None:
+    assert alpaca_interface.get_exchange_type() == 'alpaca'
+
+
+# TODO: uncomment and fix this when we get the function to work
+# def test_get_products(alpaca_interface: AlpacaInterface) -> None:
+#     return_val = alpaca_interface.get_products()
+#
+#     expected_answer = {
+#         "symbol": "AAPL-USD",
+#         "base_asset": "AAPL",
+#         "quote_asset": "USD",
+#         "base_min_size": -1,
+#         "base_max_size": -1,
+#         "base_increment": -1,
+#     }
+#     for k, v in expected_answer.items():
+#         assert return_val[0][k] == v
+#
+#     assert "exchange_specific" in return_val[0]
+
+def test_get_buy_sell(alpaca_interface: AlpacaInterface) -> None:
+    if not alpaca_interface.get_calls().get_clock()['is_open']:
+        return
+
+    known_apple_info = {
+        "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
+    }
+
+    # query for the unique ID of AAPL
+    products = alpaca_interface.get_products()
+    apple_id = None
+
+    for product in products:
+        if product['base_asset'] == 'AAPL':
+            apple_id = product['exchange_specific']['id']
+
+    assert apple_id
+
+    market_order = alpaca_interface.market_order(apple_id, 'buy', 200)
+
+    num_retries = 0
+    # wait until order is filled
+    while alpaca_interface.get_order('AAPL', market_order.get_id())['funds'] is None:
+        time.sleep(1)
+        num_retries += 1
+        if num_retries > 10:
+            raise TimeoutError("Too many retries, cannot get order status")
+
+    resp = alpaca_interface.get_order('AAPL', market_order.get_id())
+
+    # get_position(symbol)
+
+    # get_asset(symbol)
+
+    # place sell order
+
+
+def test_get_product_history(alpaca_interface: AlpacaInterface) -> None:
+    start = dt.strptime("2021-02-04", "%Y-%m-%d").timestamp()
+    end = dt.strptime("2021-02-05", "%Y-%m-%d").timestamp()
+
+    return_df = alpaca_interface.get_product_history("AAPL", start, end, 60)
+    return_df_2 = alpaca_interface.get_product_history("AAPL", start, end, 120)
+    assert len(return_df) > 0
+    assert len(return_df) >= len(return_df_2)
+
+
+def test_get_product_history_est_timezone(alpaca_interface: AlpacaInterface) -> None:
+    start = dateparser.parse("2021-02-04 9:30AM EST").timestamp()
+    end = dateparser.parse("2021-02-04 9:35AM EST").timestamp()
+
+    return_df = alpaca_interface.get_product_history("AAPL", start, end, 60)
+    desired_timestamp = dp.parse("2021-02-04 14:30:00+00:00").timestamp()
+    assert return_df.iloc[0]['time'] == desired_timestamp
+    assert (len(return_df) == 6)
+
+
+def test_get_product_history_custom(alpaca_interface: AlpacaInterface) -> None:
+    assert isinstance(alpaca_interface.calls, alpaca_trade_api.REST)
+    end = dateparser.parse("2021-02-04 9:35AM EST")
+
+    return_df = alpaca_interface.history("AAPL", to=10, resolution='1h', end_date=end)
+    assert (len(return_df) == 10)
+
+
+def test_get_account(alpaca_interface: AlpacaInterface) -> None:
+    products = alpaca_interface.get_account()
+    for _, val in products.items():
+        assert 'available' in val
+        assert 'hold' in val
+
+    assert "USD" in products
+
+
+def test_get_price(alpaca_interface: AlpacaInterface) -> None:
+    price = alpaca_interface.get_price("AAPL")
+    assert isinstance(price, float)
+
+def test_get_open_orders(alpaca_interface: AlpacaInterface) -> None:
+    orders = alpaca_interface.get_open_orders()
+    print(orders)
diff --git a/tests/exchanges/interfaces/binance/test_binance_interface.py b/tests/exchanges/interfaces/binance/test_binance_interface.py
new file mode 100644
index 00000000..b316ffb8
--- /dev/null
+++ b/tests/exchanges/interfaces/binance/test_binance_interface.py
@@ -0,0 +1,122 @@
+"""
+    Unit tests for binance.
+    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 <https://www.gnu.org/licenses/>.
+"""
+import time
+import unittest
+from pathlib import Path
+
+import pytest
+
+import blankly
+from blankly.exchanges.interfaces.binance.binance_auth import BinanceAuth
+from blankly.exchanges.interfaces.binance.binance_interface import BinanceInterface
+from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory
+from blankly.utils.utils import AttributeDict
+
+
+class BinanceInterface_test(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls.Binance = blankly.Binance(portfolio_name="Spot Test Key",
+                                      keys_path="./tests/config/keys.json",
+                                      settings_path="./tests/config/settings.json")
+        cls.Binance_Interface = cls.Binance.get_interface()
+
+    def test_get_exchange_type(self):
+        binance = self.Binance_Interface.get_exchange_type()
+        self.assertEqual(binance, "binance")
+
+
+@pytest.fixture
+def binance_interface():
+    keys_file_path = Path("tests/config/keys.json").resolve()
+    settings_file_path = Path("tests/config/settings.json").resolve()
+
+    auth_obj = BinanceAuth(str(keys_file_path), "Spot Test Key")
+    _, binance_interface = DirectCallsFactory.create("binance", auth_obj, str(settings_file_path))
+    return binance_interface
+
+
+def test_get_exchange(binance_interface: BinanceInterface) -> None:
+    assert binance_interface.get_exchange_type() == 'binance'
+
+
+def test_get_account(binance_interface: BinanceInterface) -> None:
+    resp = binance_interface.get_account()
+    assert type(resp) is AttributeDict
+
+    found_btc = False
+    for asset in resp.keys():
+        if asset == "BTC":
+            resp['BTC']['available'] += 1
+            resp['BTC']['hold'] += 1
+
+            found_btc = True
+
+    assert found_btc
+
+
+def test_get_buy_sell_order(binance_interface: BinanceInterface) -> None:
+    # query for the unique ID of BTC-USD
+    products = binance_interface.get_products()
+    btc_usd_id = None
+
+    for i in products:
+        if i['symbol'] == 'BTC-USDT':
+            btc_usd_id = i['symbol']
+            break
+    print(btc_usd_id)
+    assert btc_usd_id
+
+    market_buy_order = binance_interface.market_order(btc_usd_id, 'buy', 200)
+
+    time.sleep(1.5)
+
+    num_retries = 0
+    # wait until order is filled
+    while binance_interface.get_order(btc_usd_id, market_buy_order.get_id())['status'] != "filled":
+        time.sleep(1)
+        num_retries += 1
+        if num_retries > 10:
+            raise TimeoutError("Too many retries, cannot get order status")
+
+    resp = binance_interface.get_order(btc_usd_id, market_buy_order.get_id())
+    assert type(resp) is dict
+    assert resp["side"] == "buy"
+    assert resp["type"] == "market"
+
+    market_sell_order = binance_interface.market_order(btc_usd_id, 'sell', 200)
+    # this test has been flaky, so lets add a sleep for engine to process order
+    time.sleep(1)
+    # wait until order is filled
+    while binance_interface.get_order(btc_usd_id, market_sell_order.get_id())['status'] != "filled":
+        time.sleep(1)
+        num_retries += 1
+        if num_retries > 10:
+            raise TimeoutError("Too many retries, cannot get order status")
+
+    resp = binance_interface.get_order(btc_usd_id, market_sell_order.get_id())
+    assert type(resp) is dict
+    assert resp["side"] == "sell"
+    assert resp["type"] == "market"
+
+
+def test_get_price(binance_interface: BinanceInterface) -> None:
+    # todo: just call a couple of popular coins and see if they all return floats
+    btcusd = 'BTC-USDT'
+    resp = binance_interface.get_price(btcusd)
+    assert type(resp) is float
\ No newline at end of file
diff --git a/tests/exchanges/interfaces/coinbase_pro/test_coinbase_pro_interface.py b/tests/exchanges/interfaces/coinbase_pro/test_coinbase_pro_interface.py
new file mode 100644
index 00000000..e5ef7a28
--- /dev/null
+++ b/tests/exchanges/interfaces/coinbase_pro/test_coinbase_pro_interface.py
@@ -0,0 +1,94 @@
+"""
+    Unit tests for Coinbase Pro.
+    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 <https://www.gnu.org/licenses/>.
+"""
+import unittest
+from datetime import datetime as dt
+from pathlib import Path
+
+import dateparser
+import pytest
+
+import blankly
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_auth import CoinbaseProAuth
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_interface import CoinbaseProInterface
+from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory
+
+
+class CoinbaseInterface2(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        cls.Coinbase_Pro = blankly.CoinbasePro(portfolio_name="Sandbox Portfolio",
+                                               keys_path='./tests/config/keys.json',
+                                               settings_path="./tests/config/settings.json")
+        cls.Coinbase_Pro_Interface = cls.Coinbase_Pro.get_interface()
+
+    def test_get_exchange_type(self):
+        coinbase_pro = self.Coinbase_Pro_Interface.get_exchange_type()
+        self.assertEqual(coinbase_pro, "coinbase_pro")
+
+
+@pytest.fixture
+def coinbase_interface():
+    keys_file_path = Path("tests/config/keys.json").resolve()
+    settings_file_path = Path("tests/config/settings.json").resolve()
+
+    auth_obj = CoinbaseProAuth(str(keys_file_path), "Sandbox Portfolio")
+    _, coinbase_interface = DirectCallsFactory.create("coinbase_pro", auth_obj, str(settings_file_path))
+    return coinbase_interface
+
+
+def test_get_exchange(coinbase_interface: CoinbaseProInterface) -> None:
+    assert coinbase_interface.get_exchange_type() == 'coinbase_pro'
+
+    products = coinbase_interface.get_products()
+    btc_usd_id = None
+
+    for product in products:
+        if product['base_asset'] == 'BTC' and product['quote_asset'] == 'USD':
+            btc_usd_id = product['symbol']
+
+    assert btc_usd_id
+
+    # market_buy_order = coinbase_interface.market_order(btc_usd_id, 'buy', 200)
+
+
+def test_get_price(coinbase_interface: CoinbaseProInterface) -> None:
+    responses = []
+    responses.append(coinbase_interface.get_price('BTC-USD'))
+    responses.append(coinbase_interface.get_price('BTC-EUR'))
+    responses.append(coinbase_interface.get_price('BTC-GBP'))
+    responses.append(coinbase_interface.get_price('ETH-BTC'))
+    for resp in responses:
+        assert type(resp) is float
+
+
+def test_start_with_end_history(coinbase_interface: CoinbaseProInterface) -> None:
+    # This initial selection could fail because of the slightly random day that they delete their data
+    start_dt = dateparser.parse("2021-07-15")
+    start = str(start_dt.replace(day=1).date())
+    stop = str(start_dt.date())
+
+    # The dates are offset by one because the time is the open time
+    close_stop = str(dt.today().replace(day=start_dt.day - 1).date())
+
+    resp = coinbase_interface.history('BTC-USD', resolution='1h', start_date=start, end_date=stop)
+
+    start_date = dt.fromtimestamp(resp['time'][0]).strftime('%Y-%m-%d')
+    end_date = dt.fromtimestamp(resp['time'].iloc[-1]).strftime('%Y-%m-%d')
+
+    assert start_date == start
+    assert end_date == close_stop
diff --git a/tests/exchanges/test_interface_homogeneity.py b/tests/exchanges/test_interface_homogeneity.py
index d95430f9..b25fa356 100644
--- a/tests/exchanges/test_interface_homogeneity.py
+++ b/tests/exchanges/test_interface_homogeneity.py
@@ -16,22 +16,28 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-
-import Blankly
-from Blankly.utils.utils import compare_dictionaries
-import unittest
 import time
+import unittest
+from datetime import datetime as dt
 
-from Blankly.utils.purchases.market_order import MarketOrder
+import dateparser
+import numpy
+import pandas as pd
 
+import blankly
+from blankly.exchanges.orders.limit_order import LimitOrder
+from blankly.exchanges.orders.market_order import MarketOrder
+from blankly.utils.time_builder import build_hour
+from blankly.utils.utils import compare_dictionaries
 
-def compare_responses(response_list):
+
+def compare_responses(response_list, force_exchange_specific=True):
     """
     Compare a set of responses against the others. This supports a large set of interfaces
     """
-    for i in range(len(response_list)-1):
-        if not compare_dictionaries(response_list[i], response_list[i+1]):
-            print("Failed checking index " + str(i+1) + " against index " + str(i))
+    for i in range(len(response_list) - 1):
+        if not compare_dictionaries(response_list[i], response_list[i + 1], force_exchange_specific):
+            print("Failed checking index " + str(i + 1) + " against index " + str(i))
             return False
     return True
 
@@ -40,36 +46,54 @@ class InterfaceHomogeneity(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls) -> None:
-        cls.Interfaces = []
+        cls.interfaces = []
 
-        cls.Coinbase_Pro = Blankly.Coinbase_Pro(portfolio_name="Sandbox Portfolio",
-                                                keys_path='./tests/config/keys.json',
-                                                settings_path="./tests/config/settings.json")
+        # Coinbase Pro definition and appending
+        cls.Coinbase_Pro = blankly.CoinbasePro(portfolio_name="Sandbox Portfolio",
+                                               keys_path='./tests/config/keys.json',
+                                               settings_path="./tests/config/settings.json")
         cls.Coinbase_Pro_Interface = cls.Coinbase_Pro.get_interface()
-        cls.Interfaces.append(cls.Coinbase_Pro_Interface)
+        cls.interfaces.append(cls.Coinbase_Pro_Interface)
 
-        cls.Binance = Blankly.Binance(portfolio_name="Spot Test Key",
+        # binance definition and appending
+        cls.Binance = blankly.Binance(portfolio_name="Spot Test Key",
                                       keys_path='./tests/config/keys.json',
                                       settings_path="./tests/config/settings.json")
         cls.Binance_Interface = cls.Binance.get_interface()
-        cls.Interfaces.append(cls.Binance_Interface)
+        cls.interfaces.append(cls.Binance_Interface)
+
+        # alpaca definition and appending
+        cls.alpaca = blankly.Alpaca(portfolio_name="alpaca test portfolio",
+                                    keys_path='./tests/config/keys.json',
+                                    settings_path="./tests/config/settings.json")
+        cls.Alpaca_Interface = cls.alpaca.get_interface()
+        cls.interfaces.append(cls.Alpaca_Interface)
 
-        cls.Paper_Trade = Blankly.PaperTrade(cls.Binance)
-        cls.Paper_Trade_Interface = cls.Paper_Trade.get_interface()
-        cls.Interfaces.append(cls.Paper_Trade_Interface)
+        # Paper trade wraps binance
+        cls.paper_trade_binance = blankly.PaperTrade(cls.Binance)
+        cls.paper_trade_binance_interface = cls.paper_trade_binance.get_interface()
+        cls.interfaces.append(cls.paper_trade_binance_interface)
+
+        # Another wraps coinbase pro
+        cls.paper_trade_coinbase_pro = blankly.PaperTrade(cls.Coinbase_Pro)
+        cls.paper_trade_coinbase_pro_interface = cls.paper_trade_coinbase_pro.get_interface()
+        cls.interfaces.append(cls.paper_trade_coinbase_pro_interface)
 
     def test_get_products(self):
         responses = []
-        for i in range(len(self.Interfaces)):
-            responses.append(self.Interfaces[i].get_products()[0])
+        for i in range(len(self.interfaces)):
+            responses.append(self.interfaces[i].get_products()[0])
 
-        self.assertTrue(compare_responses(responses))
+        self.assertTrue(compare_responses(responses, force_exchange_specific=False))
 
     def test_get_account(self):
         responses = []
-        for i in range(len(self.Interfaces)):
-            responses.append(self.Interfaces[i].get_account()[0])
-        self.assertTrue(compare_responses(responses))
+        for i in range(len(self.interfaces)):
+            if self.interfaces[i].get_exchange_type() == "alpaca":
+                responses.append(self.interfaces[i].get_account()['AAPL'])
+            else:
+                responses.append(self.interfaces[i].get_account()['BTC'])
+        self.assertTrue(compare_responses(responses, force_exchange_specific=False))
 
     def check_market_order(self, order1: MarketOrder, side, funds):
         """
@@ -85,26 +109,25 @@ def check_market_order(self, order1: MarketOrder, side, funds):
 
     def test_market_order(self):
         # Make sure to buy back the funds we're loosing from fees - minimum balance of .1 bitcoin
-        btc_account = self.Binance_Interface.get_account(currency="BTC")['available']
+        btc_account = self.Binance_Interface.get_account(symbol="BTC")['available']
         if btc_account < .1:
             price = self.Binance_Interface.get_price("BTC-USDT")
             self.Binance_Interface.market_order("BTC-USDT", "buy", price * .1)
 
-        binance_buy = self.Binance_Interface.market_order('BTC-USDT', 'buy', 20)
-        binance_sell = self.Binance_Interface.market_order('BTC-USDT', 'sell', 20)
+        binance_buy = self.Binance_Interface.market_order('BTC-USDT', 'buy', 25)
+        binance_sell = self.Binance_Interface.market_order('BTC-USDT', 'sell', 25)
 
-        self.check_market_order(binance_buy, 'buy', 20)
-        self.check_market_order(binance_sell, 'sell', 20)
+        self.check_market_order(binance_buy, 'buy', 25)
+        self.check_market_order(binance_sell, 'sell', 25)
 
         self.assertTrue(compare_dictionaries(binance_buy.get_response(), binance_sell.get_response()))
+        time.sleep(.5)
         self.assertTrue(compare_dictionaries(binance_buy.get_status(full=True), binance_sell.get_status(full=True)))
 
         coinbase_buy = self.Coinbase_Pro_Interface.market_order('BTC-USD', 'buy', 20)
         coinbase_sell = self.Coinbase_Pro_Interface.market_order('BTC-USD', 'sell', 20)
 
         self.assertTrue(compare_dictionaries(coinbase_buy.get_response(), coinbase_sell.get_response()))
-        print(coinbase_buy.get_status(full=True))
-        print(coinbase_sell.get_status(full=True))
         self.assertTrue(compare_dictionaries(coinbase_buy.get_status(full=True), coinbase_sell.get_status(full=True)))
 
         response_list = [coinbase_buy.get_response(),
@@ -124,3 +147,244 @@ def test_market_order(self):
         self.assertTrue(compare_responses(response_list))
 
         self.assertTrue(compare_responses(status_list))
+
+    def check_limit_order(self, limit_order: LimitOrder, expected_side: str, size, product_id):
+        self.assertEqual(limit_order.get_side(), expected_side)
+        self.assertEqual(limit_order.get_type(), 'limit')
+        self.assertEqual(limit_order.get_time_in_force(), 'GTC')
+        # TODO fix status homogeneity
+        # self.assertEqual(limit_order.get_status(), {'status': 'new'})
+        self.assertEqual(limit_order.get_quantity(), size)
+        self.assertEqual(limit_order.get_asset_id(), product_id)
+
+    def test_limit_order(self):
+        """
+        This function tests a few components of market orders:
+        - Opening market orders
+        - Monitoring market orders using the order status function
+        - Comparing with open orders
+        - Canceling orders
+        """
+        binance_limits = self.Binance_Interface.get_order_filter('BTC-USDT')["limit_order"]
+
+        binance_buy = self.Binance_Interface.limit_order('BTC-USDT', 'buy', int(binance_limits['min_price']+30), .01)
+        time.sleep(3)
+        self.check_limit_order(binance_buy, 'buy', .01, 'BTC-USDT')
+
+        coinbase_buy = self.Coinbase_Pro_Interface.limit_order('BTC-USD', 'buy', .01, .001)
+        self.check_limit_order(coinbase_buy, 'buy', .001, 'BTC-USD')
+
+        binance_sell = self.Binance_Interface.limit_order('BTC-USDT', 'sell', int(binance_limits['max_price']-30), .01)
+        self.check_limit_order(binance_sell, 'sell', .01, 'BTC-USDT')
+
+        coinbase_sell = self.Coinbase_Pro_Interface.limit_order('BTC-USD', 'sell', 100000, .001)
+        self.check_limit_order(coinbase_sell, 'sell', .001, 'BTC-USD')
+
+        limits = [binance_buy, coinbase_buy, binance_sell, coinbase_sell]
+        responses = []
+        status = []
+
+        cancels = []
+
+        coinbase_open = self.Coinbase_Pro_Interface.get_open_orders('BTC-USD')
+        for i in [coinbase_buy, coinbase_sell]:
+            found = False
+            for j in coinbase_open:
+                if i.get_id() == j['id']:
+                    found = True
+            self.assertTrue(found)
+
+        binance_open = self.Binance_Interface.get_open_orders('BTC-USDT')
+        for i in [binance_buy, binance_sell]:
+            found = False
+            for j in binance_open:
+                if i.get_id() == j['id']:
+                    found = True
+                    compare_dictionaries(i.get_response(), j)
+            self.assertTrue(found)
+
+        for i in limits:
+            responses.append(i.get_response())
+            status.append(i.get_status(full=True))
+
+        self.assertTrue(compare_responses(responses))
+        self.assertTrue(compare_responses(status))
+
+        cancels.append(self.Binance_Interface.cancel_order('BTC-USDT', binance_buy.get_id()))
+        cancels.append(self.Binance_Interface.cancel_order('BTC-USDT', binance_sell.get_id()))
+
+        cancels.append(self.Coinbase_Pro_Interface.cancel_order('BTC-USD', coinbase_sell.get_id()))
+        cancels.append(self.Coinbase_Pro_Interface.cancel_order('BTC-USD', coinbase_buy.get_id()))
+
+        self.assertTrue(compare_responses(cancels, force_exchange_specific=False))
+
+    def test_get_keys(self):
+        responses = []
+        for i in self.interfaces:
+            responses.append(i.get_fees())
+
+        self.assertTrue(compare_responses(responses, force_exchange_specific=False))
+
+    def check_product_history_types(self, df: pd.DataFrame):
+        self.assertTrue(isinstance(df['time'][0], numpy.int64))
+        self.assertTrue(isinstance(df['low'][0], float))
+        self.assertTrue(isinstance(df['high'][0], float))
+        self.assertTrue(isinstance(df['open'][0], float))
+        self.assertTrue(isinstance(df['close'][0], float))
+        self.assertTrue(isinstance(df['volume'][0], float))
+
+    def check_product_history_columns(self, df: pd.DataFrame):
+        self.assertTrue(isinstance(df['time'], pd.Series))
+        self.assertTrue(isinstance(df['low'], pd.Series))
+        self.assertTrue(isinstance(df['high'], pd.Series))
+        self.assertTrue(isinstance(df['open'], pd.Series))
+        self.assertTrue(isinstance(df['close'], pd.Series))
+        self.assertTrue(isinstance(df['volume'], pd.Series))
+    
+    def test_single_point_history(self):
+        responses = []
+        for i in self.interfaces:
+            if i.get_exchange_type() == "binance":
+                responses.append(i.history('BTC-USDT', 1))
+            elif i.get_exchange_type() == "alpaca":
+                responses.append(i.history('MSFT', 1))
+            else:
+                responses.append(i.history('BTC-USD', 1))
+
+        for i in responses:
+            self.check_product_history_columns(i)
+
+            self.assertEqual(len(i), 1)
+
+            self.check_product_history_types(i)
+
+    def test_point_based_history(self):
+        responses = []
+        for i in self.interfaces:
+            if i.get_exchange_type() == "binance":
+                responses.append(i.history('BTC-USDT', 150, resolution='1m'))
+            elif i.get_exchange_type() == "alpaca":
+                responses.append(i.history('MSFT', 150, resolution='1m'))
+            else:
+                responses.append(i.history('BTC-USD', 150, resolution='1m'))
+        for i in responses:
+            self.check_product_history_columns(i)
+
+            self.assertEqual(150, len(i))
+
+            self.check_product_history_types(i)
+
+    def test_point_with_end_history(self):
+        responses = []
+
+        today = dateparser.parse("2021-07-09 9:35AM")
+
+        # This won't work at the start of the month
+        end_date = dt.today().replace(day=today.day-2)
+        close_stop = str(dt.today().replace(day=today.day-3).date())
+
+        expected_hours = end_date.day * 24 - (24*2)
+
+        end_date_str = str(end_date.date())
+
+        for i in self.interfaces:
+            if i.get_exchange_type() == "binance":
+                responses.append((i.history('BTC-USDT', to=expected_hours, resolution='1h', end_date=end_date_str), 'binance'))
+            elif i.get_exchange_type() == "alpaca":
+                responses.append((i.history('MSFT', to=expected_hours, resolution='1h', end_date=end_date_str), 'alpaca'))
+            else:
+                responses.append((i.history('BTC-USD', to=expected_hours, resolution='1h', end_date=end_date_str), 'coinbase_pro'))
+        for i in responses:
+            self.check_product_history_columns(i[0])
+
+            last_date = dt.fromtimestamp(i[0]['time'].iloc[-1]).strftime('%Y-%m-%d')
+            self.assertEqual(close_stop, last_date)
+
+            self.check_product_history_types(i[0])
+
+    def test_start_with_end_history(self):
+        responses = []
+
+        # This initial selection could fail because of the slightly random day that they delete their data
+        start_dt = dateparser.parse("2021-07-15")
+        start = str(start_dt.replace(day=1).date())
+        stop = str(start_dt.date())
+
+        # The dates are offset by one because the time is the open time
+        close_stop = str(dt.today().replace(day=start_dt.day-1).date())
+
+        for i in self.interfaces:
+            if i.get_exchange_type() == "binance":
+                responses.append(i.history('BTC-USDT', resolution='1h', start_date=start, end_date=stop))
+            elif i.get_exchange_type() == "alpaca":
+                responses.append(i.history('MSFT', resolution='1h', start_date=start, end_date=stop))
+            else:
+                responses.append(i.history('BTC-USD', resolution='1h', start_date=start, end_date=stop))
+
+        for idx, resp in enumerate(responses):
+            start_date = dt.fromtimestamp(resp['time'][0]).strftime('%Y-%m-%d')
+            end_date = dt.fromtimestamp(resp['time'].iloc[-1]).strftime('%Y-%m-%d')
+
+            print("Homogeneity test started on interface: " + self.interfaces[idx].get_exchange_type())
+            self.check_product_history_columns(resp)
+            self.assertEqual(start_date, start)
+            self.assertEqual(end_date, close_stop)
+
+    def test_get_product_history(self):
+        # Setting for number of hours to test backwards to
+        test_intervals = 100
+
+        current_time = time.time()
+        current_date = dt.fromtimestamp(current_time).strftime('%Y-%m-%d')
+        intervals_ago = time.time() - (build_hour() * test_intervals)
+        intervals_ago_date = dt.fromtimestamp(intervals_ago).strftime('%Y-%m-%d')
+
+        responses = []
+        for i in self.interfaces:
+            if i.get_exchange_type() == "binance":
+                responses.append(i.get_product_history('BTC-USDT', intervals_ago, current_time, 3600))
+            elif i.get_exchange_type() == "alpaca":
+                # responses.append(i.get_product_history('MSFT', intervals_ago, current_time, 3600))
+                pass
+            else:
+                responses.append(i.get_product_history('BTC-USD', intervals_ago, current_time, 3600))
+
+        for i in responses:
+            self.check_product_history_columns(i)
+
+            self.assertEqual(len(i), test_intervals)
+            start_date = dt.fromtimestamp(i['time'][0]).strftime('%Y-%m-%d')
+            end_date = dt.fromtimestamp(i['time'].iloc[-1]).strftime('%Y-%m-%d')
+
+            self.assertEqual(start_date, intervals_ago_date)
+            self.assertEqual(end_date, current_date)
+
+            self.check_product_history_types(i)
+
+    def test_get_order_filter(self):
+        responses = []
+
+        for i in self.interfaces:
+            if i.get_exchange_type() == "binance":
+                responses.append(i.get_order_filter('BTC-USDT'))
+            elif i.get_exchange_type() == "alpaca":
+                responses.append(i.get_order_filter('MSFT'))
+            else:
+                responses.append(i.get_order_filter('BTC-USD'))
+
+
+        self.assertTrue(compare_responses(responses))
+
+    def test_get_price(self):
+        responses = []
+
+        for i in self.interfaces:
+            if i.get_exchange_type() == "binance":
+                responses.append(i.get_price('BTC-USDT'))
+            elif i.get_exchange_type() == "alpaca":
+                responses.append(i.get_price('MSFT'))
+            else:
+                responses.append(i.get_price('BTC-USD'))
+
+        for i in responses:
+            self.assertTrue(isinstance(i, float))
diff --git a/tests/indicators/test_moving_averages.py b/tests/indicators/test_moving_averages.py
new file mode 100644
index 00000000..0fe7d718
--- /dev/null
+++ b/tests/indicators/test_moving_averages.py
@@ -0,0 +1,81 @@
+import pickle
+import unittest
+from pathlib import Path
+
+import numpy as np
+
+from blankly.indicators import ema, hma, kaufman_adaptive_ma, macd, sma, trima, vwma, wma, zlema
+
+
+def compare_equal(a, b):
+    # compares two numpy arrays
+    return np.array_equal(a, b)
+
+
+class MovingAverages(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        data_path = Path("tests/config/test_data.p").resolve()
+        with open(data_path, 'rb') as f:
+            cls.data = pickle.load(f)
+        
+    def test_one_param_moving_averages(self):
+        period = self.data['periods'][0]
+
+        ema_res = ema(self.data['close'], period)
+        self.assertTrue(compare_equal(ema_res, self.data[period]['ema']))
+
+        wma_res = wma(self.data['close'], period)
+        self.assertTrue(compare_equal(wma_res, self.data[period]['wma']))
+
+        zlema_res = zlema(self.data['close'], period)
+        self.assertTrue(compare_equal(zlema_res, self.data[period]['zlema']))
+
+        sma_res = sma(self.data['close'], period)
+        self.assertTrue(compare_equal(sma_res, self.data[period]['sma']))
+
+        hma_res = hma(self.data['close'], period)
+        self.assertTrue(compare_equal(hma_res, self.data[period]['hma']))
+
+        kaufman_res = kaufman_adaptive_ma(self.data['close'], period)
+        self.assertTrue(compare_equal(kaufman_res, self.data[period]['kaufman_adaptive_ma']))
+
+        trima_res = trima(self.data['close'], period)
+        self.assertTrue(compare_equal(trima_res, self.data[period]['trima']))
+
+    def test_one_param_moving_averages_diff_period(self):
+        period = self.data['periods'][1]
+
+        ema_res = ema(self.data['close'], period)
+        self.assertTrue(compare_equal(ema_res, self.data[period]['ema']))
+
+        wma_res = wma(self.data['close'], period)
+        self.assertTrue(compare_equal(wma_res, self.data[period]['wma']))
+
+        zlema_res = zlema(self.data['close'], period)
+        self.assertTrue(compare_equal(zlema_res, self.data[period]['zlema']))
+
+        sma_res = sma(self.data['close'], period)
+        self.assertTrue(compare_equal(sma_res, self.data[period]['sma']))
+
+        hma_res = hma(self.data['close'], period)
+        self.assertTrue(compare_equal(hma_res, self.data[period]['hma']))
+
+        kaufman_res = kaufman_adaptive_ma(self.data['close'], period)
+        self.assertTrue(compare_equal(kaufman_res, self.data[period]['kaufman_adaptive_ma']))
+
+        trima_res = trima(self.data['close'], period)
+        self.assertTrue(compare_equal(trima_res, self.data[period]['trima']))
+
+    def test_macd(self):
+        short_period = self.data['short_period']
+        long_period = self.data['long_period']
+        for period in self.data['periods']:
+            macd_res = macd(self.data['close'], short_period, long_period, period)
+            self.assertTrue(compare_equal(macd_res, self.data[period]['macd']))
+
+    def tests_vwma(self):
+        volume = self.data['volume']
+        for period in self.data['periods']:
+            vwma_res = vwma(self.data['close'], volume, period)
+            self.assertTrue(compare_equal(vwma_res, self.data[period]['vwma']))
\ No newline at end of file
diff --git a/tests/indicators/test_oscillators.py b/tests/indicators/test_oscillators.py
new file mode 100644
index 00000000..1f1f8a0c
--- /dev/null
+++ b/tests/indicators/test_oscillators.py
@@ -0,0 +1,59 @@
+import pickle
+import unittest
+from pathlib import Path
+
+import numpy as np
+
+from blankly.indicators import absolute_price_oscillator, aroon_oscillator, chande_momentum_oscillator, \
+    percentage_price_oscillator, rsi, stochastic_oscillator
+
+
+def compare_equal(a, b):
+    # compares two numpy arrays
+    return np.array_equal(a, b)
+
+
+class Oscillators(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        data_path = Path("tests/config/test_data.p").resolve()
+        with open(data_path, 'rb') as f:
+            cls.data = pickle.load(f)
+
+    def test_rsi(self):
+        for period in self.data['periods']:
+            rsi_res = rsi(self.data['close'], period)
+            self.assertTrue(compare_equal(rsi_res, self.data[period]['rsi']))
+
+    def test_aroon_oscillator(self):
+        for period in self.data['periods']:
+            aroon_res = aroon_oscillator(self.data['high'], self.data['low'], period)
+            self.assertTrue(compare_equal(aroon_res, self.data[period]['aroon_oscillator']))
+
+    def test_chande_momentum_oscillator(self):
+        for period in self.data['periods']:
+            chande_res = chande_momentum_oscillator(self.data['close'], period)
+            self.assertTrue(compare_equal(chande_res, self.data[period]['chande_momentum_oscillator']))
+
+
+    def test_absolute_price_oscillator(self):
+        short_period = self.data['short_period']
+        long_period = self.data['long_period']
+        res = absolute_price_oscillator(self.data['close'], short_period, long_period)
+        # use the first one since general period doesn't matter
+        self.assertTrue(compare_equal(res, self.data[self.data['periods'][0]]['absolute_price_oscillator']))
+
+    def test_percentage_price_oscillator(self):
+        short_period = self.data['short_period']
+        long_period = self.data['long_period']
+        res = percentage_price_oscillator(self.data['close'], short_period, long_period)
+        # use the first one since general period doesn't matter
+        self.assertTrue(compare_equal(res, self.data[self.data['periods'][0]]['percentage_price_oscillator']))
+
+    def test_stochastic_oscillator(self):
+        pct_k_period = self.data['pct_k_period']
+        pct_k_slowing_period = self.data['pct_k_slowing_period']
+        pct_d_period = self.data['pct_d_period']
+        res = stochastic_oscillator(self.data['high'], self.data['low'], self.data['close'], pct_k_period, pct_k_slowing_period, pct_d_period)
+        # use the first one since general period doesn't matter
+        self.assertTrue(compare_equal(res, self.data[self.data['periods'][0]]['stochastic_oscillator']))
\ No newline at end of file
diff --git a/tests/indicators/test_statistics.py b/tests/indicators/test_statistics.py
new file mode 100644
index 00000000..83dbe125
--- /dev/null
+++ b/tests/indicators/test_statistics.py
@@ -0,0 +1,56 @@
+import pickle
+import unittest
+from pathlib import Path
+
+import numpy as np
+
+from blankly.indicators import max_period, min_period, stddev_period, stderr_period, var_period
+
+
+def compare_equal(a, b):
+    # compares two numpy arrays
+    return np.array_equal(a, b)
+
+
+class Statistics(unittest.TestCase):
+    @classmethod
+    def setUpClass(cls) -> None:
+        data_path = Path("tests/config/test_data.p").resolve()
+        with open(data_path, 'rb') as f:
+            cls.data = pickle.load(f)
+
+    def test_one_param_moving_averages(self):
+        period = self.data['periods'][0]
+
+        stddev_res = stddev_period(self.data['close'], period)
+        self.assertTrue(compare_equal(stddev_res, self.data[period]['stddev_period']))
+
+        var_res = var_period(self.data['close'], period)
+        self.assertTrue(compare_equal(var_res, self.data[period]['var_period']))
+
+        stderr_res = stderr_period(self.data['close'], period)
+        self.assertTrue(compare_equal(stderr_res, self.data[period]['stderr_period']))
+
+        min_res = min_period(self.data['close'], period)
+        self.assertTrue(compare_equal(min_res, self.data[period]['min_period']))
+
+        max_res = max_period(self.data['close'], period)
+        self.assertTrue(compare_equal(max_res, self.data[period]['max_period']))
+
+    def test_one_param_moving_averages_diff_period(self):
+        period = self.data['periods'][1]
+
+        stddev_res = stddev_period(self.data['close'], period)
+        self.assertTrue(compare_equal(stddev_res, self.data[period]['stddev_period']))
+
+        var_res = var_period(self.data['close'], period)
+        self.assertTrue(compare_equal(var_res, self.data[period]['var_period']))
+
+        stderr_res = stderr_period(self.data['close'], period)
+        self.assertTrue(compare_equal(stderr_res, self.data[period]['stderr_period']))
+
+        min_res = min_period(self.data['close'], period)
+        self.assertTrue(compare_equal(min_res, self.data[period]['min_period']))
+
+        max_res = max_period(self.data['close'], period)
+        self.assertTrue(compare_equal(max_res, self.data[period]['max_period']))
\ No newline at end of file
diff --git a/Blankly/auth/auth.py b/tests/metrics/test_portfolio_metrics.py
similarity index 100%
rename from Blankly/auth/auth.py
rename to tests/metrics/test_portfolio_metrics.py
diff --git a/tests/test_auth_constructor.py b/tests/test_auth_constructor.py
index e212e8f4..1a4c990a 100644
--- a/tests/test_auth_constructor.py
+++ b/tests/test_auth_constructor.py
@@ -1,5 +1,4 @@
 import unittest
-import Blankly.auth_constructor as auth
 
 
 class TestAuthConstructor(unittest.TestCase):