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

Issue:
Merge backtest to development



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..600d2d33
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,1 @@
+.vscode
\ No newline at end of file
diff --git a/Blankly/__init__.py b/Blankly/__init__.py
index dc290141..a6b84e1e 100644
--- a/Blankly/__init__.py
+++ b/Blankly/__init__.py
@@ -18,16 +18,18 @@
 
 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.interface.currency_Interface import CurrencyInterface as Interface
 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 as StrategyHelper
+from Blankly.strategy.strategy_base import Strategy
diff --git a/Blankly/auth_constructor.py b/Blankly/auth_constructor.py
index ad63e601..eed01e82 100644
--- a/Blankly/auth_constructor.py
+++ b/Blankly/auth_constructor.py
@@ -27,7 +27,7 @@ def load_json(keys_file):
         f.close()
         return contents
     except FileNotFoundError:
-        raise FileNotFoundError("Make sure a Keys.json file is placed in the same folder as the project working "
+        raise FileNotFoundError("Make sure a keys.json file is placed in the same folder as the project working "
                                 "directory!")
 
 
diff --git a/Blankly/exchanges/Alpaca/Alpaca.py b/Blankly/exchanges/Alpaca/Alpaca.py
index dc349111..c6b1e932 100644
--- a/Blankly/exchanges/Alpaca/Alpaca.py
+++ b/Blankly/exchanges/Alpaca/Alpaca.py
@@ -1,6 +1,6 @@
 """
-    Coinbase Pro exchange definitions and setup
-    Copyright (C) 2021  Emerson Dove
+    Alpaca exchange definitions and setup
+    Copyright (C) 2021  Emerson Dove, Arun Annamalai, 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
@@ -22,11 +22,11 @@
 from Blankly.interface.currency_factory import InterfaceFactory
 
 class Alpaca(Exchange):
-    def __init__(self, portfolio_name=None, auth_path="Keys.json", preferences_path=None):
+    def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
         if not portfolio_name:
-            portfolio_name = default_first_portfolio(auth_path, 'alpaca')
-        Exchange.__init__(self, 'alpaca', portfolio_name, preferences_path)
-        alpaca_auth = AuthFactory(auth_path, 'alpaca', portfolio_name)
+            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)
 
 
diff --git a/Blankly/exchanges/Alpaca/Alpaca_API.py b/Blankly/exchanges/Alpaca/Alpaca_API.py
index a798d044..b60b6e22 100644
--- a/Blankly/exchanges/Alpaca/Alpaca_API.py
+++ b/Blankly/exchanges/Alpaca/Alpaca_API.py
@@ -1,11 +1,18 @@
 from Blankly.auth.Alpaca.auth import alpaca_auth
 
-import alpaca_trade_api as tradeapi
+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):
@@ -14,7 +21,7 @@ def __init__(self, auth: alpaca_auth, paper_trading=True):
         else:
             self.__api_url = APCA_API_LIVE_URL
 
-        self.alp_client = tradeapi.REST(auth.API_KEY, auth.API_SECRET, self.__api_url, 'v2')
+        self.alp_client = alpaca_trade_api.REST(auth.API_KEY, auth.API_SECRET, self.__api_url, 'v2')
 
 
 api = os.getenv("ALPACA_PUBLIC")
diff --git a/Blankly/exchanges/Alpaca/alpaca_api_interface.py b/Blankly/exchanges/Alpaca/alpaca_api_interface.py
index 5263c033..dde2523b 100644
--- a/Blankly/exchanges/Alpaca/alpaca_api_interface.py
+++ b/Blankly/exchanges/Alpaca/alpaca_api_interface.py
@@ -3,19 +3,19 @@
 from Blankly.utils import utils as utils
 from Blankly.exchanges.Alpaca.Alpaca_API import API
 from Blankly.interface.currency_Interface import CurrencyInterface
-import alpaca_trade_api as tradeapi
+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):
-        super().__init__('alpaca', authenticated_API)
-        assert isinstance(self.calls, tradeapi.REST)
+    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, tradeapi.REST)
-        account_info = self.calls.get_account()._raw
+        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....')
@@ -27,7 +27,7 @@ def init_exchange(self):
             "taker_fee_rate": 0
         }
 
-    def get_products(self):
+    def get_products(self) -> dict:
         '''
         [
             {
@@ -46,7 +46,7 @@ def get_products(self):
         ]
         '''
         needed = self.needed['get_products']
-        assets = self.calls.list_assets(status=None, asset_class=None)._raw
+        assets = self.calls.list_assets(status=None, asset_class=None)
 
         for asset in assets:
             asset['currency_id'] = asset.pop('id')
@@ -62,11 +62,11 @@ def get_products(self):
         return assets
 
     def get_account(self, currency=None, override_paper_trading=False):
-        assert isinstance(self.calls, tradeapi.REST)
+        assert isinstance(self.calls, alpaca_trade_api.REST)
         needed = self.needed['get_account']
 
-        account_dict = self.calls.get_account()._raw
-        account_dict['currency'] = account_dict.pop('cash')
+        account_dict = self.calls.get_account()
+        account_dict['available'] = account_dict.pop('cash')
         account_dict['hold'] = -1
 
         positions = self.calls.list_positions()
@@ -83,7 +83,7 @@ def get_account(self, currency=None, override_paper_trading=False):
         return positions
 
     def market_order(self, product_id, side, funds) -> MarketOrder:
-        assert isinstance(self.calls, tradeapi.REST)
+        assert isinstance(self.calls, alpaca_trade_api.REST)
         needed = self.needed['market_order']
 
         order = {
@@ -92,7 +92,7 @@ def market_order(self, product_id, side, funds) -> MarketOrder:
             'product_id': product_id,
             'type': 'market'
         }
-        response = self.calls.submit_order(product_id, side=side, type='market', time_int_force='day', notional=funds)._raw
+        response = 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)
 
@@ -100,7 +100,7 @@ def limit_order(self, product_id, side, price, size) -> LimitOrder:
         pass
 
     def cancel_order(self, currency_id, order_id) -> dict:
-        assert isinstance(self.calls, tradeapi.REST)
+        assert isinstance(self.calls, alpaca_trade_api.REST)
         self.calls.cancel_order(order_id)
 
         #TODO: handle the different response codes
@@ -108,9 +108,9 @@ def cancel_order(self, currency_id, order_id) -> dict:
 
     # TODO: this doesnt exactly fit
     def get_open_orders(self, product_id=None):
-        assert isinstance(self.calls, tradeapi.REST)
+        assert isinstance(self.calls, alpaca_trade_api.REST)
         needed = self.needed['get_open_orders']
-        orders = self.calls.list_orders()._raw
+        orders = self.calls.list_orders()
         renames = [
             ["asset_id", "product_id"],
             ["filled_at", "price"],
@@ -123,9 +123,9 @@ def get_open_orders(self, product_id=None):
         return orders
 
     def get_order(self, currency_id, order_id) -> dict:
-        assert isinstance(self.calls, tradeapi.REST)
+        assert isinstance(self.calls, alpaca_trade_api.REST)
         needed = self.needed['get_order']
-        order = self.calls.get_order(order_id)._raw
+        order = self.calls.get_order(order_id)
         renames = [
             ["asset_id", "product_id"],
             ["filled_at", "price"],
@@ -137,23 +137,23 @@ def get_order(self, currency_id, order_id) -> dict:
         return order
 
     def get_fees(self):
-        assert isinstance(self.calls, tradeapi.REST)
+        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, tradeapi.REST)
+        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, tradeapi.REST)
+        assert isinstance(self.calls, alpaca_trade_api.REST)
         pass
 
     def get_price(self, currency_pair) -> float:
-        assert isinstance(self.calls, tradeapi.REST)
+        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
index 1c3021c0..81955623 100644
--- a/Blankly/exchanges/Binance/Binance.py
+++ b/Blankly/exchanges/Binance/Binance.py
@@ -25,11 +25,11 @@
 
 
 class Binance(Exchange):
-    def __init__(self, portfolio_name=None, auth_path="Keys.json", preferences_path=None):
+    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(auth_path, portfolio_name)
+        auth, defined_name = Blankly.auth_constructor.load_auth_binance(keys_path, portfolio_name)
 
-        Exchange.__init__(self, "binance", defined_name, preferences_path)
+        Exchange.__init__(self, "binance", defined_name, settings_path)
 
         preferences = utils.load_user_preferences()
         if preferences["settings"]["use_sandbox"] or preferences["settings"]["paper_trade"]:
diff --git a/Blankly/exchanges/Binance/Binance_Interface.py b/Blankly/exchanges/Binance/Binance_Interface.py
index 425b7b14..749421da 100644
--- a/Blankly/exchanges/Binance/Binance_Interface.py
+++ b/Blankly/exchanges/Binance/Binance_Interface.py
@@ -436,7 +436,7 @@ def get_open_orders(self, product_id=None):
             orders = self.calls.get_open_orders(symbol=product_id)
             orders = utils.rename_to(renames, orders)
             orders = utils.isolate_specific(needed, orders)
-            orders['product_id'] = utils.to_blankly_coin_id(orders['product_id'], 'binance', quote_currency=None)
+            orders['product_id'] = utils.to_blankly_coin_id(orders['product_id'], 'binance', quote_guess=None)
             return orders
         else:
             orders = self.calls.get_open_orders()
@@ -444,7 +444,7 @@ def get_open_orders(self, product_id=None):
         for i in range(len(orders)):
             orders[i] = utils.rename_to(renames, orders[i])
             orders[i] = utils.isolate_specific(needed, orders[i])
-            orders[i]['product_id'] = utils.to_blankly_coin_id(orders[i]['product_id'], 'binance', quote_currency=None)
+            orders[i]['product_id'] = utils.to_blankly_coin_id(orders[i]['product_id'], 'binance', quote_guess=None)
 
         return orders
 
@@ -490,7 +490,7 @@ def get_order(self, currency_id, order_id) -> dict:
     Binance: get_trade_fee
     """
 
-    def get_fees(self):
+    def get_fees(self) -> dict:
         needed = self.needed['get_fees']
         """
         {
@@ -579,6 +579,7 @@ def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
 
         # Figure out how many points are needed
         need = int((epoch_stop - epoch_start) / granularity)
+        initial_need = need
         window_open = epoch_start
         history = []
 
@@ -586,14 +587,15 @@ def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
         product_id = utils.to_exchange_coin_id(product_id, 'binance')
         while need > 1000:
             # Close is always 300 points ahead
-            window_close = window_open + 1000 * granularity
+            window_close = int(window_open + 1000 * granularity)
             history = history + self.calls.get_klines(symbol=product_id, startTime=window_open * 1000,
                                                       endTime=window_close * 1000, interval=gran_string,
                                                       limit=1000)
 
             window_open = window_close
             need -= 1000
-            time.sleep(1)
+            time.sleep(.2)
+            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,
@@ -608,6 +610,23 @@ def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
         del data_frame['ignore']
         # Want them in this order: ['time (epoch)', 'low', 'high', 'open', 'close', 'volume']
 
+        # Cast dataframe
+        data_frame = data_frame.astype({'time': int,
+                                        'open': float,
+                                        'high': float,
+                                        'low': float,
+                                        'close': float,
+                                        'volume': float,
+                                        'close time': int,
+                                        'quote asset volume': float,
+                                        'number of trades': int,
+                                        'taker buy base asset volume': float,
+                                        'taker buy quote asset volume': float
+                                        })
+
+        # Convert time to seconds
+        data_frame['time'] = data_frame['time'].div(1000)
+
         return data_frame.reindex(columns=['time', 'low', 'high', 'open', 'close', 'volume'])
 
     """
@@ -726,6 +745,7 @@ def get_market_limits(self, product_id):
             "max_orders": max_orders,
             "min_price": min_price,
             "max_price": max_price,
+            "exchange_specific": {}
         }
 
     def get_price(self, currency_pair) -> float:
diff --git a/Blankly/exchanges/Binance/websocket_utils.py b/Blankly/exchanges/Binance/websocket_utils.py
index 68a478db..7beb7f0a 100644
--- a/Blankly/exchanges/Binance/websocket_utils.py
+++ b/Blankly/exchanges/Binance/websocket_utils.py
@@ -1,5 +1,5 @@
 """
-    Websocket feeds need to be modular due to the subscription methods, this provides dynamic management.
+    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
@@ -91,11 +91,10 @@ def trade_interface(message):
     ]
     message = utils.rename_to(renames, message)
     needed = [
-        ["type", str],
         ["product_id", str],
         ["price", float],
         ["time", int],
         ["trade_id", int]
     ]
+    message["time"] = message["time"]/1000
     return utils.isolate_specific(needed, message)
-
diff --git a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro.py b/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro.py
index 6f87440d..581f1a41 100644
--- a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro.py
+++ b/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro.py
@@ -22,12 +22,12 @@
 
 
 class Coinbase_Pro(Exchange):
-    def __init__(self, portfolio_name=None, auth_path="Keys.json", preferences_path=None):
+    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(auth_path, portfolio_name)
+        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, preferences_path)
+        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],
diff --git a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Interface.py b/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Interface.py
index 9b7a8083..df60de17 100644
--- a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Interface.py
+++ b/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Interface.py
@@ -89,7 +89,6 @@ def get_account(self, currency=None):
 
             These arguments are mutually exclusive
         Coinbase Pro: get_account
-        Binance: get_account["balances"]
         """
         currency = super().get_account(currency=currency)
 
@@ -363,7 +362,7 @@ def get_order(self, currency_id, order_id) -> dict:
     Binance: get_trade_fee
     """
 
-    def get_fees(self):
+    def get_fees(self) -> dict:
         needed = self.needed['get_fees']
         """
         {
@@ -402,8 +401,11 @@ def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
             granularity = accepted_grans[min(range(len(accepted_grans)),
                                              key=lambda i: abs(accepted_grans[i] - granularity))]
 
+        granularity = int(granularity)
+
         # Figure out how many points are needed
         need = int((epoch_stop - epoch_start) / granularity)
+        initial_need = need
         window_open = epoch_start
         history = []
         # Iterate while its more than max
@@ -412,18 +414,23 @@ def get_product_history(self, product_id, epoch_start, epoch_stop, granularity):
             window_close = window_open + 300 * granularity
             open_iso = utils.ISO8601_from_epoch(window_open)
             close_iso = utils.ISO8601_from_epoch(window_close)
-            history = history + self.calls.get_product_historic_rates(product_id, open_iso, close_iso,
-                                                                      granularity)
+            response = self.calls.get_product_historic_rates(product_id, open_iso, close_iso, granularity)
+            if isinstance(response, dict):
+                raise APIException(response['message'])
+            history = history + response
 
             window_open = window_close
             need -= 300
-            time.sleep(1)
+            time.sleep(.2)
+            utils.update_progress((initial_need - need) / initial_need)
 
         # Fill the remainder
         open_iso = utils.ISO8601_from_epoch(window_open)
         close_iso = utils.ISO8601_from_epoch(epoch_stop)
-        history_block = history + self.calls.get_product_historic_rates(product_id, open_iso, close_iso,
-                                                                        granularity)
+        response = self.calls.get_product_historic_rates(product_id, open_iso, close_iso, granularity)
+        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'])
 
diff --git a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Tickers.py b/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Websocket.py
similarity index 79%
rename from Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Tickers.py
rename to Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Websocket.py
index e29738dd..485b221f 100644
--- a/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Tickers.py
+++ b/Blankly/exchanges/Coinbase_Pro/Coinbase_Pro_Websocket.py
@@ -23,33 +23,37 @@
 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
 
 
-def create_ticker_connection(id, url):
+def create_ticker_connection(id, url, channel):
     ws = create_connection(url, sslopt={"cert_reqs": ssl.CERT_NONE})
     request = """{
     "type": "subscribe",
     "product_ids": [
         \"""" + id + """\"
     ],
-    "channels": [
-        {
-            "name": "ticker",
-            "product_ids": [
-                \"""" + id + """\"
-            ]
-        }
-    ]
+    "channels": [ \"""" + channel + """\" ]
     }"""
     ws.send(request)
     return ws
 
+# This could be needed:
+# "channels": [
+#     {
+#         "name": "ticker",
+#         "product_ids": [
+#             \"""" + id + """\"
+#         ]
+#     }
+# ]
+
 
 class Tickers(IExchangeWebsocket):
-    def __init__(self, currency_id, log=None, WEBSOCKET_URL="wss://ws-feed.pro.coinbase.com"):
+    def __init__(self, currency_id, stream, log=None, pre_event_callback=None, WEBSOCKET_URL="wss://ws-feed.pro.coinbase.com"):
         """
         Create and initialize the ticker
         Args:
@@ -58,6 +62,8 @@ def __init__(self, currency_id, log=None, WEBSOCKET_URL="wss://ws-feed.pro.coinb
             WEBSOCKET_URL: Default websocket URL feed.
         """
         self.__id = currency_id
+        self.__stream = stream
+        self.__logging_callback, self.__interface_callback, log_message = websocket_utils.switch_type(stream)
 
         # Initialize log file
         if log is not None:
@@ -65,9 +71,7 @@ def __init__(self, currency_id, log=None, WEBSOCKET_URL="wss://ws-feed.pro.coinb
             self.__filePath = log
             try:
                 self.__file = open(log, 'x+')
-                self.__file.write(
-                    "time,system_time,price,open_24h,volume_24h,low_24h,high_24h,volume_30d,best_bid,best_ask,"
-                    "last_size\n")
+                self.__file.write(log_message)
             except FileExistsError:
                 self.__file = open(log, 'a')
         else:
@@ -79,6 +83,7 @@ def __init__(self, currency_id, log=None, WEBSOCKET_URL="wss://ws-feed.pro.coinb
         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()
@@ -94,7 +99,7 @@ def start_websocket(self):
         Restart websocket if it was asked to stop.
         """
         if self.ws is None:
-            self.ws = create_ticker_connection(self.__id, self.URL)
+            self.ws = create_ticker_connection(self.__id, self.URL, self.__stream)
             self.__response = self.ws.recv()
             thread = threading.Thread(target=self.read_websocket)
             thread.start()
@@ -108,6 +113,15 @@ def start_websocket(self):
                 self.start_websocket()
 
     def read_websocket(self):
+        # This is unique because coinbase first sends the entire orderbook to use
+        if self.__pre_event_callback is not None and self.__stream == "level2":
+            received_string = json.loads(self.ws.recv())
+            if received_string['type'] == 'snapshot':
+                try:
+                    self.__pre_event_callback(received_string)
+                except:
+                    traceback.print_exc()
+
         counter = 0
         # TODO port this to "WebSocketApp" found in the websockets documentation
         while self.ws.connected:
@@ -116,27 +130,25 @@ def read_websocket(self):
             try:
                 received_string = self.ws.recv()
                 received = json.loads(received_string)
-                self.__most_recent_time = Blankly.utils.epoch_from_ISO8601(received["time"])
                 # Modify time to use epoch
+                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
                 self.__ticker_feed.append(received)
-                self.__time_feed.append(self.__most_recent_time)
 
                 if self.__log:
                     if counter % 100 == 0:
                         self.__file.close()
                         self.__file = open(self.__filePath, 'a')
-                    line = str(received["time"]) + "," + str(time.time()) + "," + received["price"] + "," + received[
-                        "open_24h"] + "," + received["volume_24h"] + "," + received["low_24h"] + "," + received[
-                               "high_24h"] + "," + received["volume_30d"] + "," + received["best_bid"] + "," + received[
-                               "best_ask"] + "," + received["last_size"] + "\n"
+                    line = self.__logging_callback(received)
                     self.__file.write(line)
 
                 # Manage price events and fire for each manager attached
+                interface_message = self.__interface_callback(received)
                 try:
                     for i in self.__callbacks:
-                        i(self.__most_recent_tick)
+                        i(interface_message)
                 except Exception:
                     traceback.print_exc()
 
@@ -147,11 +159,12 @@ def read_websocket(self):
                     pass
                 else:
                     traceback.print_exc()
-                    print("Error reading ticker websocket for " + self.__id + ": attempting to re-initialize")
+                    print("Error reading ticker websocket for " + self.__id + " 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.__id, self.URL)
+                    self.ws = create_ticker_connection(self.__id, self.URL, self.__stream)
                     # Update response
                     self.__response = self.ws.recv()
 
@@ -193,7 +206,7 @@ def close_websocket(self):
         if self.ws.connected:
             self.ws.close()
         else:
-            print("Websocket for " + self.__id + " is already closed")
+            print("Websocket for " + self.__id + ' on channel ' + self.__stream + " is already closed")
 
     """ Required in manager """
     def restart_ticker(self):
diff --git a/Blankly/exchanges/Coinbase_Pro/orderbook_websocket.py b/Blankly/exchanges/Coinbase_Pro/orderbook_websocket.py
index 899f88fe..a5ae51df 100644
--- a/Blankly/exchanges/Coinbase_Pro/orderbook_websocket.py
+++ b/Blankly/exchanges/Coinbase_Pro/orderbook_websocket.py
@@ -15,7 +15,6 @@
     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
@@ -27,6 +26,10 @@
 
 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})
diff --git a/Blankly/exchanges/Coinbase_Pro/websocket_utils.py b/Blankly/exchanges/Coinbase_Pro/websocket_utils.py
new file mode 100644
index 00000000..74363418
--- /dev/null
+++ b/Blankly/exchanges/Coinbase_Pro/websocket_utils.py
@@ -0,0 +1,62 @@
+"""
+    Websocket feeds need to be modular due to the subscription methods, this provides dynamic management for CBP.
+    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 Blankly.utils.utils as utils
+
+
+def switch_type(stream):
+    if stream == "ticker":
+        return trade, \
+               trade_interface, \
+               "time,system_time,price,open_24h,volume_24h,low_24h,high_24h,volume_30d,best_bid,best_ask," \
+               "last_size\n"
+    elif stream == "level2":
+        return no_callback, \
+               no_callback, \
+               ""
+    else:
+        return no_callback, no_callback, ""
+
+
+def no_callback(message):
+    return message
+
+
+def trade(received):
+    line = str(received["time"]) + "," + str(time.time()) + "," + received["price"] + "," + received[
+        "open_24h"] + "," + received["volume_24h"] + "," + received["low_24h"] + "," + received[
+               "high_24h"] + "," + received["volume_30d"] + "," + received["best_bid"] + "," + received[
+               "best_ask"] + "," + received["last_size"] + "\n"
+    return line
+
+
+def trade_interface(message):
+    """
+    Homogenizing with binance's return:
+
+    Args:
+        message (str): Message from the exchange.
+    """
+    needed = [
+        ["product_id", str],
+        ["price", float],
+        ["time", int],
+        ["trade_id", int]
+    ]
+    return utils.isolate_specific(needed, message)
diff --git a/Blankly/exchanges/IExchange.py b/Blankly/exchanges/IExchange.py
index f6ac3ef3..63dc0421 100644
--- a/Blankly/exchanges/IExchange.py
+++ b/Blankly/exchanges/IExchange.py
@@ -28,10 +28,6 @@ class IExchange(abc.ABC):
     def append_model(self, model, coin, args):
         pass
 
-    @abc.abstractmethod
-    def get_model(self, coin):
-        pass
-
     @abc.abstractmethod
     def get_full_state(self, currency):
         pass
diff --git a/Blankly/exchanges/Paper_Trade/Paper_Trade.py b/Blankly/exchanges/Paper_Trade/Paper_Trade.py
index a9e54fa6..7d19476a 100644
--- a/Blankly/exchanges/Paper_Trade/Paper_Trade.py
+++ b/Blankly/exchanges/Paper_Trade/Paper_Trade.py
@@ -15,18 +15,11 @@
     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 typing
 
-import pandas
 
-from Blankly.utils.time_builder import time_interval_to_seconds
-from Blankly.exchanges.exchange import Exchange
 from Blankly.exchanges.Paper_Trade.Paper_Trade_Interface import PaperTradeInterface
-from Blankly.exchanges.Paper_Trade.backtest_controller import BackTestController
 from Blankly.exchanges.exchange import Exchange
 
-import pandas as pd
-
 
 class PaperTrade(Exchange):
     def __init__(self, authenticated_exchange: Exchange):
@@ -56,33 +49,5 @@ def get_exchange_state(self):
         # TODO Populate this with useful information
         return self.Interface.get_fees()
 
-    def append_backtest_price_data(self, asset_id, price_data: pandas.DataFrame):
-        if isinstance(price_data, str):
-            price_data = pd.read_csv(price_data)
-        elif isinstance(price_data, pandas.DataFrame):
-            pass
-        else:
-            raise TypeError("Price data is not of type str or dataframe.")
-        self.price_data[asset_id] = price_data
-
-    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.backtest_price_events.append([callback, asset_id, time_interval])
-
-    def backtest(self):
-        # Create arrays of price events along with their interval
-
-        # Create a new controller
-        if self.price_data == {} or self.backtest_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.")
-        else:
-            controller = BackTestController(self.get_interface(), self.price_data, self.backtest_price_events)
-
-        # Run the controller
-        return controller.run()
-
     def get_direct_calls(self):
         return None
diff --git a/Blankly/exchanges/Paper_Trade/Paper_Trade_Interface.py b/Blankly/exchanges/Paper_Trade/Paper_Trade_Interface.py
index 98b0d1f1..eb1ab5ab 100644
--- a/Blankly/exchanges/Paper_Trade/Paper_Trade_Interface.py
+++ b/Blankly/exchanges/Paper_Trade/Paper_Trade_Interface.py
@@ -50,6 +50,8 @@ def __init__(self, derived_interface):
 
         self.__run_watchdog = True
 
+        self.__thread = None
+
         CurrencyInterface.__init__(self, "paper_trade", derived_interface)
         BacktestingWrapper.__init__(self)
 
@@ -59,7 +61,10 @@ def __init__(self, derived_interface):
 
         # Iterate & pair
         for i in accounts:
-            value_pairs[i['currency']] = i['available']
+            value_pairs[i['currency']] = {
+                'available': i['available'],
+                'hold': i['hold']
+            }
 
         # Initialize the local account
         trade_local.init_local_account(value_pairs)
@@ -99,6 +104,28 @@ def __paper_trade_watchdog(self):
                 break
             self.evaluate_limits()
 
+    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
+        value dictionary that currently exist will be set to zero.
+
+        Args:
+            value_dictionary (dict): Account dictionary of format {'BTC': 2.3, 'GRT': 1.1}
+        """
+        current_account = trade_local.get_accounts()
+        for k, v in current_account.items():
+            if k in value_dictionary.keys():
+                current_account[k] = {
+                    'available': value_dictionary[k],
+                    'hold': 0
+                }
+            else:
+                current_account[k] = {
+                    'available': 0,
+                    'hold': 0
+                }
+        trade_local.init_local_account(current_account)
+
     def evaluate_limits(self):
         """
         When this is run it checks the local paper trade orders to see if any need to go through
@@ -141,7 +168,19 @@ def evaluate_limits(self):
 
                 if index['side'] == 'buy':
                     if index['price'] > current_price:
-                        order, funds = self.evaluate_paper_trade(index, current_price)
+                        # Take everything off hold
+                        asset_id = index['product_id']
+                        quote = utils.get_quote_currency(asset_id)
+
+                        available = trade_local.get_account(quote)['available']
+                        # Put it back into available
+                        trade_local.update_available(quote, available + (index['size'] * index['price']))
+
+                        # Take it out of hold
+                        hold = trade_local.get_account(quote)['hold']
+                        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'],
                                                 side='buy',
                                                 base_delta=float(order['filled_size']),  # Gain filled size after fees
@@ -152,11 +191,24 @@ def evaluate_limits(self):
                         self.paper_trade_orders[i] = order
                 elif index['side'] == 'sell':
                     if index['price'] < prices[index['product_id']]:
-                        order, funds = self.evaluate_paper_trade(index, current_price)
+                        # Take everything off hold
+
+                        asset_id = index['product_id']
+                        base = utils.get_base_currency(asset_id)
+
+                        available = trade_local.get_account(base)['available']
+                        # Put it back into available
+                        trade_local.update_available(base, available + index['size'])
+
+                        # Remove it from hold
+                        hold = trade_local.get_account(base)['hold']
+                        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'],
                                                 side='sell',
-                                                base_delta=float(index['size'] * - 1),  # Loose size before any fees
-                                                quote_delta=index['executed_value'])  # Gain executed value after fees
+                                                base_delta=float(order['size'] * - 1),  # Loose size before any fees
+                                                quote_delta=float(order['executed_value']))  # Executed value after fees
                         order['status'] = 'done'
                         order['settled'] = 'true'
 
@@ -170,9 +222,9 @@ def evaluate_paper_trade(self, order, current_price):
             current_price (float): The current price of the currency pair the limit order was created on
         """
         funds = order['size'] * current_price
-        executed_value = funds - funds * float((self.exchange_properties["maker_fee_rate"]))
-        fill_fees = funds * float((self.exchange_properties["maker_fee_rate"]))
-        fill_size = order['size'] - order['size'] * float((self.exchange_properties["maker_fee_rate"]))
+        executed_value = funds - funds * float((self.__exchange_properties["maker_fee_rate"]))
+        fill_fees = funds * float((self.__exchange_properties["maker_fee_rate"]))
+        fill_size = order['size'] - order['size'] * float((self.__exchange_properties["maker_fee_rate"]))
 
         order['executed_value'] = str(executed_value)
         order['fill_fees'] = str(fill_fees)
@@ -189,9 +241,8 @@ def get_account(self, currency=None):
         for key, value in local_account.items():
             accounts.append({
                 'currency': key,
-                'balance': value,
-                'available': value,
-                'hold': 0
+                'available': value['available'],
+                'hold': value['hold']
             })
 
         # We have to sort through it if the accounts are none
@@ -229,8 +280,8 @@ def market_order(self, product_id, side, funds) -> MarketOrder:
 
         qty = funds / price
 
-        if not trade_local.test_trade(product_id, side, qty , price):
-            raise InvalidOrder("Invalid Order: Insufficient funds")
+        # Test the purchase
+        trade_local.test_trade(product_id, side, qty, price)
         # 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
@@ -312,8 +363,8 @@ def limit_order(self, product_id, side, price, size) -> LimitOrder:
         if size < min_base:
             raise InvalidOrder("Invalid Order: Order quantity is too small. Minimum is: " + str(min_base))
 
-        if not trade_local.test_trade(product_id, side, size, price):
-            raise InvalidOrder("Invalid Order: Insufficient funds")
+        # Test the trade
+        trade_local.test_trade(product_id, side, size, price)
 
         # Create coinbase pro-like id
         coinbase_pro_id = paper_trade.generate_coinbase_pro_id()
@@ -336,6 +387,26 @@ 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)
+
+        if side == "buy":
+            available = trade_local.get_account(quote)['available']
+            # Loose the funds when buying
+            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))
+        elif side == "sell":
+            available = trade_local.get_account(base)['available']
+            # Loose the size when selling
+            trade_local.update_available(base, available - size)
+
+            # Gain size on hold when buying
+            hold = trade_local.get_account(base)['hold']
+            trade_local.update_hold(base, hold + size)
         return LimitOrder(order, response, self)
 
     def cancel_order(self, currency_id, order_id) -> dict:
@@ -349,14 +420,14 @@ def cancel_order(self, currency_id, order_id) -> dict:
                 order_index = i
 
         if order_index is not None:
-            id = self.paper_trade_orders[order_index]['id']
+            order_id = self.paper_trade_orders[order_index]['id']
             del self.paper_trade_orders[order_index]
-            return {"order_id": id}
+            return {"order_id": order_id}
 
     def get_open_orders(self, product_id=None):
         open_orders = []
         for i in self.paper_trade_orders:
-            if i["status"] == "open":
+            if i["status"] == "pending":
                 open_orders.append(i)
         return open_orders
 
diff --git a/Blankly/exchanges/exchange.py b/Blankly/exchanges/exchange.py
index 5c682589..7c43102b 100644
--- a/Blankly/exchanges/exchange.py
+++ b/Blankly/exchanges/exchange.py
@@ -1,6 +1,6 @@
 """
     Inherited exchange object.
-    Copyright (C) 2021  Emerson Dove
+    Copyright (C) 2021  Emerson Dove, Arun Annamalai, 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
@@ -65,7 +65,6 @@ def start_models(self, coin_id=None):
             # Run a specific model with the args
             if not self.models[coin_id]["model"].is_running():
                 self.models[coin_id]["model"].run(self.models[coin_id]["args"])
-            return "Started model attached to: " + coin_id
         else:
             for coin_iterator in self.models:
                 # Start all models
@@ -76,9 +75,11 @@ def start_models(self, coin_id=None):
                     time.sleep(.1)
                 else:
                     print("Ignoring the model on " + coin_iterator)
-            return "Started all models"
 
-    def get_model(self, coin):
+    def __get_model(self, coin):
+        """
+        Get the model to reference to
+        """
         return self.models[coin]["model"]
 
     def get_model_state(self, currency):
@@ -88,7 +89,7 @@ def get_model_state(self, currency):
         Args:
             currency: Currency that the selected model is running on.
         """
-        return (self.get_model(currency)).get_state()
+        return (self.__get_model(currency)).get_state()
 
     def get_full_state(self, currency):
         """
@@ -114,7 +115,7 @@ def write_value(self, currency, key, value):
             key: Key to assign a value to
             value: Value to assign to the key
         """
-        self.get_model(currency).update_state(key, value)
+        self.__get_model(currency).update_state(key, value)
 
     def append_model(self, model, coin_id, args=None):
         """
@@ -132,6 +133,7 @@ def append_model(self, model, coin_id, args=None):
         model.setup(self.__type, coin_id, self.preferences, self.get_full_state(coin_id),
                     self.Interface)
 
+    @abc.abstractmethod
     def get_currency_state(self, currency):
         pass
 
diff --git a/Blankly/exchanges/general_stream_manager.py b/Blankly/exchanges/general_stream_manager.py
new file mode 100644
index 00000000..c528eba1
--- /dev/null
+++ b/Blankly/exchanges/general_stream_manager.py
@@ -0,0 +1,121 @@
+"""
+    Class for creating general websocket streams
+    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_Websocket
+from Blankly.exchanges.Binance.Binance_Websocket import Tickers as Binance_Websocket
+
+from Blankly.exchanges.websocket_manager import WebsocketManager
+
+
+class GeneralManager(WebsocketManager):
+    def __init__(self, default_exchange, default_currency):
+        self.__default_exchange = default_exchange
+        self.__default_currency = default_currency
+
+        self.__websockets = {}
+
+        super().__init__(self.__websockets, default_currency, default_exchange)
+
+    def create_general_connection(self, callback, channel, log=None, asset_id=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_exchange is not None:
+            exchange_cache = override_exchange
+
+        preferences = Blankly.utils.load_user_preferences()
+
+        if channel not in self.__websockets.keys():
+            self.__websockets[channel] = {}
+
+        if exchange_cache not in self.__websockets[channel].keys():
+            self.__websockets[channel][exchange_cache] = {}
+
+        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']
+
+        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")
+            else:
+                websocket = Coinbase_Pro_Websocket(asset_id_cache, channel, log)
+            websocket.append_callback(callback)
+
+            self.__websockets[channel][exchange_cache][asset_id_cache] = websocket
+
+            return websocket
+        elif exchange_cache == "binance":
+            if use_sandbox:
+                websocket = Binance_Websocket(asset_id_cache, channel, log,
+                                              WEBSOCKET_URL="wss://testnet.binance.vision/ws")
+            else:
+                websocket = Binance_Websocket(asset_id_cache, channel, log)
+            websocket.append_callback(callback)
+
+            self.__websockets[channel][exchange_cache][asset_id_cache] = websocket
+
+            return websocket
+
+    """
+    Create overriden method signatures
+    """
+    def append_callback(self, callback_object, channel, override_currency=None, override_exchange=None):
+        self.websockets = self.__websockets[channel]
+        super().append_callback(callback_object, override_currency, override_exchange)
+
+    def is_websocket_open(self, channel, override_currency=None, override_exchange=None):
+        self.websockets = self.__websockets[channel]
+        return super().is_websocket_open(override_currency, override_exchange)
+
+    def get_most_recent_time(self, channel, override_currency=None, override_exchange=None):
+        self.websockets = self.__websockets[channel]
+        return super().get_most_recent_time(override_currency, override_exchange)
+
+    def get_time_feed(self, channel, override_currency=None, override_exchange=None):
+        self.websockets = self.__websockets[channel]
+        return super().get_time_feed(override_currency, override_exchange)
+
+    def get_feed(self, channel, override_currency=None, override_exchange=None):
+        self.websockets = self.__websockets[channel]
+        return super().get_feed(override_currency, override_exchange)
+
+    def get_response(self, channel, override_currency=None, override_exchange=None):
+        self.websockets = self.__websockets[channel]
+        return super().get_response(override_currency, override_exchange)
+
+    def close_websocket(self, channel, override_currency=None, override_exchange=None):
+        self.websockets = self.__websockets[channel]
+        return super().close_websocket(override_currency, override_exchange)
+
+    def restart_ticker(self, channel, override_currency=None, override_exchange=None):
+        self.websockets = self.__websockets[channel]
+        return super().restart_ticker(override_currency, override_exchange)
+
+    def get_most_recent_tick(self, channel, override_currency=None, override_exchange=None):
+        self.websockets = self.__websockets[channel]
+        return super().get_most_recent_tick(override_currency, override_exchange)
diff --git a/Blankly/exchanges/orderbook_manager.py b/Blankly/exchanges/orderbook_manager.py
index 791f92f3..81a052c7 100644
--- a/Blankly/exchanges/orderbook_manager.py
+++ b/Blankly/exchanges/orderbook_manager.py
@@ -19,7 +19,7 @@
 import Blankly.auth_constructor
 import requests
 
-from Blankly.exchanges.Coinbase_Pro.orderbook_websocket import OrderBook as Coinbase_Pro_Orderbook
+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
@@ -84,10 +84,10 @@ def create_orderbook(self, callback, currency_id=None, override_exchange=None):
         if exchange_name == "coinbase_pro":
             if currency_id is None:
                 currency_id = self.__default_currency
-            websocket = Coinbase_Pro_Orderbook(currency_id)
+            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)
-            websocket.append_snapshot_callback(self.coinbase_snapshot_update)
+
             # Store this object
             self.__websockets['coinbase_pro'][currency_id] = websocket
             self.__websockets_callbacks['coinbase_pro'][currency_id] = callback
@@ -160,7 +160,7 @@ def coinbase_update(self, update):
         self.__websockets_callbacks['coinbase_pro'][update['product_id']](book)
 
     def binance_update(self, update):
-        # TODO this needs a snapshot to work correctly
+        # TODO this needs a snapshot to work correctly, which needs arun's rest code
         # Get symbol first
         symbol = update['s']
 
@@ -210,5 +210,20 @@ def append_orderbook_callback(self, callback_object, override_currency_id=None,
         if override_exchange is None:
             override_exchange = self.__default_exchange
 
-        # if override_exchange == "coinbase_pro":
         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
index e843d2a5..467176ab 100644
--- a/Blankly/exchanges/ticker_manager.py
+++ b/Blankly/exchanges/ticker_manager.py
@@ -17,7 +17,7 @@
 """
 import Blankly.utils.utils
 
-from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_Tickers import Tickers as Coinbase_Pro_Ticker
+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
@@ -56,6 +56,9 @@ def create_ticker(self, callback, log=None, currency_id=None, override_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:
@@ -68,7 +71,12 @@ def create_ticker(self, callback, log=None, currency_id=None, override_exchange=
             if currency_id is None:
                 currency_id = self.__default_currency
 
-            ticker = Coinbase_Pro_Ticker(currency_id, log=log)
+            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
@@ -78,7 +86,10 @@ def create_ticker(self, callback, log=None, currency_id=None, override_exchange=
                 currency_id = self.__default_currency
 
             currency_id = Blankly.utils.to_exchange_coin_id(currency_id, "binance").lower()
-            ticker = Binance_Ticker(currency_id, "trade", log=log)
+            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
diff --git a/Blankly/exchanges/websocket_manager.py b/Blankly/exchanges/websocket_manager.py
index d2a3edb0..d3ceebbe 100644
--- a/Blankly/exchanges/websocket_manager.py
+++ b/Blankly/exchanges/websocket_manager.py
@@ -21,7 +21,7 @@
 
 class WebsocketManager(IExchangeWebsocket):
     def __init__(self, websockets, default_currency, default_exchange):
-        self.__websockets = websockets
+        self.websockets = websockets
         self.__default_currency = default_currency
         self.__default_exchange = default_exchange
 
@@ -29,7 +29,7 @@ def close_all_websockets(self):
         """
         Iterate through orderbooks and make sure they're closed
         """
-        self.__recursive_close(self.__websockets)
+        self.__recursive_close(self.websockets)
 
     def __recursive_close(self, d):
         for k, v in d.items():
@@ -55,7 +55,7 @@ def __evaluate_overrides(self, override_currency, override_exchange):
 
         if exchange == "binance":
             currency_id = Blankly.utils.to_exchange_coin_id(currency_id, "binance")
-        return currency_id, exchange
+        return self.websockets[exchange][currency_id]
 
     def get_ticker(self, currency_id, override_exchange=None):
         """
@@ -65,17 +65,17 @@ def get_ticker(self, currency_id, override_exchange=None):
             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][currency_id]
         else:
-            return self.__websockets[override_exchange][currency_id]
+            return self.websockets[override_exchange][currency_id]
 
-    def get_all_tickers(self):
+    def get_all_tickers(self) -> dict:
         """
         Get the tickers object dictionary. This can be used for individual management, or to stop using the manager.
         Returns:
             Dictionary with tickers referenced for each currency
         """
-        return self.__websockets
+        return self.websockets
 
     """
     Ticker functions/overridden
@@ -92,71 +92,71 @@ def append_callback(self, callback_object, override_currency=None, override_exch
             override_currency: Ticker id, such as "BTC-USD" or exchange equivalents.
             override_exchange: Forces the manager to use a different supported exchange.
         """
-        currency_id, exchange = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_currency, override_exchange)
 
-        self.__websockets[exchange][currency_id].append_callback(callback_object)
+        websocket.append_callback(callback_object)
 
-    def is_websocket_open(self, override_currency=None, override_exchange=None):
+    def is_websocket_open(self, override_currency=None, override_exchange=None) -> bool:
         """
         Check if the websocket attached to a currency is open
         """
-        currency_id, exchange = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_currency, override_exchange)
 
-        return self.__websockets[exchange][currency_id].is_websocket_open()
+        return websocket.is_websocket_open()
 
     def get_most_recent_time(self, override_currency=None, override_exchange=None):
         """
         Get the most recent time associated with the most recent tick
         """
-        currency_id, exchange = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_currency, override_exchange)
 
-        return self.__websockets[exchange][currency_id].get_most_recent_time()
+        return websocket.get_most_recent_time()
 
     def get_time_feed(self, override_currency=None, override_exchange=None):
         """
         Get a time array associated with the ticker feed.
         """
-        currency_id, exchange = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_currency, override_exchange)
 
-        return self.__websockets[exchange][currency_id].get_time_feed()
+        return websocket.get_time_feed()
 
     def get_feed(self, override_currency=None, override_exchange=None):
         """
         Get the full ticker array. This can be extremely large.
         """
-        currency_id, exchange = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_currency, override_exchange)
 
-        return self.__websockets[exchange][currency_id].get_feed()
+        return websocket.get_feed()
 
     def get_response(self, override_currency=None, override_exchange=None):
         """
         Get the exchange's response to the request to subscribe to a feed
         """
-        currency_id, exchange = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_currency, override_exchange)
 
-        return self.__websockets[exchange][currency_id].get_response()
+        return websocket.get_response()
 
     def close_websocket(self, override_currency=None, override_exchange=None):
         """
         Close a websocket thread
         """
-        currency_id, exchange = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_currency, override_exchange)
 
-        self.__websockets[exchange][currency_id].close_websocket()
+        websocket.close_websocket()
 
     def restart_ticker(self, override_currency=None, override_exchange=None):
         """
         Restart a websocket feed after asking it to stop
         """
-        currency_id, exchange = self.__evaluate_overrides(override_currency, override_exchange)
+        websocket = self.__evaluate_overrides(override_currency, override_exchange)
 
-        self.__websockets[exchange][currency_id].restart_ticker()
+        websocket.restart_ticker()
 
     def get_most_recent_tick(self, override_currency=None, override_exchange=None):
         """
         Get the most recent tick received
         """
-        currency_id, exchange = self.__evaluate_overrides(override_currency, override_exchange)
+        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
         #  allow a pointer to be subbed in for whichever exchange/currency/websocket type is overridden
-        return self.__websockets[exchange][currency_id].get_most_recent_tick()
+        return websocket.get_most_recent_tick()
diff --git a/Blankly/indicators/indicators.py b/Blankly/indicators/indicators.py
index 33aeff91..b8a92248 100644
--- a/Blankly/indicators/indicators.py
+++ b/Blankly/indicators/indicators.py
@@ -1,42 +1,55 @@
+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):
-    if type(data) == list:
-        data = np.asarray(data)
+    data = convert_to_numpy(data)    
     return ti.bbands(data, period, stddev)
 
-def wad(data, period=50):
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.wad(data, period)
-
-def wilders(data, period=50):
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.wilders(data, period)
-
-def willr(data, period=50):
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.willr(data, period)
-
-def true_range(high_data, low_data, close_data, period=50):
-    if type(high_data) == list:
-        high_data = np.asarray(high_data)
-    if type(low_data) == list:
-        low_data = np.asarray(low_data)
-    if type(close_data) == list:
-        close_data = np.asarray(close_data)
-    return ti.tr(high_data, low_data, close_data, period=period)
-
-def average_true_range(high_data, low_data, close_data, period=50):
-    if type(high_data) == list:
-        high_data = np.asarray(high_data)
-    if type(low_data) == list:
-        low_data = np.asarray(low_data)
-    if type(close_data) == list:
-        close_data = np.asarray(close_data)
-    return ti.atr(high_data, low_data, close_data, period=period)
+
+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/moving_averages.py b/Blankly/indicators/moving_averages.py
index 730f4032..b0354d97 100644
--- a/Blankly/indicators/moving_averages.py
+++ b/Blankly/indicators/moving_averages.py
@@ -1,56 +1,94 @@
-import numpy as np
+from Blankly.indicators.utils import convert_to_numpy
+from Blankly.indicators.utils import check_series
+from typing import Any
+import pandas as pd
 import tulipy as ti
 
-def ema(data, period=50) -> np.array:
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.ema(data, period=period)
 
-def vwma(data, volume_data, period=50):
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.vwma(data, volume_data, period=period)
+def ema(data: Any, period: int = 50, use_series=False) -> Any:
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    ema = ti.ema(data, period=period)
+    return pd.Series(ema) if use_series else ema
 
-def wma(data, period=50) -> np.array:
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.wma(data, period)
 
+def vwma(data: Any, volume_data: Any, period: int = 50) -> Any:
+    use_series = False
+    if check_series(data):
+        use_series = True
 
-def zlema(data, period=50) -> np.array:
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.zlema(data, period)
+    data = convert_to_numpy(data)
+    volume_data = convert_to_numpy(volume_data)
 
+    vwma = ti.vwma(data, volume_data, period=period)
+    return pd.Series(vwma) if use_series else vwma
 
-def sma(data, period=50) -> np.array:
+
+def wma(data: Any, period: int = 50) -> Any:
+    use_series = False
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    wma = ti.wma(data, period)
+    return pd.Series(wma) if use_series else wma
+
+
+def zlema(data: Any, period: int = 50) -> Any:
+    use_series = False
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    zlema = ti.zlema(data, period)
+    return pd.Series(zlema) if use_series else zlema
+
+
+def sma(data: Any, period: int = 50) -> 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
     """
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.sma(data, period=period)
-
-
-def hma(data, period=50) -> np.array:
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.hma(data, period)
-
-def kaufman_adaptive_ma(data, period=50) -> np.array:
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.kama(data, period)
-
-def trima(data, period=50) -> np.array:
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.trima(data, period)
-
-def macd(data, short_period=12, long_period=26, signal_period=9) -> np.array:
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.macd(data, short_period, long_period, signal_period)
+    use_series = False
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    sma = ti.sma(data, period=period)
+    return pd.Series(sma) if use_series else sma
+
+
+def hma(data: Any, period: int = 50) -> Any:
+    use_series = False
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    hma = ti.hma(data, period)
+    return pd.Series(hma) if use_series else hma
+
+
+def kaufman_adaptive_ma(data: Any, period: int = 50) -> Any:
+    use_series = False
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    kama = ti.kama(data, period)
+    return pd.Series(kama) if use_series else kama
+
+
+def trima(data: Any, period: int = 50) -> Any:
+    use_series = False
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    trima = ti.trima(data, period)
+    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
+    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
diff --git a/Blankly/indicators/oscillators.py b/Blankly/indicators/oscillators.py
index 1a9beabb..82dcc206 100644
--- a/Blankly/indicators/oscillators.py
+++ b/Blankly/indicators/oscillators.py
@@ -1,53 +1,73 @@
+from Blankly.indicators.utils import convert_to_numpy
+from Blankly.indicators.utils import check_series
+from typing import Any
 import numpy as np
 import pandas as pd
 import tulipy as ti
 
-def rsi(data, period=14, round_rsi=True) -> np.array:
+
+def rsi(data: Any, period: int = 14, round_rsi: bool = True) -> np.array:
     """ Implements RSI Indicator """
-    if type(data) == list:
-        data = np.asarray(data)
-    
+    use_series = False
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
     rsi_values = ti.rsi(data, period)
-    return np.round(rsi_values, 2) if round_rsi else rsi
-
-def aroon_oscillator(high_data, low_data, period=14):
-    if type(high_data) == list:
-        high_data = np.asarray(high_data)
-    if type(low_data) == list:
-        low_data = np.asarray(low_data)
-    return ti.aroonosc(high_data, low_data, period=period)
-
-def chande_momentum_oscillator(data, period=14):
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.cmo(data, period)
-
-def absolute_price_oscillator(data, short_period=12, long_period=26):
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.apo(data, short_period, long_period)
-
-def percentage_price_oscillator(data, short_period=12, long_period=26):
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.ppo(data, short_period, long_period)
-
-def stochastic_oscillator(high_data, low_data, close_data, pct_k_period=14, pct_k_slowing_period=3, pct_d_period=3):
-    if type(high_data) == list:
-        high_data = np.asarray(high_data)
-    if type(low_data) == list:
-        low_data = np.asarray(low_data)
-    if type(close_data) == list:
-        close_data = np.asarray(close_data)
-    return ti.stoch(high_data, low_data, close_data, pct_k_period, pct_k_slowing_period, pct_d_period)
+    if round_rsi:
+        rsi_values = np.round(rsi_values, 2)
+    return pd.Series(rsi_values) if use_series else rsi_values
+
+
+def aroon_oscillator(high_data: Any, low_data: Any, period=14, use_series=False):
+    if check_series(high_data) or check_series(low_data):
+        use_series = True
+    high_data = convert_to_numpy(high_data)
+    low_data = convert_to_numpy(low_data)
+    aroonsc = ti.aroonosc(high_data, low_data, period=period)
+    return pd.Series(aroonsc) if use_series else aroonsc
+
+
+def chande_momentum_oscillator(data, period=14, use_series=False):
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    cmo = ti.cmo(data, period)
+    return pd.Series(cmo) if use_series else cmo
+
+
+def absolute_price_oscillator(data, short_period=12, long_period=26, use_series=False):
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    apo = ti.apo(data, short_period, long_period)
+    return pd.Series(apo) if use_series else apo
+
+
+def percentage_price_oscillator(data, short_period=12, long_period=26, use_series=False):
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    ppo = ti.ppo(data, short_period, long_period)
+    return pd.Series(ppo) if use_series else ppo
+
+
+def stochastic_oscillator(high_data, low_data, close_data, pct_k_period=14, pct_k_slowing_period=3, pct_d_period=3,
+                          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)
+    stoch = ti.stoch(high_data, low_data, close_data, pct_k_period, pct_k_slowing_period, pct_d_period)
+    return pd.Series(stoch) if use_series else stoch
 
 
 def stochastic_rsi(data, period=14, smooth_pct_k=3, smooth_pct_d=3):
     """ Calculates Stochoastic RSI Courteous of @lukazbinden
-    :param ohlc:
+    :param data:
     :param period:
-    :param smoothK:
-    :param smoothD:
+    :param smooth_pct_k:
+    :param smooth_pct_d:
     :return:
     """
     # Calculate RSI
@@ -55,8 +75,8 @@ 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 - rsi.rolling(period).min()) / (rsi.rolling(period).max() - rsi.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()
\ No newline at end of file
+    return round(rsi_values, 2).tolist(), round(stochrsi_K * 100, 2).tolist(), round(stochrsi_D * 100, 2).tolist()
diff --git a/Blankly/indicators/statistics.py b/Blankly/indicators/statistics.py
index 28ae7e47..b1a67b6d 100644
--- a/Blankly/indicators/statistics.py
+++ b/Blankly/indicators/statistics.py
@@ -1,21 +1,45 @@
+from typing import Any
+from Blankly.indicators.utils import convert_to_numpy
+from Blankly.indicators.utils import check_series
 import tulipy as ti
-import numpy as np
+import pandas as pd
 
-def stdev_period(data, period=14) -> np.array:
-    return ti.stddev(data, period)
 
-def var_period(data, period=14) -> np.array:
-    return ti.var(data, period)
+def stdev_period(data, period=14, use_series=False) -> Any:
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)    
+    stddev = ti.stddev(data, period)
+    return pd.Series(stddev) if use_series else stddev
 
-def stderr_period(data, period=14) -> np.array:
-    return ti.stderr(data, period)
 
-def min_period(data, period) -> np.array:
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.min(data, period)
+def var_period(data, period=14, use_series=False) -> Any:
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    var = ti.var(data, period)
+    return pd.Series(var) if use_series else var
 
-def max_period(data, period) -> np.array:
-    if type(data) == list:
-        data = np.asarray(data)
-    return ti.max(data, period)
\ No newline at end of file
+
+def stderr_period(data, period=14, use_series=False) -> Any:
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    stderr = ti.stderr(data, period)
+    return pd.Series(stderr) if use_series else stderr
+
+
+def min_period(data, period, use_series=False) -> Any:
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    minimum = ti.min(data, period)
+    return pd.Series(minimum) if use_series else minimum
+
+
+def max_period(data, period, use_series=False) -> Any:
+    if check_series(data):
+        use_series = True
+    data = convert_to_numpy(data)
+    maximum = ti.max(data, period)
+    return pd.Series(maximum) if use_series else maximum
diff --git a/Blankly/indicators/utils.py b/Blankly/indicators/utils.py
index 5594e906..df66907e 100644
--- a/Blankly/indicators/utils.py
+++ b/Blankly/indicators/utils.py
@@ -1,6 +1,19 @@
+from typing import Any
 import pandas as pd
-import tulipy as ti
 import numpy as np
 
-def to_historical_returns(data):
-    return pd.series(data).diff().tolist()
\ No newline at end of file
+
+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
similarity index 100%
rename from Blankly/Interface_README.md
rename to Blankly/interface/README.md
diff --git a/Blankly/interface/abc_currency_interface.py b/Blankly/interface/abc_currency_interface.py
index bae78103..e11cbc12 100644
--- a/Blankly/interface/abc_currency_interface.py
+++ b/Blankly/interface/abc_currency_interface.py
@@ -137,7 +137,7 @@ def get_order(self, currency_id, order_id) -> dict:
         pass
 
     @abc.abstractmethod
-    def get_fees(self):
+    def get_fees(self) -> dict:
         """
         Get market fees
         TODO add return example
diff --git a/Blankly/interface/currency_Interface.py b/Blankly/interface/currency_Interface.py
index 63bb08d1..1eaa4487 100644
--- a/Blankly/interface/currency_Interface.py
+++ b/Blankly/interface/currency_Interface.py
@@ -16,7 +16,6 @@
     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
@@ -24,11 +23,11 @@
 
 # TODO: need to add a cancel all orders function
 class CurrencyInterface(ICurrencyInterface, abc.ABC):
-    def __init__(self, exchange_name, authenticated_API):
+    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()
+        self.user_preferences = utils.load_user_preferences(preferences_path)
 
         # TODO, improve creation of its own properties
         self.exchange_properties = None
@@ -135,6 +134,7 @@ def init_exchange(self):
         pass
 
     """ Needs to be overridden here """
+
     def get_calls(self):
         """
         Returns:
@@ -144,6 +144,7 @@ def get_calls(self):
         return self.calls
 
     """ Needs to be overridden here """
+
     def get_exchange_type(self):
         return self.exchange_name
 
diff --git a/Blankly/interface/currency_factory.py b/Blankly/interface/currency_factory.py
index 59936efe..8ec18870 100644
--- a/Blankly/interface/currency_factory.py
+++ b/Blankly/interface/currency_factory.py
@@ -1,15 +1,15 @@
 from Blankly.exchanges.Alpaca.alpaca_api_interface import AlpacaInterface
 from Blankly.exchanges.Coinbase_Pro.Coinbase_Pro_API import API as Coinbase_Pro_API
 from binance.client import Client
-from Blankly.exchanges.Alpaca.Alpaca_API import API as Alpaca_API
+from Blankly.exchanges.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
-from Blankly.auth.auth_factory import AuthFactory
+import Blankly.utils.utils as utils
 
 class InterfaceFactory:
-
     @staticmethod
-    def create_interface(exchange_name: str, preferences, auth):
+    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],
@@ -31,6 +31,5 @@ def create_interface(exchange_name: str, preferences, auth):
             return BinanceInterface(exchange_name, calls)
 
         elif exchange_name == 'alpaca':
-            # TODO: Fix the hardcoded true
-            calls = Alpaca_API(auth, True)
-            return AlpacaInterface(calls)
\ No newline at end of file
+            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/portfolio.py b/Blankly/metrics/portfolio.py
index 90680217..1fbc28fe 100644
--- a/Blankly/metrics/portfolio.py
+++ b/Blankly/metrics/portfolio.py
@@ -1,56 +1,67 @@
 import pandas as pd
 import numpy as np
 
+
 def cagr(start_value, end_value, years):
     return (end_value / start_value) ** (1 / years)
 
+
 def cum_returns(start_value, end_value):
     return (start_value - end_value) / start_value
 
-def sortino(returns, days=252, risk_free_rate=None):
+
+def sortino(returns, n=252, risk_free_rate=None):
     returns = pd.Series(returns)
     if risk_free_rate:
-        mean = returns.mean() * days - risk_free_rate
+        mean = returns.mean() - risk_free_rate
     else:
-        mean = returns.mean() * days
-    std_neg = returns[returns < 0].std() * np.sqrt(days)
-    return mean / std_neg
+        mean = returns.mean()
+    std_neg = returns[returns < 0].std() 
+    return mean / std_neg * np.sqrt(n)
 
-def sharpe(returns, days=252, risk_free_rate=None):
+
+def sharpe(returns, n=252, risk_free_rate=None):
     returns = pd.Series(returns)
     if risk_free_rate:
-        mean = returns.mean() * days - risk_free_rate
+        mean = returns.mean() - risk_free_rate
     else:
-        mean = returns.mean() * days
-    std = returns.std() * np.sqrt(days)
-    return mean / std
+        mean = returns.mean()
+    std = returns.std() 
+    return mean / std * np.sqrt(n)
+
 
-def calmar(returns, days=252):
+def calmar(returns, n=252):
     return_series = pd.Series(returns)
-    return return_series.mean() * days / abs(max_drawdown(return_series))
+    return return_series.mean() * np.sqrt(n) / abs(max_drawdown(return_series))
+
 
-def volatility(returns, days=None):
-    return np.std(returns) * np.sqrt(days) if days else np.std(returns)
+def volatility(returns, n=None):
+    return np.std(returns) * np.sqrt(n) if n else np.std(returns)
+
+
+def variance(returns, n=None):
+    return np.var(returns) * np.sqrt(n) if n else np.var(returns)
 
-def variance(returns, days=None):
-    return np.var(returns) * np.sqrt(days) if days else np.var(returns)
 
 def beta(returns, market_base_returns):
     m = np.matrix([returns, market_base_returns])
     return np.cov(m)[0][1] / np.std(market_base_returns)
 
-def var(returns, alpha: float):
+
+def var(initial_value, returns, alpha: float):
     returns_sorted = np.sort(returns)
     index = int(alpha * len(returns_sorted))
-    return abs(returns_sorted[index])
+    return initial_value * abs(returns_sorted[index])
 
-def cvar(returns, alpha):
+
+def cvar(initial_value, returns, alpha):
     returns_sorted = np.sort(returns)
     index = int(alpha * len(returns_sorted))
     sum_var = returns_sorted[0]
     for i in range(1, index):
         sum_var += returns_sorted[i]
-    return abs(sum_var / index)
+    return initial_value * abs(sum_var / index)
+
 
 def max_drawdown(returns):
     return_series = pd.Series(returns)
diff --git a/Blankly/strategy/__init__.py b/Blankly/strategy/__init__.py
index 32c6cbec..cff3959b 100644
--- a/Blankly/strategy/__init__.py
+++ b/Blankly/strategy/__init__.py
@@ -1,2 +1,3 @@
 from Blankly.strategy.order import *
-from Blankly.strategy.strategy_base import *
\ No newline at end of file
+from Blankly.strategy.strategy_base import *
+from Blankly.strategy.strategy_state import *
\ No newline at end of file
diff --git a/Blankly/strategy/cookbook/recipes.py b/Blankly/strategy/cookbook/recipes.py
new file mode 100644
index 00000000..a0e3f819
--- /dev/null
+++ b/Blankly/strategy/cookbook/recipes.py
@@ -0,0 +1,38 @@
+import Blankly
+
+
+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)[
+        -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)
\ No newline at end of file
diff --git a/Blankly/strategy/order.py b/Blankly/strategy/order.py
index 4eca0950..7379dc77 100644
--- a/Blankly/strategy/order.py
+++ b/Blankly/strategy/order.py
@@ -19,15 +19,16 @@
 
 class Order:
     """Order Type"""
-    def __init__(self, order_type, side, size, price=None):
+    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))
         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))
         self.side = side
-        if self.size <= 0:
+        if self.amount <= 0:
             raise ValueError("Size must be greater than zero, but was given: {}".format(size))
+        self.amount = amount
         if self.type == 'limit' and price == None:
             raise ValueError("Limit orders require a price but None was given")
 
diff --git a/Blankly/strategy/strategy_base.py b/Blankly/strategy/strategy_base.py
index 07646732..8b9a2eaf 100644
--- a/Blankly/strategy/strategy_base.py
+++ b/Blankly/strategy/strategy_base.py
@@ -15,23 +15,36 @@
     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.currency_pair = currency_pair
+        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()
 
-        self.Schedulers = []
+        # 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):
         """
@@ -42,17 +55,30 @@ def add_price_event(self, callback: typing.Callable, currency_pair: str, resolut
             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(
+            self.__schedulers.append(
                 Blankly.Scheduler(self.__price_event_websocket, resolution,
-                                  callback=callback, currency_pair=currency_pair)
+                                  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, callback=callback, currency_pair=currency_pair)
+            # 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):
@@ -61,19 +87,48 @@ def __idle_event(self):
         """
         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)
-        callback(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)
-        callback(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 add_orderbook_event(self, callback: typing.Callable, currency_pair: str):
         """
@@ -83,46 +138,130 @@ def add_orderbook_event(self, callback: typing.Callable, currency_pair: str):
             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(self.__idle_event, currency_id=currency_pair)
+        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
-        self.Schedulers.append(
-            Blankly.Scheduler(self.__orderbook_event_websocket, 1, callback=callback, currency_pair=currency_pair)
-        )
+        # 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']
 
-        price = self.Orderbook_Manager.get_most_recent_tick(override_currency=currency_pair)
-        callback(price, currency_pair)
-
-    def backtest(self, asset_id: str, price_data: pd.DataFrame):
+        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.
-        Make sure the price events use a paper trade object unless you want actual live trading
 
         Args:
-            asset_id (str): Asset ID of the price passed in - such as "BTC-USD"
-            price_data (dataframe): Price dataframe from blankly historical download
+            ** 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
         """
 
-        # Ensure that the current exchange is a backtesting exchange
-        backtesting_exchange = self.exchange
-        if not self.exchange.get_type() == "paper_trade":
-            backtesting_exchange = Blankly.PaperTrade(backtesting_exchange)
+        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:
+        for i in self.__schedulers:
             kwargs = i.get_kwargs()
-            backtesting_exchange.append_backtest_price_event(callback=i.get_callback(),
-                                                             asset_id=kwargs['currency_pair'],
-                                                             time_interval=i.get_interval()
-                                                             )
-
-        # Append the price data just passed in
-        backtesting_exchange.append_backtest_price_data(asset_id, price_data)
+            backtesting_controller.append_backtest_price_event(callback=kwargs['callback'],
+                                                               asset_id=kwargs['currency_pair'],
+                                                               time_interval=i.get_interval()
+                                                               )
 
         # Run the backtest & return results
-        return backtesting_exchange.backtest()
+        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
new file mode 100644
index 00000000..39f71210
--- /dev/null
+++ b/Blankly/strategy/strategy_state.py
@@ -0,0 +1,30 @@
+"""
+    Class to pass in the current Strategy State
+    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/>.
+"""
+
+
+from Blankly.utils.utils import AttributeDict
+from Blankly import Interface
+
+class StrategyState:
+    """Strategy State"""
+    def __init__(self, strategy, interface: Interface, variables: AttributeDict, resolution: str = None):
+        self.strategy = strategy
+        self.interface = interface
+        self.variables = variables
+        self.resolution = resolution
+
diff --git a/Blankly/strategy/strategy_usage_example.py b/Blankly/strategy/strategy_usage_example.py
index a499af8a..6e519185 100644
--- a/Blankly/strategy/strategy_usage_example.py
+++ b/Blankly/strategy/strategy_usage_example.py
@@ -1,24 +1,54 @@
+from Blankly.strategy.order import Order
 import Blankly
-from Blankly.strategy.strategy_base import Strategy
+from Blankly.strategy.strategy_base import Strategy, StrategyState
 
-prices = []
 
+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
 
-def price_event(price, currency_pair):
-    prices.append(price)
-    sma50 = Blankly.analysis.calculate_sma(price, window=10)
-    if price > sma50:
-        interface.market_order(currency_pair, 'buy', 10)
-    else:
-        interface.market_order(currency_pair, 'sell', 10)
+    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__":
-    # Authenticate coinbase pro strategy
     coinbase_pro = Blankly.Coinbase_Pro()
-    interface = coinbase_pro.get_interface()
+    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')
 
-    coinbase_strategy.add_price_event(price_event, currency_pair='BTC-USD', resolution='15m')
-    coinbase_strategy.add_price_event(price_event, currency_pair='XLM-USD', resolution='15m')
-    coinbase_strategy.add_price_event(price_event, currency_pair='COMP-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/exceptions.py b/Blankly/utils/exceptions.py
index 9808bac2..b41a075d 100644
--- a/Blankly/utils/exceptions.py
+++ b/Blankly/utils/exceptions.py
@@ -1,6 +1,6 @@
 """
     Exceptions definitions for Blankly
-    Copyright (c) 2017 Daniel Paquin, modified by Emerson Dove
+    Copyright (c) 2021 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
diff --git a/Blankly/utils/paper_trading/local_account/trade_local.py b/Blankly/utils/paper_trading/local_account/trade_local.py
index 9dadd502..72390fd4 100644
--- a/Blankly/utils/paper_trading/local_account/trade_local.py
+++ b/Blankly/utils/paper_trading/local_account/trade_local.py
@@ -17,6 +17,7 @@
 """
 import Blankly.utils.paper_trading.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:
@@ -38,22 +39,22 @@ def trade_local(currency_pair, side, base_delta, quote_delta) -> None:
 
     # Push these abstracted deltas to the local account
     try:
-        local_account.account[base] = local_account.account[base] + base_delta
+        local_account.account[base]['available'] = local_account.account[base]['available'] + base_delta
     except KeyError:
         raise KeyError("Base currency specified not found in local account")
 
     try:
-        local_account.account[quote] = local_account.account[quote] + quote_delta
+        local_account.account[quote]['available'] = local_account.account[quote]['available'] + quote_delta
     except KeyError:
         raise KeyError("Quote currency specified not found in local account")
 
 
-def init_local_account(currencies) -> None:
+def init_local_account(currencies: dict) -> None:
     """
     Create the local account to paper trade with.
 
     Args:
-        currencies: (dict) with key/value pairs such as {'BTC': 2.3, 'USD': 4352, 'XLM': 32}
+        currencies: (dict) with key/value pairs such as {'BTC': {'available: 2.3, 'hold': 0}}
     """
     local_account.account = currencies
 
@@ -70,18 +71,36 @@ def test_trade(currency_pair, side, qty, quote_price) -> bool:
     """
     if side == 'buy':
         quote = utils.get_quote_currency(currency_pair)
-        current_funds = local_account.account[quote]
+        account = local_account.account[quote]
+        current_funds = account['available']
         purchase_funds = quote_price * qty
 
         # If you have more funds than the purchase requires then return true
-        return current_funds > purchase_funds
+        if current_funds > purchase_funds:
+            return True
+        else:
+            raise InvalidOrder("Insufficient funds. Available:" +
+                               str(current_funds) +
+                               " hold: " +
+                               str(account['hold']) +
+                               " requested: " +
+                               str(qty) + ".")
 
     elif side == 'sell':
         base = utils.get_base_currency(currency_pair)
-        current_base = local_account.account[base]
+        account = local_account.account[base]
+        current_base = account['available']
 
         # If you have more base than the sell requires then return true
-        return current_base > qty
+        if current_base > qty:
+            return True
+        else:
+            raise InvalidOrder("Not enough base currency. Available: " +
+                               str(current_base) +
+                               ". hold: " +
+                               str(account['hold']) +
+                               ". requested: " +
+                               str(qty) + ".")
 
     else:
         raise LookupError("Invalid purchase side")
@@ -92,3 +111,18 @@ def get_accounts() -> dict:
     Get the paper trading local account
     """
     return local_account.account
+
+
+def get_account(asset_id) -> dict:
+    """
+    Get a single account under an asset id
+    """
+    return local_account.account[asset_id]
+
+
+def update_available(asset_id, new_value):
+    local_account.account[asset_id]['available'] = new_value
+
+
+def update_hold(asset_id, new_value):
+    local_account.account[asset_id]['hold'] = new_value
diff --git a/Blankly/utils/paper_trading/utils.py b/Blankly/utils/paper_trading/utils.py
index 59696590..feba1929 100644
--- a/Blankly/utils/paper_trading/utils.py
+++ b/Blankly/utils/paper_trading/utils.py
@@ -26,4 +26,4 @@ def generate_coinbase_pro_id():
     coinbase_pro_id = coinbase_pro_id[:18] + '-' + coinbase_pro_id[18:]
     coinbase_pro_id = coinbase_pro_id[:23] + '-' + coinbase_pro_id[23:]
 
-    return coinbase_pro_id
\ No newline at end of file
+    return coinbase_pro_id
diff --git a/Blankly/utils/purchases/order.py b/Blankly/utils/purchases/order.py
index 65822ea2..a936e156 100644
--- a/Blankly/utils/purchases/order.py
+++ b/Blankly/utils/purchases/order.py
@@ -60,7 +60,7 @@ def get_id(self) -> str:
         """
         return self.__response["id"]
 
-    def get_product_id(self):
+    def get_product_id(self) -> str:
         """
         Get the asset id of the exchange
         """
@@ -101,4 +101,4 @@ def get_side(self) -> str:
         """
         Get the order side such as buy/sell as a str.
         """
-        return self.__response["side"]
\ No newline at end of file
+        return self.__response["side"]
diff --git a/Blankly/utils/scheduler.py b/Blankly/utils/scheduler.py
index 42ebc884..7f656918 100644
--- a/Blankly/utils/scheduler.py
+++ b/Blankly/utils/scheduler.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/>.
 """
+import warnings
 
 from Blankly.utils.time_builder import time_interval_to_seconds
 import threading
@@ -23,27 +24,28 @@
 
 
 class Scheduler:
-    def __init__(self, function, interval, **kwargs):
+    def __init__(self, function, interval, initially_stopped=False, **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)
 
         # Stop flag
         self.__stop = False
-
-        self.__thread = threading.Thread(target=self.__threading_wait, args=(function, interval, kwargs))
-        self.__thread.start()
+        self.__thread = None
 
         self.__interval = interval
         self.__kwargs = kwargs
         self.__callback = function
 
+        if not initially_stopped:
+            self.start()
+
         # This can be used for a statically typed, single-process bot decorator.
         # Leaving this because it probably will be used soon
 
@@ -55,6 +57,20 @@ def __init__(self, function, interval, **kwargs):
         #     wrapper()
         # return decorator
 
+    def start(self, force=False):
+        """
+        Start the scheduler.
+
+        Args:
+            force: Override multi-thread protection and create more threads of the scheduler callback
+        """
+        if self.__thread is None or force:
+            self.__thread = threading.Thread(target=self.__threading_wait,
+                                             args=(self.__callback, self.__interval, self.__kwargs))
+            self.__thread.start()
+        else:
+            warnings.warn("Scheduler already started and force not enabled...skipping start.")
+
     def get_interval(self):
         """
         Get the seconds between each scheduler run
diff --git a/Blankly/utils/time_builder.py b/Blankly/utils/time_builder.py
index baaebce7..11019bdf 100644
--- a/Blankly/utils/time_builder.py
+++ b/Blankly/utils/time_builder.py
@@ -17,43 +17,43 @@
 """
 
 
-def build_second():
+def build_second() -> int:
     return 1
 
 
-def build_minute():
+def build_minute() -> int:
     return build_second() * 60
 
 
-def build_hour():
+def build_hour() -> int:
     return build_minute() * 60
 
 
-def build_day():
+def build_day() -> int:
     return build_hour() * 24
 
 
-def build_week():
+def build_week() -> int:
     return build_day() * 7
 
 
-def build_month():
+def build_month() -> int:
     return build_day() * 30
 
 
-def build_year():
+def build_year() -> int:
     return build_day() * 365
 
 
-def build_decade():
+def build_decade() -> int:
     return build_year() * 10
 
 
-def build_century():
+def build_century() -> int:
     return build_decade() * 10
 
 
-def build_millennium():
+def build_millennium() -> int:
     return build_century() * 10
 
 
diff --git a/Blankly/utils/utils.py b/Blankly/utils/utils.py
index 5db9e453..f7c9f7e0 100644
--- a/Blankly/utils/utils.py
+++ b/Blankly/utils/utils.py
@@ -21,6 +21,7 @@
 import json
 import numpy
 import warnings
+import sys
 
 from sklearn.linear_model import LinearRegression
 
@@ -48,33 +49,98 @@ def __compare_dicts(default_settings, user_settings):
 
 
 settings_cache = None
+settings_path_override = None
 
-default_settings = {
+backtest_cache = None
+backteset_path_override = None
+
+# Copy of settings to compare defaults vs overrides
+default_global_settings = {
     "settings": {
         "account_update_time": 5000,
-        "use_sandbox": True,
+        "use_sandbox": False,
+        "use_sandbox_websockets": False,
         "binance_tld": "us",
         "websocket_buffer_size": 100000,
     }
 }
 
+default_backtest_settings = {
+    "use_price": "close",
+    "smooth_prices": False,
+    "GUI_output": True,
+    "show_tickers_with_zero_delta": False,
+    "save_initial_account_value": False,
+    "show_progress_during_backtest": False,
+    "cache_location": "./price_caches"
+}
+
+
+def load_json_file(override_path=None):
+    f = open(override_path, )
+    json_file = json.load(f)
+    f.close()
+    return json_file
+
+
+def load_backtest_preferences(override_path=None) -> dict:
+    global backtest_cache
+    global backteset_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
+    elif override_path is not None:
+        backteset_path_override = override_path
+
+    if backtest_cache is None:
+        try:
+            if override_path is None:
+                preferences = load_json_file('backtest.json')
+            else:
+                preferences = load_json_file(override_path)
+        except FileNotFoundError:
+            raise FileNotFoundError("To perform a backtest, make sure a backtest.json file is placed in the same "
+                                    "folder as the project working directory!")
+        # Just compare the settings because everything else could be chaotic
+        preferences['settings'] = __compare_dicts(default_backtest_settings, preferences['settings'])
+        backtest_cache = preferences
+        return preferences
+    else:
+        return backtest_cache
+
+
+def write_backtest_preferences(json_file, override_path=None):
+    global backtest_cache
+    backtest_cache = json_file
+
+    if override_path is not None:
+        f = open(override_path, "w")
+    else:
+        f = open('backtest.json', "w")
+    f.write(json.dumps(json_file, indent=2))
+
 
-def load_user_preferences(override_path=None):
+def load_user_preferences(override_path=None) -> dict:
     global settings_cache
+    global settings_path_override
+
+    # Allow this override to be evaluated anywhere
+    if override_path is None and settings_path_override is not None:
+        override_path = settings_path_override
+    elif override_path is not None:
+        settings_path_override = override_path
+
     if settings_cache is None:
         try:
             if override_path is None:
-                f = open("Settings.json", )
-                preferences = json.load(f)
-                f.close()
+                preferences = load_json_file('settings.json')
             else:
-                f = open(override_path, )
-                preferences = json.load(f)
-                f.close()
+                preferences = load_json_file(override_path)
         except FileNotFoundError:
-            raise FileNotFoundError("Make sure a Settings.json file is placed in the same folder as the project "
+            raise FileNotFoundError("Make sure a settings.json file is placed in the same folder as the project "
                                     "working directory!")
-        preferences = __compare_dicts(default_settings, preferences)
+        preferences = __compare_dicts(default_global_settings, preferences)
         settings_cache = preferences
         return preferences
     else:
@@ -90,11 +156,11 @@ def pretty_print_JSON(json_object):
     return out
 
 
-def epoch_from_ISO8601(ISO8601):
+def epoch_from_ISO8601(ISO8601) -> float:
     return DP.parse(ISO8601).timestamp()
 
 
-def ISO8601_from_epoch(epoch):
+def ISO8601_from_epoch(epoch) -> str:
     return DT.datetime.utcfromtimestamp(epoch).isoformat() + 'Z'
 
 
@@ -144,23 +210,23 @@ def fit_parabola(ticker, point_number):
     return numpy.polyfit(times, prices, 2, full=True)
 
 
-def to_blankly_coin_id(coin_id, exchange, quote_currency=None) -> str:
+def to_blankly_coin_id(asset_id, exchange, quote_guess=None) -> str:
     if exchange == "binance":
-        if quote_currency is not None:
-            index = int(coin_id.find(quote_currency))
-            coin_id = coin_id[0:index]
-            return coin_id + "-" + quote_currency
+        if quote_guess is not None:
+            index = int(asset_id.find(quote_guess))
+            asset_id = asset_id[0:index]
+            return asset_id + "-" + quote_guess
         else:
             # Try your best to try to parse anyway
             quotes = ['BNB', 'BTC', 'TRX', 'XRP', 'ETH', 'USDT', 'BUSD', 'AUD', 'BRL', 'EUR', 'GBP', 'RUB',
                       'TRY', 'TUSD', 'USDC', 'PAX', 'BIDR', 'DAI', 'IDRT', 'UAH', 'NGN', 'VAI', 'BVND']
             for i in quotes:
-                if __check_ending(coin_id, i):
-                    return to_blankly_coin_id(coin_id, 'binance', quote_currency=i)
-            raise LookupError("Unable to parse binance coin id of: " + str(coin_id))
+                if __check_ending(asset_id, i):
+                    return to_blankly_coin_id(asset_id, 'binance', quote_guess=i)
+            raise LookupError("Unable to parse binance coin id of: " + str(asset_id))
 
     if exchange == "coinbase_pro":
-        return coin_id
+        return asset_id
 
 
 def __check_ending(full_string, checked_ending) -> bool:
@@ -209,6 +275,7 @@ def rename_to(keys_array, renaming_dictionary):
         del mutated_dictionary[i[0]]
     return mutated_dictionary
 
+
 # Non-recursive check
 def isolate_specific(needed, compare_dictionary):
     """
@@ -314,3 +381,35 @@ def compare_dictionaries(dict1, dict2) -> bool:
         print(dict1)
         print(dict2)
         return False
+
+
+def update_progress(progress):
+    # 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'.
+    # A value at 1 or bigger represents 100%
+    bar_length = 10  # Modify this to change the length of the progress bar
+    status = ""
+    if isinstance(progress, int):
+        progress = float(progress)
+    if not isinstance(progress, float):
+        progress = 0
+        status = "error: progress var must be float\r\n"
+    if progress < 0:
+        progress = 0
+        status = "Halt...\r\n"
+    if progress >= 1:
+        progress = 1
+        status = "Done...\r\n"
+    block = int(round(bar_length * progress))
+    text = "\rProgress: [{0}] {1}% {2}".format("#" * block + "-" * (bar_length - block), round(progress * 100, 2),
+                                               status)
+    sys.stdout.write(text)
+    sys.stdout.flush()
+
+
+class AttributeDict(dict):
+    def __getattr__(self, attr):
+        return self[attr]
+    def __setattr__(self, attr, value):
+        self[attr] = value
diff --git a/Examples/basic_example.py b/Examples/basic_example.py
index 9d05ac3f..aab3e7da 100644
--- a/Examples/basic_example.py
+++ b/Examples/basic_example.py
@@ -16,9 +16,12 @@ def price_event(price, currency_pair):
     interface = coinbase_pro.get_interface()
 
     # Use our strategy helper on coinbase pro
-    coinbase_strategy = Blankly.StrategyHelper(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')
+
+    # 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 100%
rename from Examples/Keys_Example.json
rename to Examples/keys_example.json
diff --git a/Examples/modeling_example.py b/Examples/modeling_example.py
index 58c224c6..9510c928 100644
--- a/Examples/modeling_example.py
+++ b/Examples/modeling_example.py
@@ -18,11 +18,17 @@
                                             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(x, 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(x, 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
@@ -34,4 +40,3 @@
     p.legend.title_text_font_style = "bold"
     p.legend.title_text_font_size = "20px"
     show(p)
-
diff --git a/Examples/multicore_bot.py b/Examples/multicore_bot.py
index c6e22483..dffb6515 100644
--- a/Examples/multicore_bot.py
+++ b/Examples/multicore_bot.py
@@ -36,9 +36,9 @@ def price_event(self, tick):
         This is called on price update events from the ticker feeds
         """
         # Example of updating the price state for the GUI
-        self.update_state("Price", tick["price"])
+        self.update_state("Price", str(tick["price"]))
         # Show these new ticks to the console
-        print("New price tick at: " + tick["price"])
+        print("New price tick at: " + str(tick["price"]))
 
 
 if __name__ == "__main__":
diff --git a/Examples/settings.json b/Examples/settings.json
new file mode 100644
index 00000000..cb6cc007
--- /dev/null
+++ b/Examples/settings.json
@@ -0,0 +1,9 @@
+{
+  "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 9ca6a556..1c33f5cb 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,17 @@
-![Blankly_Github_Logo](https://blankly.finance/remote_hosting/logo-dark.svg)
+![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)
+
 
 ## What is it?
 
 ​	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.
 
+Try it out: `pip install blankly`
 
 
 Check out our [website](http://blankly.finance).
@@ -50,6 +54,12 @@ Check out our [website](http://blankly.finance).
 
 - Status management for purchases
 
+- Create strategies using an event-based framework
+
+- Instantly backtest your strategies (`strategy.backtest(to='1y')`)
+
+- Automatic price downloads & caching for use in backtests
+
 - Interface that allows calls to each supported exchange to be identical:
 
   > Buy example for Coinbase Pro and Binance:
@@ -70,7 +80,6 @@ Check out our [website](http://blankly.finance).
 
 ## Coming Features
 
-- Backtesting (`bot.backtest(time_frame)`)
 - Deployment (`> Blankly push bot/*`)
 - Online hosting
 
@@ -109,97 +118,141 @@ Check out our [website](http://blankly.finance).
 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.
 
-## What is it doing?
+[comment]: <> (## What is it doing?)
+
+[comment]: <> (The comments offer a decent amount of description for the behavior, but here is a broader overview:)
 
-The comments offer a decent amount of description for the behavior, but here is a broader overview:
+[comment]: <> (​	The motivation behind this is to **allow full independence** between each bot, but still **giving it the ability to report** back to the main thread easily. The setup runs by specifying three increasingly specific things about the behavior we want:)
 
-​	The motivation behind this is to **allow full independence** between each bot, but still **giving it the ability to report** back to the main thread easily. The setup runs by specifying three increasingly specific things about the behavior we want:
+[comment]: <> (1. We first **declare** that we want to run on a certain `exchange`, such as Coinbase Pro or Binance. This is done with &#40;for example&#41; `Blankly.Coinbase_Pro&#40;&#41;`)
 
-1. We first **declare** that we want to run on a certain `exchange`, such as Coinbase Pro or Binance. This is done with (for example) `Blankly.Coinbase_Pro()`
+[comment]: <> (   - Documentation refers to this as the `exchange`)
 
-   - Documentation refers to this as the `exchange`
+[comment]: <> (   - ```python)
 
-   - ```python
-     if __name__ == "__main__":
-         """
-         Easily setup and run a model on any exchange
-         """
+[comment]: <> (     if __name__ == "__main__":)
+
+[comment]: <> (         """)
+
+[comment]: <> (         Easily setup and run a model on any exchange)
+
+[comment]: <> (         """)
      
-         # This creates an authenticated exchange. Now we can append models.
-         exchange = Blankly.Coinbase_Pro()
-         # Imagine this:
-         #   Coinbase Pro <-- Choosing to assign this bot to this exchange
-         #   Kraken
-         #   Binance
-     ```
-
-2. We initialize the bot object. This creates a boilerplate bot that isn't attached or running on anything yet.
-
-   - ```python
-     # Create the bot.
-     bot=Bot()
-     ```
-
-3. This same function is also **attached** to a `portfolio` within the exchange. Each portfolio has access to each `currency` on the `exchange`. This means that each portfolio is independent from the other. **You can tell it which portfolio you want** by naming it in the `Keys.json` file and then declaring the `portfolio_name` argument to match the same name in `Keys.json`
-
-   - Documentation refers to this set of currencies as a `portfolio`
-
-   - ```python
-     # Add it to run as the coinbase_pro bitcoin model
-     exchange.append_model(bot, "BTC-USD")
-     # Imagine this:
-     #   Coinbase Pro:
-     #       Bitcoin
-     #       Ethereum
-     #       Stellar
-     #       The Graph <-- Added to the data from this currency
-     ```
-
-4. The code above also **declares** the `currency` that we want it to run on within the `portfolio`. The bot is attached to this currency and is provided default ways to interact with the exchange.
-
-   - Documentation calls this the `currency`. Bots are by default **not** currency specific because this dramatically enhances portability.
-
-5. We then ask the model to start. By default this iterates through all the attached models and queries them to start but you can also specify a particular currency to begin executing the bot attached to it.
-
-   - ```python
-     # Begins running the main() function of the model on a different process
-     exchange.start_models()
-     # Imagine this:
-     #   Coinbase Pro:
-     #       Bitcoin
-     #       Ethereum
-     #       Stellar
-     #       The Graph <-- Bot <-- Asking to start
-     ```
-
-6. The bot then starts the main class. The example updates the "heartbeat" value every second. The main thread then reads this and prints it along with some exchange information about that currency.
-
-   - Some default, pre-authenticaed objects are provided to quick start interact with the exchange:
-
-     > self.Interface: allows API through the Blankly exchange interface. The interface object is already authenticated, so the calls are ready to go!
-
-     > self.Ticker_Manager: Allows easy access to a websocket ticker. The actual ticker object can be pulled by self.Ticker_Manager.get_ticker(). This offers all kinds of functionality. See the docs for more information. By default this will be calling the `price_event` function.
-
-   - Main thread calling:
-
-   - ```python
-     # Now other processes can be created or just continue with this one.
-     while True:
-        # Print the state every 2 seconds
-        state = exchange.get_full_state("BTC-USD")
-        Blankly.utils.pretty_print_JSON(state)
-        time.sleep(1)
-     ```
-
-   - Bot state updates:
-
-   - ```python
-     while True:
-        # This demonstrates a way to change the state. The default script just reports the state on this currency.
-        # Increment heartbeat value by one every second
-        self.update_state("Heartbeat", self.get_state()["Heartbeat"] + 1)
-        time.sleep(1)
-     ```
+[comment]: <> (         # This creates an authenticated exchange. Now we can append models.)
+
+[comment]: <> (         exchange = Blankly.Coinbase_Pro&#40;&#41;)
+
+[comment]: <> (         # Imagine this:)
+
+[comment]: <> (         #   Coinbase Pro <-- Choosing to assign this bot to this exchange)
+
+[comment]: <> (         #   Kraken)
+
+[comment]: <> (         #   Binance)
+
+[comment]: <> (     ```)
+
+[comment]: <> (2. We initialize the bot object. This creates a boilerplate bot that isn't attached or running on anything yet.)
+
+[comment]: <> (   - ```python)
+
+[comment]: <> (     # Create the bot.)
+
+[comment]: <> (     bot=Bot&#40;&#41;)
+
+[comment]: <> (     ```)
+
+[comment]: <> (3. This same function is also **attached** to a `portfolio` within the exchange. Each portfolio has access to each `currency` on the `exchange`. This means that each portfolio is independent from the other. **You can tell it which portfolio you want** by naming it in the `Keys.json` file and then declaring the `portfolio_name` argument to match the same name in `Keys.json`)
+
+[comment]: <> (   - Documentation refers to this set of currencies as a `portfolio`)
+
+[comment]: <> (   - ```python)
+
+[comment]: <> (     # Add it to run as the coinbase_pro bitcoin model)
+
+[comment]: <> (     exchange.append_model&#40;bot, "BTC-USD"&#41;)
+
+[comment]: <> (     # Imagine this:)
+
+[comment]: <> (     #   Coinbase Pro:)
+
+[comment]: <> (     #       Bitcoin)
+
+[comment]: <> (     #       Ethereum)
+
+[comment]: <> (     #       Stellar)
+
+[comment]: <> (     #       The Graph <-- Added to the data from this currency)
+
+[comment]: <> (     ```)
+
+[comment]: <> (4. The code above also **declares** the `currency` that we want it to run on within the `portfolio`. The bot is attached to this currency and is provided default ways to interact with the exchange.)
+
+[comment]: <> (   - Documentation calls this the `currency`. Bots are by default **not** currency specific because this dramatically enhances portability.)
+
+[comment]: <> (5. We then ask the model to start. By default this iterates through all the attached models and queries them to start but you can also specify a particular currency to begin executing the bot attached to it.)
+
+[comment]: <> (   - ```python)
+
+[comment]: <> (     # Begins running the main&#40;&#41; function of the model on a different process)
+
+[comment]: <> (     exchange.start_models&#40;&#41;)
+
+[comment]: <> (     # Imagine this:)
+
+[comment]: <> (     #   Coinbase Pro:)
+
+[comment]: <> (     #       Bitcoin)
+
+[comment]: <> (     #       Ethereum)
+
+[comment]: <> (     #       Stellar)
+
+[comment]: <> (     #       The Graph <-- Bot <-- Asking to start)
+
+[comment]: <> (     ```)
+
+[comment]: <> (6. The bot then starts the main class. The example updates the "heartbeat" value every second. The main thread then reads this and prints it along with some exchange information about that currency.)
+
+[comment]: <> (   - Some default, pre-authenticaed objects are provided to quick start interact with the exchange:)
+
+[comment]: <> (     > self.Interface: allows API through the Blankly exchange interface. The interface object is already authenticated, so the calls are ready to go!)
+
+[comment]: <> (     > self.Ticker_Manager: Allows easy access to a websocket ticker. The actual ticker object can be pulled by self.Ticker_Manager.get_ticker&#40;&#41;. This offers all kinds of functionality. See the docs for more information. By default this will be calling the `price_event` function.)
+
+[comment]: <> (   - Main thread calling:)
+
+[comment]: <> (   - ```python)
+
+[comment]: <> (     # Now other processes can be created or just continue with this one.)
+
+[comment]: <> (     while True:)
+
+[comment]: <> (        # Print the state every 2 seconds)
+
+[comment]: <> (        state = exchange.get_full_state&#40;"BTC-USD"&#41;)
+
+[comment]: <> (        Blankly.utils.pretty_print_JSON&#40;state&#41;)
+
+[comment]: <> (        time.sleep&#40;1&#41;)
+
+[comment]: <> (     ```)
+
+[comment]: <> (   - Bot state updates:)
+
+[comment]: <> (   - ```python)
+
+[comment]: <> (     while True:)
+
+[comment]: <> (        # This demonstrates a way to change the state. The default script just reports the state on this currency.)
+
+[comment]: <> (        # Increment heartbeat value by one every second)
+
+[comment]: <> (        self.update_state&#40;"Heartbeat", self.get_state&#40;&#41;["Heartbeat"] + 1&#41;)
+
+[comment]: <> (        time.sleep&#40;1&#41;)
+
+[comment]: <> (     ```)
 
      
 
@@ -222,7 +275,7 @@ The comments offer a decent amount of description for the behavior, but here is
 * Interface calls take ~300 µs extra to homogenize the exchange data.
 
 ## Docs
-https://edove.gitbook.io/blankly/
+https://docs.blankly.finance
 
 ## Bugs
 
@@ -230,7 +283,8 @@ Please report any bugs or issues in Github's Issues page.
 
 ## Disclaimer 
 
-Trading is risky. We are not responsible for losses incurred using this software.
+Trading is risky. We are not responsible for losses incurred using this software, software fitness for any particular purpose, or responsibility for any issues or bugs.
+This is free software.
 
 ## Contributing
 
diff --git a/setup.py b/setup.py
index 1269dec3..d3bd8453 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@
     name='Blankly',  # How you named your package folder (MyLib)
     packages=find_packages(),
     # packages=['Blankly'],  # Potentially should be the same thing as name
-    version='v0.1.11-alpha',
+    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
     long_description=long_description,
@@ -35,7 +35,9 @@
         'python-binance',
         'tulipy',
         'alpaca-trade-api',
-        'pytest'
+        'pytest',
+        'bokeh',
+        'pytest-mock'
     ],
     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 2fa61768..edf39e2c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -4,43 +4,35 @@
 # Avoiding these tests currently because they mess with clone history.
 
 #name: Blankly Module Tests
-#
-#on:
-#  push:
-#    branches: [ main ]
-#  pull_request:
-#    branches: [ main ]
-#
-#jobs:
-#  build:
-#
-#    runs-on: ubuntu-latest
-#
-#    steps:
-#    - uses: actions/checkout@v2
-#    - name: Set up Python 3.7
-#      uses: actions/setup-python@v2
-#      with:
-#        python-version: 3.7
-#
-#    - name: Install dependencies
-#      run: |
-#        python -m pip install --upgrade pip
-#        pip install -r ./Blankly/utils/tests/requirements.txt
-#        cp ./Blankly/utils/tests/tests.py .
-#        cp ./Blankly/utils/tests/Settings.json .
-#
-#    - name: Inject Keys
-#      run: |
-#        CBP_API_KEY=${{ secrets.CBP_API_KEY }}
-#        CBP_API_SECRET=${{ secrets.CBP_API_SECRET }}
-#        CBP_API_PASS=${{ secrets.CBP_API_PASS }}
-#
-#        keys_file=$(eval "echo \"$(cat ./Blankly/utils/tests/keys_template.json)\"")
-#        echo $keys_file >> ./Keys.json
-#      shell: bash
-#
-#
-#    - name: Run test file
-#      run: |
-#        python ./tests.py
+
+on:
+  push:
+    branches:
+      - main
+      - development
+      - *
+  pull_request:
+    branches:
+      - main
+      - development
+
+jobs:
+  build:
+
+    runs-on: ubuntu-latest
+
+    steps:
+    - uses: actions/checkout@v2
+    - name: Set up Python 3.7
+      uses: actions/setup-python@v2
+      with:
+        python-version: 3.7
+
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install -e .
+
+    - name: Run test file
+      run: |
+        pytest
diff --git a/Blankly/exchanges/Paper_Trade/backtest_controller.py b/Blankly/exchanges/Paper_Trade/backtest_controller.py
index 8b170deb..e20e37ae 100644
--- a/Blankly/exchanges/Paper_Trade/backtest_controller.py
+++ b/Blankly/exchanges/Paper_Trade/backtest_controller.py
@@ -17,56 +17,177 @@
 """
 
 
+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 get_blank_settings():
-    """
-    This is a format draft for multi-equity backtesting
-    """
-    return {
-        'price_event': [{
-                'function': None,  # Run interval in seconds
-                'interval': 0.0,  # Run interval in seconds
-                'price_data': None  # Price data for this specific function
-            }],
-        'orderbook_event': [{
-            'function': None,  # Run interval in seconds
-            'interval': 0.0,  # Run interval in seconds
-            'price_data': None,
-            'orderbook_data': None,
-            }],
-        'price_dictionary': None,  # Set a global price dictionary for all functions to use
-
-        'interval_enabled': {  # Choose between passing in a price dictionary or a
-            # linear list with internally modified time
-            'enabled': True  # True will mean interval, which means price data should be lists
-        },
-        'Use_Price': 'close',
-        'Assume_Linear_Prices': False,
-        'GUI_Output': True,
-        'Show_Tickers_With_Zero_Delta': False,
-    }
+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_interface: PaperTradeInterface, prices: dict, price_events=None):
-        if price_events is None:
-            price_events = []  # [[price_event, asset_id, time_interval_to_seconds(interval)]]
-        self.price_events = price_events
-        self.paper_trade_interface = paper_trade_interface
-        self.current_time = 0
-        self.prices = []
+    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 this currently only works with open & iterrows() is allegedly pretty slow
-                self.prices.append([row.time, k, row.open])
+                # 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])
 
@@ -82,9 +203,15 @@ def __init__(self, paper_trade_interface: PaperTradeInterface, prices: dict, pri
                 'next_run': self.initial_time
             }
 
-    def run(self):
-        self.paper_trade_interface.set_backtesting(True)
-        account = self.paper_trade_interface.get_account()
+        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'])
@@ -93,29 +220,36 @@ def run(self):
         # 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 price_array in self.prices:
-                self.paper_trade_interface.receive_price(price_array[1], price_array[2])
+            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.paper_trade_interface.receive_time(self.current_time)
+                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.paper_trade_interface.get_price(function_dict['asset_id']),
+                        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 = {}
-                        # Grab the account status
-                        account_status = self.paper_trade_interface.get_account()
-                        for i in account_status:
-                            available_dict[i['currency']] = i['available']
-                        # Make sure to add the time key in
-                        available_dict['time'] = local_time - self.initial_time
+                        available_dict = self.format_account_data(local_time)
                         price_data.append(available_dict)
         except Exception:
             traceback.print_exc()
@@ -123,24 +257,38 @@ def run(self):
         # Push the accounts to the dataframe
         cycle_status = cycle_status.append(price_data, ignore_index=True)
 
-        p = figure(plot_width=800, plot_height=900)
-        color = Category10_10.__iter__()
-        for column in cycle_status:
-            if column != 'time':
-                p.step(cycle_status['time'].tolist(), 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 = "Backtest results"
-        p.legend.title_text_font_style = "bold"
-        p.legend.title_text_font_size = "20px"
-        show(p)
-
-        self.paper_trade_interface.set_backtesting(False)
-        return cycle_status
+        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/Examples/backtest.json b/Examples/backtest.json
new file mode 100644
index 00000000..e74e0d17
--- /dev/null
+++ b/Examples/backtest.json
@@ -0,0 +1,27 @@
+{
+  "price_data": {
+    "assets": [
+      [
+        "BTC-USD",
+        1621726015,
+        1622590015,
+        3600.0
+      ],
+      [
+        "BTC-USD",
+        1591389962.5150762,
+        1622925962.515081,
+        3600.0
+      ]
+    ]
+  },
+  "settings": {
+    "use_price": "close",
+    "smooth_prices": false,
+    "GUI_output": true,
+    "show_tickers_with_zero_delta": false,
+    "save_initial_account_value": false,
+    "show_progress_during_backtest": false,
+    "cache_location": "./price_caches"
+  }
+}
\ No newline at end of file
diff --git a/Examples/backtesting_example.py b/Examples/backtesting_example.py
index 8b9d354b..2a7efdde 100644
--- a/Examples/backtesting_example.py
+++ b/Examples/backtesting_example.py
@@ -1,10 +1,20 @@
 import Blankly
-import time
 
 
 def price_event(price, product_id):
-    # Run this every "2 minutes" in the backtest
-    paper_trade_interface.market_order(product_id, 'buy', 10)
+    # 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
+
+    delta = price-usd_amount
+    if delta > 10:
+        strategy.Interface.market_order(product_id, 'sell', delta)
+    if delta < -10:
+        strategy.Interface.market_order(product_id, 'buy', -1*delta)
 
 
 if __name__ == "__main__":
@@ -15,33 +25,20 @@ def price_event(price, product_id):
     # Create an authenticated coinbase pro object
     coinbase_pro = Blankly.Coinbase_Pro()
 
-    # Wrap the coinbase pro object with a paper trade object so that no orders go through
-    paper_trade = Blankly.PaperTrade(coinbase_pro)
-
-    # Get the interface so it can be used above to trade
-    paper_trade_interface = paper_trade.get_interface()
+    # Create a strategy object
+    strategy = Blankly.Strategy(coinbase_pro)
 
-    """
-    Download price data
-    """
-    print("Downloading...")
-    hist = paper_trade_interface.get_product_history('BTC-USD',
-                                                     time.time() - Blankly.time_builder.build_day() * 3,  # 3 days before now
-                                                     time.time(),  # To now
-                                                     Blankly.time_builder.build_hour())  # At one hour resolution
-
-    # Optionally save the prices instead of downloading each time. The price_data argument supports a path
-    # price_path = './backtesting_data.csv'
-    # hist.to_csv(price_path)
     """
     Backtest
     """
     print("Backtesting...")
-    # Add the price data to the backtest
-    paper_trade.append_backtest_price_data(asset_id='BTC-USD', price_data=hist)
-
-    # Add the price event to the backtest
-    paper_trade.append_backtest_price_event(price_event, 'BTC-USD', '2m')
-
-    # Run the backtest
-    print(paper_trade.backtest())
+    # Add the function to the strategy class
+    strategy.add_price_event(price_event, 'BTC-USD', resolution='1h')
+
+    # The backtest function will now turn our strategy class into a class that can be backtested
+    print(strategy.backtest(to='1y',
+                            initial_values={
+                                            'BTC': 100000,
+                                            'USD': 100000
+                                            }
+                            ))
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/config/keys.json b/tests/config/keys.json
new file mode 100644
index 00000000..0c092ef5
--- /dev/null
+++ b/tests/config/keys.json
@@ -0,0 +1,21 @@
+{
+    "coinbase_pro": {
+        "alpaca test portfolio": {
+            "API_KEY": "******************************",
+            "API_SECRET": "**************************************************************************************",
+            "API_PASS": "**********"
+        }
+    },
+    "binance": {
+        "alpaca test portfolio": {
+            "API_KEY": "**************************************************************",
+            "API_SECRET": "*************************************************************"
+        }
+    },
+    "alpaca": {
+        "alpaca test portfolio": {
+            "API_KEY": "**************************************************************",
+            "API_SECRET": "*************************************************************"
+        }
+    }
+}
\ No newline at end of file
diff --git a/Examples/Settings.json b/tests/config/settings.json
similarity index 100%
rename from Examples/Settings.json
rename to tests/config/settings.json
diff --git a/tests/exchanges/__init__.py b/tests/exchanges/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/exchanges/alpaca/__init__.py b/tests/exchanges/alpaca/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/exchanges/alpaca/alpaca_api_interface_test.py b/tests/exchanges/alpaca/alpaca_api_interface_test.py
index e55ac1d9..0d4cd146 100644
--- a/tests/exchanges/alpaca/alpaca_api_interface_test.py
+++ b/tests/exchanges/alpaca/alpaca_api_interface_test.py
@@ -1,7 +1,159 @@
+from unittest.mock import patch
+
 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
+import pathlib
+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
 
-def test_alpaca_interface():
-    auth_obj = alpaca_auth("/Users/arunannamalai/Documents/Blankly/Examples/keys.json", "paper account")
-    alpaca_interface = InterfaceFactory.create_interface("alpaca", None, auth_obj)
+    assert "exchange_specific" in return_val[0]
 
diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/helpers/comparisons.py b/tests/helpers/comparisons.py
new file mode 100644
index 00000000..464ce70d
--- /dev/null
+++ b/tests/helpers/comparisons.py
@@ -0,0 +1,6 @@
+def is_sub_dict(dict1: dict, dict2: dict) -> bool:
+    for key in dict1:
+        if dict1[key] != dict2.get(key):
+            return False
+
+    return True