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

Issue:
Avi Kucoin Integration



Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 67e1c062..6d5245ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -142,4 +142,8 @@ notify.json
 .pytype/
 
 # Cython debug symbols
-cython_debug/
\ No newline at end of file
+cython_debug/
+
+# User-Specific Files
+*.code-workspace
+
diff --git a/README.md b/README.md
index 360c9555..e863dbb1 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,6 @@
 
 <p align="center">
    <a target="_blank" href="https://sonarcloud.io/dashboard?id=Blankly-Finance_Blankly"><img src="https://sonarcloud.io/api/project_badges/measure?project=Blankly-Finance_Blankly&metric=sqale_rating"></a>
-   <a target="_blank" href="https://sonarcloud.io/dashboard?id=Blankly-Finance_Blankly"><img src="https://sonarcloud.io/api/project_badges/measure?project=Blankly-Finance_Blankly&metric=security_rating"></a>
    <a target="_blank" href="https://sonarcloud.io/dashboard?id=Blankly-Finance_Blankly"><img src="https://sonarcloud.io/api/project_badges/measure?project=Blankly-Finance_Blankly&metric=alert_status"></a>
    <a target="_blank" href="https://github.com/Blankly-Finance/Blankly/actions/workflows/test.yml"><img src="https://github.com/Blankly-Finance/Blankly/actions/workflows/test.yml/badge.svg?branch=main"></a> <br>
    <a target="_blank" href="https://pepy.tech/project/blankly"><img src="https://pepy.tech/badge/blankly/month"></a>
@@ -37,13 +36,16 @@
 
 ## Why Blankly? 
 
-Blankly is an elegant python library for interacting with crypto, stocks, and forex for in a consistent and streamlined way. Now, no more reading API or struggling to get data. Blankly offers a powerful feature-set, optimized for speed and ease of use, better backtesting, and ultimately better models. 
+​	Blankly is a live trading engine, backtest runner and development framework wrapped into one powerful open source package. Models can be instantly backtested, paper traded, sandbox tested and run live by simply changing a single line. We built blankly for every type of quant including training & running ML models in the same environment, cross-exchange/cross-symbol arbitrage, and even long/short positions on stocks (all with built-in websockets).
 
-We're bridging the gap between local development systems & live APIs by building a framework which allows backtesting, 
-paper trading, sandbox testing, and live cross-exchange deployment without modifying a single line of trading logic.
+​	Convert your existing model or build a new one - unlock the ability to run & optimize across all of our supported exchanges. Getting started is easy - just `pip install blankly` and `blankly init`.
 
 Check out our [website](https://blankly.finance) and our [docs](https://docs.blankly.finance).
 
+[YouTube - Under 25 Lines Build an Alpaca RSI Trading Bot](https://youtu.be/pcm0h63rhUU)
+<a target="_blank" href="https://youtu.be/pcm0h63rhUU"><img src="https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/github%2Fbuild_a_bot_readme_thumbnail.png?alt=media&token=5193b40b-c949-46df-b385-8214474dc26a" style="border-radius:10px"></a>
+
+
 ### Trade Stocks, Crypto, and Forex Seamlessly
 
 ```python
@@ -84,7 +86,7 @@ strategy.backtest(to='1y')
 #### Accurate Backtest Holdings
 
 <div align="center">
-  <img style="margin: 0 auto; padding-bottom: 15px; padding-top: 30px" width=100%" src="https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/screely-1631725027541.png?alt=media&token=1446fcf0-680b-4ee9-afcf-fbaea044eba7">
+    <a><img src="https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/screely-1631725027541.png?alt=media&token=1446fcf0-680b-4ee9-afcf-fbaea044eba7" style="border-radius:10px"></a>
 </div>
 
 #### Useful Metrics
@@ -131,7 +133,7 @@ $ blankly init
 ```
 This will initialize your working directory.
 
-The command will create the files `keys.json`, `settings.json`, `backtest.json`, `deploy.json` and an example script called `bot.py`.
+The command will create the files `keys.json`, `settings.json`, `backtest.json`, `blankly.json` and an example script called `bot.py`.
 
 If you don't want to use our `init` command, you can find the same files in the `examples` folder under [`settings.json`](https://github.com/Blankly-Finance/Blankly/blob/main/examples/settings.json) and [`keys_example.json`](https://github.com/Blankly-Finance/Blankly/blob/main/examples/keys_example.json)
 
@@ -141,7 +143,7 @@ More information can be found on our [docs](https://docs.blankly.finance)
 
 ### Directory format
 
-The working directory format should have at least these files:
+The working directory format should have *at least* these files:
 ```
 Project
    |-bot.py
@@ -162,16 +164,18 @@ For more info, and ways to do more advanced things, check out our [getting start
 
 ## Supported Exchanges
 
-| Exchange     | Integrate | Websockets  | Paper Trading | Backtesting |
-| ------------ | ------------ | ---------------- | ---------- | --------- |
-| Coinbase Pro | 🟢           | 🟢               | 🟢        | 🟢        |
-| Binance      | 🟢           | 🟢               | 🟢        | 🟢       |
-| Alpaca       | 🟢         | 🟢             | 🟢      | 🟢        |
-| OANDA | 🟢 | 🟡 | 🟢 | 🟢 |
-| FTX | 🔴 | 🔴 | 🔴 | 🔴 |
-| TD Ameritrade | 🔴 | 🔴 | 🔴 | 🔴 |
-| Webull | 🔴 | 🔴 | 🔴 | 🔴 |
-| Robinhood | 🔴 | 🔴 | 🔴 | 🔴 |
+| Exchange      | Live Trading | Websockets | Paper Trading | Backtesting |
+| ------------- | ------------ | ---------- | ------------- | ----------- |
+| Coinbase Pro  | 🟢            | 🟢          | 🟢             | 🟢           |
+| Binance       | 🟢            | 🟢          | 🟢             | 🟢           |
+| Alpaca        | 🟢            | 🟢          | 🟢             | 🟢           |
+| OANDA         | 🟢            | 🟡          | 🟢             | 🟢           |
+| FTX           | 🟢            | 🟢          | 🟢             | 🟢           |
+| KuCoin        | 🟢            | 🟡          | 🟢             | 🟢           |
+| Kraken        | 🟡            | 🟡          | 🟡             | 🟡           |
+| TD Ameritrade | 🔴            | 🔴          | 🔴             | 🔴           |
+| Webull        | 🔴            | 🔴          | 🔴             | 🔴           |
+| Robinhood     | 🔴            | 🔴          | 🔴             | 🔴           |
 
 
 🟢  = working
@@ -180,8 +184,6 @@ For more info, and ways to do more advanced things, check out our [getting start
 
 🔴  = planned but not yet in development
 
-* Interface calls take ~300 µs extra to homogenize the exchange data.
-
 ## RSI Example
 
 We have a pre-built cookbook examples that implement strategies such as RSI, MACD, and the Golden Cross found in our [examples](https://docs.blankly.finance/examples/golden-cross).
diff --git a/blankly/PENDING.md b/blankly/PENDING.md
index ca9bfd7c..3ca3dfe9 100644
--- a/blankly/PENDING.md
+++ b/blankly/PENDING.md
@@ -6,3 +6,20 @@
 - Add `count_decimals` to the utils
 - get_price_derivative has been removed due to sklearn dependency
 - `fit_parabola` has been removed due to just being old 
+- Pass epoch to start_date end_date in .backtest()
+- Modified __init__ headers in strategy to remove the symbol arg
+
+
+
+## Requirements for new CONTRIBUTING.md
+- Redo [CONTRIBUTING.md](http://CONTRIBUTING.md) with good contributions
+  - Upgrade caching system
+  - Order PNL from a list of blankly orders
+  - How to get started integrating your exchange
+- Show that blankly only requires
+  - Scheduling
+  - Order execution
+  - Price data
+  - To take advantage of the full features
+  
+Run programmatic screeners across entire markets with rich results
\ No newline at end of file
diff --git a/blankly/__init__.py b/blankly/__init__.py
index 56210f22..67778626 100644
--- a/blankly/__init__.py
+++ b/blankly/__init__.py
@@ -20,6 +20,8 @@
 from blankly.exchanges.interfaces.binance.binance import Binance
 from blankly.exchanges.interfaces.alpaca.alpaca import Alpaca
 from blankly.exchanges.interfaces.oanda.oanda import Oanda
+from blankly.exchanges.interfaces.kucoin.kucoin import Kucoin
+from blankly.exchanges.interfaces.ftx.ftx import FTX
 from blankly.exchanges.interfaces.paper_trade.paper_trade import PaperTrade
 from blankly.frameworks.strategy import Strategy as Strategy
 from blankly.frameworks.strategy import StrategyState as StrategyState
diff --git a/blankly/deployment/api.py b/blankly/deployment/api.py
index 5148d4f9..43fa9ac3 100644
--- a/blankly/deployment/api.py
+++ b/blankly/deployment/api.py
@@ -16,9 +16,12 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
+import json
+
 import requests
+from blankly.utils.utils import info_print
 
-blankly_frontend_api_url = "http://localhost:8081"
+blankly_deployment_url = 'https://deploy.blankly.finance'
 
 
 class API:
@@ -26,7 +29,7 @@ def __init__(self, token, override_url: str = None):
         if override_url:
             self.url = override_url
         else:
-            self.url = blankly_frontend_api_url
+            self.url = blankly_deployment_url
 
         self.token = None
         self.auth_data = self.exchange_token(token)
@@ -61,7 +64,7 @@ def __request(self, type_: str, route: str, json_: dict = None, params: dict = N
 
         # Add the token if we have it
         if self.token is not None:
-            kwargs['headers'] = {'token': self.token, 'uid': self.user_id}
+            kwargs['headers'] = {'token': self.token}
 
         try:
             if type_ == "get":
@@ -75,6 +78,11 @@ def __request(self, type_: str, route: str, json_: dict = None, params: dict = N
         except requests.exceptions.ConnectionError:
             raise requests.exceptions.ConnectionError("Failed to connect to deployment service.")
 
+        # Show info that the request was not authorized but still
+        #  allow the process to continue
+        if out.status_code == 401:
+            info_print("Unauthorized request.")
+
         try:
             return out.json()
         except ValueError:
@@ -91,10 +99,10 @@ def get_details(self, model_id: str):
         """
         Get the details route
         """
-        return self.__request('get', 'model/details', data={'modelId': model_id})
+        return self.__request('post', 'model/details', data={'modelId': model_id})
 
-    def get_status(self, model_id: str):
-        return self.__request('get', 'model/status', data={'modelId': model_id})
+    def get_status(self):
+        return self.__request('get', 'model/status')
 
     def list_projects(self):
         return self.__request('get', 'project/list')
@@ -104,21 +112,47 @@ def get_plans(self, type_: str):
         Args:
             type_: Can be 'backtesting' or 'live'
         """
-        return self.__request('get', 'project/plans', data={'type': type_})
+        return self.__request('post', 'project/plans', data={'type': type_})
 
     def create_project(self, name: str, description: str):
         return self.__request('post', 'project/create', data={'name': name,
                                                               'description': description})
 
-    def deploy(self, file_path: str, plan: str, project_id, description: str, name: str):
+    def deploy(self, file_path: str, plan: str, project_id, model_id: str,
+               general_description: str, version_description: str, name: str, create_new: bool,
+               python_version: float):
         file_path = r'{}'.format(file_path)
         file = {'model': open(file_path, 'rb')}
         return self.__request('post', 'model/deploy', file=file, data={'plan': plan,
                                                                        'name': name,
+                                                                       'modelId': model_id,
                                                                        'projectId': project_id,
-                                                                       'description': description})
-
-    def backtest(self, project_id: str, model_id: str, args: dict):
-        return self.__request('post', 'model/backtest', data={'project_id': project_id,
-                                                              'model_id': model_id,
-                                                              'args': args})
+                                                                       'generalDescription': general_description,
+                                                                       'versionDescription': version_description,
+                                                                       'createNew': create_new,
+                                                                       'pythonVersion': str(python_version)})
+
+    def backtest_deployed(self, project_id: str, model_id: str, args: dict, version_id: str, backtest_description: str):
+        return self.__request('post', 'model/backtestUploadedModel',
+                              json_={'projectId': project_id,
+                                     'modelId': model_id,
+                                     'versionId': version_id,
+                                     'backtestArgs': args,
+                                     'backtestDescription': backtest_description})
+
+    def backtest(self, file_path: str, project_id: str, model_id: str, args: dict, plan: str,
+                 create_new: bool, name: str, python_version: float, backtest_description: str = ""):
+        file_path = r'{}'.format(file_path)
+        file = {'model': open(file_path, 'rb')}
+        return self.__request('post', 'model/backtest', file=file,
+                              data={'projectId': project_id,
+                                    'modelId': model_id,
+                                    'plan': plan,
+                                    'backtestDescription': backtest_description,
+                                    'backtestArgs': json.dumps(args),
+                                    'createNew': create_new,
+                                    'name': name,
+                                    'pythonVersion': str(python_version)})
+
+    def signal(self):
+        return self.__request('get', 'model/signalTest')
diff --git a/blankly/deployment/cli.py b/blankly/deployment/cli.py
index 49764258..2d000b37 100644
--- a/blankly/deployment/cli.py
+++ b/blankly/deployment/cli.py
@@ -18,6 +18,7 @@
 
 import argparse
 import sys
+import uuid
 import warnings
 import os
 import platform
@@ -32,7 +33,6 @@
 from blankly.deployment.api import API
 from blankly.utils.utils import load_json_file, info_print
 
-
 very_important_string = """
 ██████╗ ██╗      █████╗ ███╗   ██╗██╗  ██╗██╗  ██╗   ██╗    ███████╗██╗███╗   ██╗ █████╗ ███╗   ██╗ ██████╗███████╗
 ██╔══██╗██║     ██╔══██╗████╗  ██║██║ ██╔╝██║  ╚██╗ ██╔╝    ██╔════╝██║████╗  ██║██╔══██╗████╗  ██║██╔════╝██╔════╝
@@ -70,83 +70,200 @@ def choose_option(choice: str, options: list, descriptions: list):
             miocro:
                 CPU: 5,
                 RAM: 10
-        '
-        ]
+        ']
     """
-    import fcntl
-    import termios
-    fd = sys.stdin.fileno()
-
-    largest_option_name = 0
-    for i in options:
-        if len(i) > largest_option_name:
-            largest_option_name = len(i)
-
-    def print_selection_index(index_):
-        # We pass none when they haven't selected yet
-        if index_ is None:
-            chosen_plan = "None"
-        # When its given a number it will index and find the lengths
-        else:
-            if index_ < 0:
-                index_ = len(options) - 1
-            elif index_ >= len(options):
-                index_ = 0
-            chosen_plan = options[index_]
 
-        # Carry it in this string
-        string_ = "\r" + TermColors.UNDERLINE + TermColors.OKCYAN + "You have chosen:" + \
-                  TermColors.ENDC + " " + TermColors.BOLD + TermColors.OKBLUE + chosen_plan
+    def print_descriptions(descriptions_: list, show_index: bool):
+        index_ = 0
+        for j in descriptions_:
+            if show_index:
+                j = TermColors.ENDC + str(index_) + ": " + j
+            print(j)
+            index_ += 1
 
-        remaining_chars = largest_option_name - len(chosen_plan)
-        string_ += " " * remaining_chars
+    try:
+        import fcntl
+        import termios
+        fd = sys.stdin.fileno()
+
+        largest_option_name = 0
+        for i in options:
+            if len(i) > largest_option_name:
+                largest_option_name = len(i)
+
+        def print_selection_index(index_):
+            # We pass none when they haven't selected yet
+            if index_ is None:
+                chosen_plan = "None"
+            # When its given a number it will index and find the lengths
+            else:
+                if index_ < 0:
+                    index_ = len(options) - 1
+                elif index_ >= len(options):
+                    index_ = 0
+                chosen_plan = options[index_]
 
-        # Final print and flush the buffer
-        sys.stdout.write(string_)
-        sys.stdout.flush()
+            # Carry it in this string
+            string_ = "\r" + TermColors.UNDERLINE + TermColors.OKCYAN + "You have chosen: " + \
+                      TermColors.ENDC + " " + TermColors.BOLD + TermColors.OKBLUE + chosen_plan
 
-        return index_
+            remaining_chars = largest_option_name - len(chosen_plan)
+            string_ += " " * remaining_chars
 
-    print(TermColors.BOLD + TermColors.WARNING + f"Choose a {choice}: " + TermColors.ENDC +
-          TermColors.UNDERLINE + "(Use your arrow keys ← →)" + TermColors.ENDC)
-    for i in descriptions:
-        print(i)
+            # Final print and flush the buffer
+            sys.stdout.write(string_)
+            sys.stdout.flush()
 
-    # TODO Add a very simple version that can take simple input() if this fails
-    oldterm = termios.tcgetattr(fd)
-    newattr = termios.tcgetattr(fd)
-    newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
-    termios.tcsetattr(fd, termios.TCSANOW, newattr)
+            return index_
 
-    oldflags = fcntl.fcntl(fd, fcntl.F_GETFL)
-    fcntl.fcntl(fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)
+        print(TermColors.BOLD + TermColors.WARNING + f"Choose a {choice}: " + TermColors.ENDC +
+              TermColors.UNDERLINE + "(Use your arrow keys ← →)" + TermColors.ENDC)
 
-    index = 0
-    print_selection_index(None)
-    try:
-        while True:
-            try:
-                c = sys.stdin.read(1)
-                if c == '[':
+        # Print everything out with no index
+        print_descriptions(descriptions, False)
+
+        # TODO Add a very simple version that can take simple input() if this fails
+        oldterm = termios.tcgetattr(fd)
+        newattr = termios.tcgetattr(fd)
+        newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
+        termios.tcsetattr(fd, termios.TCSANOW, newattr)
+
+        oldflags = fcntl.fcntl(fd, fcntl.F_GETFL)
+        fcntl.fcntl(fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)
+
+        index = 0
+        print_selection_index(None)
+        try:
+            while True:
+                try:
                     c = sys.stdin.read(1)
-                    if c == 'C':
-                        index += 1
-                        index = print_selection_index(index)
-                    elif c == 'D':
-                        index -= 1
-                        index = print_selection_index(index)
-                elif c == '\n':
-                    break
-            except IOError:
-                pass
-    finally:
-        termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
-        fcntl.fcntl(fd, fcntl.F_SETFL, oldflags)
-
-    print('\n' + TermColors.BOLD + TermColors.WARNING + f"Chose {choice}:" + TermColors.ENDC + " " +
-          TermColors.BOLD + TermColors.OKBLUE + options[index] + TermColors.ENDC)
-
-    return options[index]
+                    if c == '[':
+                        c = sys.stdin.read(1)
+                        if c == 'C':
+                            index += 1
+                            index = print_selection_index(index)
+                        elif c == 'D':
+                            index -= 1
+                            index = print_selection_index(index)
+                    elif c == '\n':
+                        break
+                except IOError:
+                    pass
+        finally:
+            termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
+            fcntl.fcntl(fd, fcntl.F_SETFL, oldflags)
+
+        print('\n' + TermColors.BOLD + TermColors.WARNING + f"Chose {choice}:" + TermColors.ENDC + " " +
+              TermColors.BOLD + TermColors.OKBLUE + options[index] + TermColors.ENDC)
+
+        return options[index]
+    except Exception:
+        # info_print("Using non-interactive selection on this device.")
+
+        print_descriptions(descriptions, True)
+
+        print(TermColors.BOLD + TermColors.WARNING + f"Choose a {choice}: " + TermColors.ENDC +
+              TermColors.UNDERLINE + "(Input the index of your selection)" + TermColors.ENDC)
+
+        index = int(input(TermColors.UNDERLINE + TermColors.OKCYAN + "You have chosen:" + TermColors.ENDC + " "))
+
+        print('\n' + TermColors.BOLD + TermColors.WARNING + f"Chose {choice}:" + TermColors.ENDC + " " +
+              TermColors.BOLD + TermColors.OKBLUE + options[index] + TermColors.ENDC)
+
+        return options[index]
+
+
+def get_project_model_and_name(args, projects):
+    if args['path'] is None:
+        print(TermColors.WARNING + "Warning - No filepath specified. Assuming the current directory (./)\n" +
+              TermColors.ENDC)
+
+        args['path'] = './'
+
+    create_new = False
+    general_description = None
+
+    try:
+        f = open(os.path.join(args['path'], deployment_script_name))
+        deployment_options = json.load(f)
+
+        f.close()
+        if 'model_id' not in deployment_options or \
+                'project_id' not in deployment_options or \
+                'model_name' not in deployment_options:
+            # This performs all the querying necessary to send the data up
+            ids = []
+            for i in projects:
+                ids.append(i['projectId'])
+            descriptions = []
+            for i in projects:
+                descriptions.append("\t" + TermColors.BOLD + TermColors.WARNING + i['projectId'] + ": " +
+                                    TermColors.ENDC + TermColors.OKCYAN + i['name'])
+            project_id = choose_option('project', ids, descriptions)
+
+            model_name = input(TermColors.BOLD + TermColors.WARNING +
+                               "Enter a name for your model: " + TermColors.ENDC)
+
+            deployment_options['project_id'] = project_id
+            deployment_options['model_id'] = str(uuid.uuid4())
+            deployment_options['model_name'] = model_name
+
+            create_new = True
+
+            general_description = input(TermColors.BOLD + TermColors.WARNING +
+                                        "Enter a general description for this model model: " + TermColors.ENDC)
+
+            info_print(f"Generating new model id for this blankly.json: {deployment_options['model_id']}")
+            # Write the modified version with the ID back into the json file
+            f = open(os.path.join(args['path'], deployment_script_name), 'w+')
+            f.write(json.dumps(deployment_options, indent=2))
+            f.close()
+        model_name = deployment_options['model_name']
+        project_id = deployment_options['project_id']
+        model_id = deployment_options['model_id']
+    except FileNotFoundError:
+        raise FileNotFoundError(f"A {deployment_script_name} file must be present at the top level of the "
+                                f"directory specified.")
+
+    python_version = deployment_options['python_version']
+    return model_name, project_id, model_id, create_new, general_description, deployment_options, python_version
+
+
+temporary_zip_file = None
+
+
+def zip_dir(args: dict, deployment_options: dict):
+    global temporary_zip_file
+    temporary_zip_file = tempfile.TemporaryDirectory()
+    dist_directory = temporary_zip_file.__enter__()
+
+    # dist_directory = tempfile.TemporaryDirectory().__enter__()
+    # with tempfile.TemporaryDirectory() as dist_directory:
+    source = os.path.abspath(args['path'])
+
+    model_path = os.path.join(dist_directory, 'model.zip')
+    zip_ = zipfile.ZipFile(model_path, 'w', zipfile.ZIP_DEFLATED)
+    zipdir(source, zip_, deployment_options['ignore_files'])
+    zip_.close()
+
+    return model_path
+
+
+def select_plan(api: API, plan_type: str):
+    plans = api.get_plans(plan_type)
+
+    plan_names = list(plans.keys())
+
+    descriptions = []
+
+    for i in plans:
+        descriptions.append("\t" + TermColors.UNDERLINE + TermColors.OKBLUE + i + TermColors.ENDC +
+                            "\n\t\t" + TermColors.OKGREEN + 'CPU: ' + str(plans[i]['cpu']) +
+                            "\n\t\t" + TermColors.OKGREEN + 'RAM: ' + str(plans[i]['ram']) + TermColors.ENDC)
+
+    chosen_plan = choose_option('plan', plan_names, descriptions)
+
+    return chosen_plan
 
 
 def add_path_arg(arg_parser, required=True):
@@ -168,12 +285,11 @@ def create_and_write_file(filename: str, default_contents: str = None):
         if default_contents is not None:
             file.write(default_contents)
     except FileExistsError:
-        print("Already exists - skipping...")
+        print(f"{TermColors.WARNING}Already exists - skipping...{TermColors.ENDC}")
 
 
 parser = argparse.ArgumentParser(description='Blankly CLI & deployment tool.')
 
-
 subparsers = parser.add_subparsers(help='Different blankly commands.')
 
 init_parser = subparsers.add_parser('init', help='Sub command to create a blankly-enabled development environment.')
@@ -189,8 +305,9 @@ def create_and_write_file(filename: str, default_contents: str = None):
 project_create_parser = subparsers.add_parser('create', help='Create a new project.')
 project_create_parser.set_defaults(which='create')
 
-project_create_parser = subparsers.add_parser('backtest', help='Start a backtest on an uploaded model.')
-project_create_parser.set_defaults(which='backtest')
+backtest_parser = subparsers.add_parser('backtest', help='Start a backtest on an uploaded model.')
+backtest_parser.set_defaults(which='backtest')
+add_path_arg(backtest_parser, required=False)
 
 list_parser = subparsers.add_parser('list', help='Show available projects & exit.')
 list_parser.set_defaults(which='list')
@@ -203,7 +320,6 @@ def create_and_write_file(filename: str, default_contents: str = None):
 run_parser.set_defaults(which='run')
 add_path_arg(run_parser)
 
-
 # Create a global token value for use in the double nested function below
 token = None
 
@@ -223,6 +339,7 @@ def login(remove_cache: bool = False):
             # If we're not removing cache this will use the old files to look for the token
             if not remove_cache:
                 # If it's different from the one that was just created, remove the one just created
+                os.close(fd)
                 os.remove(os.path.join(temp_folder, file_name))
                 # Reassign file name just in case its needed below to write into the file
                 # Note that we protect against corrupted files below by overwriting any contents in case
@@ -316,18 +433,22 @@ def zipdir(path, ziph, ignore_files: list):
     # directories
     # ziph is zipfile handle
 
+    filtered_ignore_files = []
     # Set all the ignored files to be absolute
     for i in range(len(ignore_files)):
-        ignore_files[i] = os.path.abspath(ignore_files[i])
+        # Reject this case
+        if ignore_files[i] == "":
+            continue
+        filtered_ignore_files.append(os.path.abspath(ignore_files[i]))
 
     for root, dirs, files in os.walk(path, topdown=True):
         for file in files:
             # (Modification) Skip everything that is in the blankly_dist folder
             filepath = os.path.join(root, file)
 
-            if not os.path.abspath(filepath) in ignore_files:
+            if not (os.path.abspath(filepath) in filtered_ignore_files) and not (root in filtered_ignore_files):
                 # This takes of the first part of the relative path and replaces it with /model/
-                info_print(f'\tAdding: {file} in folder {root}.')
+                print(f'\tAdding: {file} in folder {root}.')
                 relpath = os.path.relpath(filepath,
                                           os.path.join(path, '..'))
                 relpath = os.path.normpath(relpath).split(os.sep)
@@ -347,11 +468,6 @@ def main():
         parser.print_help()
         return
     if which == 'deploy':
-        if args['path'] is None:
-            print(TermColors.WARNING + "Warning - No filepath specified. Assuming the current directory (./)\n" +
-                  TermColors.ENDC)
-
-            args['path'] = './'
         token_ = login()
 
         api = API(token_)
@@ -362,67 +478,37 @@ def main():
             print(TermColors.FAIL + "Please create a project with 'blankly create' first." + TermColors.ENDC)
             return
 
-        try:
-            f = open(os.path.join(args['path'], deployment_script_name))
-            deployment_options = json.load(f)
-        except FileNotFoundError:
-            raise FileNotFoundError(f"A {deployment_script_name} file must be present at the top level of the "
-                                    f"directory specified.")
+        # Read and write to the deployment options if necessary
+        model_name, project_id, model_id, create_new, general_description, deployment_options, python_version = \
+            get_project_model_and_name(args, projects)
 
         info_print("Zipping...")
 
-        with tempfile.TemporaryDirectory() as dist_directory:
-            source = os.path.abspath(args['path'])
-
-            model_path = os.path.join(dist_directory, 'model.zip')
-            zip_ = zipfile.ZipFile(model_path, 'w', zipfile.ZIP_DEFLATED)
-            zipdir(source, zip_, deployment_options['ignore_files'])
-
-            zip_.close()
-
-            # This performs all the querying necessary to send the data up
-            ids = []
-            for i in projects:
-                ids.append(i['projectId'])
-            descriptions = []
-            for i in projects:
-                descriptions.append("\t" + TermColors.BOLD + TermColors.WARNING + i['projectId'] + ": " +
-                                    TermColors.ENDC + TermColors.OKCYAN + i['name'])
-            project_id = choose_option('project', ids, descriptions)
-
-            plans = api.get_plans('live')
-
-            plan_names = list(plans.keys())
-
-            descriptions = []
-
-            for i in plans:
-                descriptions.append("\t" + TermColors.UNDERLINE + TermColors.OKBLUE + i + TermColors.ENDC +
-                                    "\n\t\t" + TermColors.OKGREEN + 'CPU: ' + str(plans[i]['cpu']) +
-                                    "\n\t\t" + TermColors.OKGREEN + 'RAM: ' + str(plans[i]['ram']) + TermColors.ENDC)
+        model_path = zip_dir(args, deployment_options)
 
-            chosen_plan = choose_option('plan', plan_names, descriptions)
+        chosen_plan = select_plan(api, 'live')
 
-            model_name = input(TermColors.BOLD + TermColors.WARNING +
-                               "Enter a name for your model: " + TermColors.ENDC)
+        version_description = input(TermColors.BOLD + TermColors.WARNING +
+                                    "Enter a description for this version of the model: " + TermColors.ENDC)
 
-            user_description = input(TermColors.BOLD + TermColors.WARNING +
-                                     "Enter a description for your model: " + TermColors.ENDC)
-
-            info_print("Uploading...")
-            response = api.deploy(model_path,
-                                  project_id=project_id,
-                                  plan=chosen_plan,
-                                  description=user_description,
-                                  name=model_name)
-            if 'error' in response:
-                info_print('Error: ' + response['error'])
-            elif 'status' in response and response['status'] == 'success':
-                info_print(f"Model upload completed at {response['timestamp']}:")
-                info_print(f"\tModel ID:\t{response['modelId']}")
-                info_print(f"\tVersion:\t{response['versionId']}")
-                info_print(f"\tStatus:  \t{response['status']}")
-                info_print(f"\tProject:\t{response['projectId']}")
+        info_print("Uploading...")
+        response = api.deploy(model_path,
+                              project_id=project_id,
+                              model_id=model_id,
+                              plan=chosen_plan,
+                              version_description=version_description,
+                              general_description=general_description,
+                              name=model_name,
+                              create_new=create_new,
+                              python_version=python_version)
+        if 'error' in response:
+            info_print('Error: ' + response['error'])
+        elif 'status' in response and response['status'] == 'success':
+            info_print(f"Model upload completed at {response['timestamp']}:")
+            info_print(f"\tModel ID:\t{response['modelId']}")
+            info_print(f"\tVersion:\t{response['versionId']}")
+            info_print(f"\tStatus:  \t{response['status']}")
+            info_print(f"\tProject:\t{response['projectId']}")
 
     elif which == 'init':
         print("Initializing...")
@@ -444,20 +530,26 @@ def main():
         backtest = requests.get('https://raw.githubusercontent.com/Blankly-Finance/Blankly/main/examples/backtest.json')
         create_and_write_file('backtest.json', backtest.text)
 
-        # Directly download an rsi bot
+        # Directly download a rsi bot
         print("Downloading RSI bot example...")
         bot = requests.get('https://raw.githubusercontent.com/Blankly-Finance/Blankly/main/examples/rsi.py')
         create_and_write_file('bot.py', bot.text)
 
         print("Writing deployment defaults...")
         # Interpret defaults and write to this folder
+        print("Detecting python version...")
         py_version = platform.python_version_tuple()
+        print(f"{TermColors.OKCYAN}{TermColors.BOLD}Found python version: "
+              f"{py_version[0]}.{py_version[1]}{TermColors.ENDC}")
         deploy = {
             "main_script": "./bot.py",
             "python_version": py_version[0] + "." + py_version[1],
             "requirements": "./requirements.txt",
             "working_directory": ".",
-            "ignore_files": ['']
+            "ignore_files": ['price_caches'],
+            "backtest_args": {
+                'to': '1y'
+            }
         }
         create_and_write_file(deployment_script_name, json.dumps(deploy, indent=2))
 
@@ -465,7 +557,7 @@ def main():
         print("Writing requirements.txt defaults...")
         create_and_write_file('requirements.txt', 'blankly')
 
-        print("Done!")
+        print(f"{TermColors.OKGREEN}{TermColors.UNDERLINE}Success!{TermColors.ENDC}")
 
     elif which == 'login':
         login(remove_cache=True)
@@ -485,7 +577,37 @@ def main():
     elif which == 'backtest':
         api = API(login())
 
-        print(api.backtest(project_id='jiCLm5a2EkgZBhHX1oUt', model_id='sCH0Ns9Pvow4p7T3D44v', args={'to': '1y'}))
+        projects = api.list_projects()
+
+        # Read and write to the deployment options if necessary
+        model_name, project_id, model_id, create_new, general_description, deployment_options, python_version = \
+            get_project_model_and_name(args, projects)
+
+        info_print("Zipping...")
+
+        model_path = zip_dir(args, deployment_options)
+
+        chosen_plan = select_plan(api, 'backtesting')
+
+        backtest_description = input(TermColors.BOLD + TermColors.WARNING +
+                                     "Enter a backtest description for this version of the model: " + TermColors.ENDC)
+
+        response = api.backtest(project_id=project_id,
+                                model_id=model_id,
+                                args=deployment_options['backtest_args'],
+                                backtest_description=backtest_description,
+                                plan=chosen_plan,
+                                file_path=model_path,
+                                create_new=create_new,
+                                python_version=python_version,
+                                name=model_name)
+
+        info_print("Uploading...")
+        info_print(f"Backtest upload completed at {response['timestamp']}:")
+        info_print(f"\tModel ID:\t{response['modelId']}")
+        info_print(f"\tVersion:\t{response['versionId']}")
+        info_print(f"\tStatus:  \t{response['status']}")
+        info_print(f"\tProject:\t{response['projectId']}")
 
     elif which == 'create':
         api = API(login())
diff --git a/blankly/deployment/reporter_headers.py b/blankly/deployment/reporter_headers.py
index 35b8ba70..94bccfe0 100644
--- a/blankly/deployment/reporter_headers.py
+++ b/blankly/deployment/reporter_headers.py
@@ -99,6 +99,18 @@ def log_strategy_event(self, strategy_object, event_name, response, args, **kwar
         """
         pass
 
+    def export_used_exchange(self, exchange_name):
+        """
+        Export the string identifier for a used exchange
+        """
+        pass
+
+    def export_used_symbol(self, symbol):
+        """
+        Export the currency name for a used currency
+        """
+        pass
+
     def text(self, text: str):
         """
         Send a text message to the number if notify.json OR the phone number attached to your account if the model
diff --git a/blankly/exchanges/abc_exchange_websocket.py b/blankly/exchanges/abc_exchange_websocket.py
index f4452e3c..72f5a047 100644
--- a/blankly/exchanges/abc_exchange_websocket.py
+++ b/blankly/exchanges/abc_exchange_websocket.py
@@ -23,7 +23,6 @@ class ABCExchangeWebsocket(abc.ABC):
     """
     Each of these functions are required to be implemented for interaction with the ticker manager class.
     """
-
     @abc.abstractmethod
     def is_websocket_open(self):
         pass
diff --git a/blankly/exchanges/auth/abc_auth.py b/blankly/exchanges/auth/abc_auth.py
deleted file mode 100644
index 89b861ce..00000000
--- a/blankly/exchanges/auth/abc_auth.py
+++ /dev/null
@@ -1,70 +0,0 @@
-"""
-    Inherited authentication object
-    Copyright (C) 2021  Arun Annamalai, Emerson Dove
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Lesser General Public License as published
-    by the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Lesser General Public License for more details.
-
-    You should have received a copy of the GNU Lesser General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-"""
-
-import abc
-
-from blankly.exchanges.auth.auth_constructor import load_auth
-from blankly.utils.exceptions import AuthError
-from blankly.utils.utils import info_print
-
-
-class ABCAuth(abc.ABC):
-    def __init__(self, keys_file: str, portfolio_name: str, exchange: str):
-        """
-        Create an auth interface
-        Args:
-            keys_file (str): filepath to keys.json
-            portfolio_name (str): name of portfolio
-            exchange (str): name of exchange
-        """
-        # self.portfolio_name = portfolio_name
-        self.exchange = exchange
-
-        # Load from file
-        self.portfolio_name, self.keys = load_auth(exchange, keys_file, portfolio_name)
-
-    def validate_credentials(self, needed_keys: list):
-        """
-        Args:
-            needed_keys (list): List of keys that the exchange needs in the format ['API_KEY', 'API_SECRET]
-
-        Returns:
-            A boolean that describes if the auth is good or bad (True if good).
-        """
-
-        # Create an error message template to throw if needed
-        error_message = ""
-
-        # Make a copy of the keys dict & list to avoid modifying passed variables
-        keys_dict = {**self.keys}
-        needed_keys = list.copy(needed_keys)
-        for i in needed_keys:
-            if i in keys_dict.keys():
-                keys_dict.pop(i)
-            else:
-                # Append a description header if not present
-                if error_message == "":
-                    error_message += "Error while loading authentication. Required keys for this are missing: \n"
-                error_message += str(str(i) + " is needed, but not defined.\n")
-
-        if len(keys_dict.keys()) > 0:
-            info_print(f"Additional keys for Exchange: {self.exchange} Portfolio: {self.portfolio_name} will be"
-                       f" ignored.")
-
-        if error_message != "":
-            raise AuthError(error_message)
diff --git a/blankly/exchanges/auth/auth_constructor.py b/blankly/exchanges/auth/auth_constructor.py
index 1d24e6d6..7d990a49 100644
--- a/blankly/exchanges/auth/auth_constructor.py
+++ b/blankly/exchanges/auth/auth_constructor.py
@@ -1,6 +1,6 @@
 """
-    Allows the user to load the Keys in any Keys.json.
-    Copyright (C) 2021  Emerson Dove
+    Inherited authentication object
+    Copyright (C) 2021  Arun Annamalai, Emerson Dove
 
     This program is free software: you can redistribute it and/or modify
     it under the terms of the GNU Lesser General Public License as published
@@ -16,105 +16,57 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-import json
-from blankly.utils.utils import info_print
-
-keys_path_cache = None
-
-# This is currently unused
-auth_cache = {}
-
-
-def load_json(keys_file):
-    try:
-        f = open(keys_file)
-        contents = json.load(f)
-        f.close()
-        return contents
-    except FileNotFoundError:
-        raise FileNotFoundError("Make sure a keys.json file is placed in the same folder as the project working "
-                                "directory (or specified in the exchange constructor)!")
-
-
-# def load_auth_coinbase_pro(keys_file, name):
-#     exchange_type = "coinbase_pro"
-#     name, portfolio = __load_auth(keys_file, name, exchange_type)
-#
-#     return [portfolio["API_KEY"], portfolio["API_SECRET"], portfolio["API_PASS"]], name
-#
-#
-# def load_auth_binance(keys_file, name):
-#     exchange_type = "binance"
-#     name, portfolio = __load_auth(keys_file, name, exchange_type)
-#
-#     return [portfolio["API_KEY"], portfolio["API_SECRET"]], name
-
-
-def load_auth(exchange_type, keys_file=None, name=None):
-    global keys_path_cache
+import abc
 
-    if keys_file is None:
-        if keys_path_cache is None:
-            # Add a default for if info is passed in and nobody knows anything about paths
-            keys_path_cache = './keys.json'
-            keys_file = keys_path_cache
-        else:
-            # Default to the cached path if the passed variable is wrong
-            keys_file = keys_path_cache
-    else:
-        # If its not non then there's no problem, just write it to the cache though
-        keys_path_cache = keys_file
-
-    auth_object = load_json(keys_file)
-    exchange_keys = auth_object[exchange_type]
-    if name is None:
-        name, portfolio = __determine_first_key(exchange_keys)
-    else:
-        portfolio = exchange_keys[name]
-
-    if exchange_type not in auth_cache:
-        auth_cache[exchange_type] = {}
-    return name, portfolio
-
-
-def __determine_first_key(exchange_keys):
-    first_key = list(exchange_keys.keys())[0]
-    warning_string = "No portfolio name to load specified, defaulting to the first in the file: " \
-                     "(" + first_key + "). This is fine if there is only one portfolio in use."
-    info_print(warning_string)
-    # Read the first in the portfolio
-    portfolio = exchange_keys[first_key]
-    name = first_key
-    return name, portfolio
-
-
-def write_auth_cache(exchange, name, auth):
-    """
-    Write an authenticated object into the global authentication cache. This can be used by other pieces of code
-    in the module to pull from exchanges at points they need the API
-
-    Args:
-        exchange (str): Exchange name ex: "coinbase_pro" or "binance"
-        name (str): Portfolio name ex: "my cool portfolio"
-        auth (obj): Authenticated object to store & recover.
-    """
-    global auth_cache
-    if exchange not in auth_cache:
-        auth_cache[exchange] = {}
-
-    auth_cache[exchange][name] = auth
+from blankly.exchanges.auth.utils import load_auth
+from blankly.utils.exceptions import AuthError
+from blankly.utils.utils import info_print
 
 
-def read_auth_cache(exchange, name=None):
-    """
-    Pull an authenticated object on an exchange. This can be used for API calls anywhere in the code.
-    """
-    global auth_cache
-    if exchange in auth_cache:
-        if name is None:
-            first_key = list(auth_cache[exchange])[0]
-            return auth_cache[exchange][first_key]
-        else:
-            return auth_cache[exchange][name]
-    else:
-        raise KeyError("Request exchange invalid or does not exist")
+class AuthConstructor(abc.ABC):
+    def __init__(self, keys_file: str, portfolio_name: str, exchange: str, needed_keys: list):
+        """
+        Create an auth interface
+        Args:
+            keys_file (str): filepath to keys.json
+            portfolio_name (str): name of portfolio
+            exchange (str): name of exchange
+        """
+        # self.portfolio_name = portfolio_name
+        self.exchange = exchange
+
+        # Load from file
+        self.portfolio_name, self.keys = load_auth(exchange, keys_file, portfolio_name)
+
+        self.validate_credentials(needed_keys)
+
+    def validate_credentials(self, needed_keys: list):
+        """
+        Args:
+            needed_keys (list): List of keys that the exchange needs in the format ['API_KEY', 'API_SECRET]
+
+        Returns:
+            A boolean that describes if the auth is good or bad (True if good).
+        """
+
+        # Create an error message template to throw if needed
+        error_message = ""
+
+        # Make a copy of the keys dict & list to avoid modifying passed variables
+        keys_dict = {**self.keys}
+        needed_keys = list.copy(needed_keys)
+        for i in needed_keys:
+            if i in keys_dict.keys():
+                keys_dict.pop(i)
+            else:
+                # Append a description header if not present
+                if error_message == "":
+                    error_message += "Error while loading authentication. Required keys for this are missing: \n"
+                error_message += str(str(i) + " is needed, but not defined.\n")
+
+        if len(keys_dict.keys()) > 0:
+            info_print(f"Additional keys for Exchange: {self.exchange} Portfolio: {self.portfolio_name} will be"
+                       f" ignored.")
+
+        if error_message != "":
+            raise AuthError(error_message)
diff --git a/blankly/exchanges/auth/auth_factory.py b/blankly/exchanges/auth/auth_factory.py
deleted file mode 100644
index c8e18ab0..00000000
--- a/blankly/exchanges/auth/auth_factory.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""
-    Switch system to find correct authentication method
-    Copyright (C) 2021  Arun Annamalai, Emerson Dove
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Lesser General Public License as published
-    by the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Lesser General Public License for more details.
-
-    You should have received a copy of the GNU Lesser General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-"""
-
-from blankly.exchanges.interfaces.alpaca.alpaca_auth import AlpacaAuth
-from blankly.exchanges.interfaces.binance.binance_auth import BinanceAuth
-from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_auth import CoinbaseProAuth
-from blankly.exchanges.interfaces.oanda.oanda_auth import OandaAuth
-
-
-class AuthFactory:
-    @staticmethod
-    def create_auth(keys_file, exchange_name, portfolio_name):
-        if exchange_name == 'alpaca':
-            return AlpacaAuth(keys_file, portfolio_name)
-        elif exchange_name == 'binance':
-            return BinanceAuth(keys_file, portfolio_name)
-        elif exchange_name == 'coinbase_pro':
-            return CoinbaseProAuth(keys_file, portfolio_name)
-        elif exchange_name == 'oanda':
-            return OandaAuth(keys_file, portfolio_name)
-        elif exchange_name == 'paper_trade':
-            return None
-        else:
-            raise KeyError("Exchange not supported")
diff --git a/blankly/exchanges/auth/utils.py b/blankly/exchanges/auth/utils.py
index 053aa895..80c7f177 100644
--- a/blankly/exchanges/auth/utils.py
+++ b/blankly/exchanges/auth/utils.py
@@ -1,6 +1,6 @@
 """
-    Utils for constructing authentication
-    Copyright (C) 2021  Arun Annamalai, Emerson Dove
+    Allows the user to load the Keys in any Keys.json.
+    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
@@ -16,12 +16,101 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-# TODO get rid of this file and merge with the auth constructor
-
 import json
-
 from blankly.utils.utils import info_print
 
+keys_path_cache = None
+
+# This is currently unused
+auth_cache = {}
+
+
+# def load_auth_coinbase_pro(keys_file, name):
+#     exchange_type = "coinbase_pro"
+#     name, portfolio = __load_auth(keys_file, name, exchange_type)
+#
+#     return [portfolio["API_KEY"], portfolio["API_SECRET"], portfolio["API_PASS"]], name
+#
+#
+# def load_auth_binance(keys_file, name):
+#     exchange_type = "binance"
+#     name, portfolio = __load_auth(keys_file, name, exchange_type)
+#
+#     return [portfolio["API_KEY"], portfolio["API_SECRET"]], name
+
+
+def load_auth(exchange_type, keys_file=None, name=None):
+    global keys_path_cache
+
+    if keys_file is None:
+        if keys_path_cache is None:
+            # Add a default for if info is passed in and nobody knows anything about paths
+            keys_path_cache = './keys.json'
+            keys_file = keys_path_cache
+        else:
+            # Default to the cached path if the passed variable is wrong
+            keys_file = keys_path_cache
+    else:
+        # If it's not none then there's no problem, just write it to the cache though
+        keys_path_cache = keys_file
+
+    auth_object = load_json(keys_file)
+    try:
+        exchange_keys = auth_object[exchange_type]
+    except KeyError as e:
+        raise KeyError(f"Cannot find exchange {e} the keys file.")
+    if name is None:
+        name, portfolio = __determine_first_key(exchange_keys)
+    else:
+        portfolio = exchange_keys[name]
+
+    if exchange_type not in auth_cache:
+        auth_cache[exchange_type] = {}
+    return name, portfolio
+
+
+def __determine_first_key(exchange_keys):
+    first_key = list(exchange_keys.keys())[0]
+    warning_string = "No portfolio name to load specified, defaulting to the first in the file: " \
+                     "(" + first_key + "). This is fine if there is only one portfolio in use."
+    info_print(warning_string)
+    # Read the first in the portfolio
+    portfolio = exchange_keys[first_key]
+    name = first_key
+    return name, portfolio
+
+
+def write_auth_cache(exchange, name, auth):
+    """
+    Write an authenticated object into the global authentication cache. This can be used by other pieces of code
+    in the module to pull from exchanges at points they need the API
+
+    Args:
+        exchange (str): Exchange name ex: "coinbase_pro" or "binance"
+        name (str): Portfolio name ex: "my cool portfolio"
+        auth (obj): Authenticated object to store & recover.
+    """
+    global auth_cache
+    if exchange not in auth_cache:
+        auth_cache[exchange] = {}
+
+    auth_cache[exchange][name] = auth
+
+
+def read_auth_cache(exchange, name=None):
+    """
+    Pull an authenticated object on an exchange. This can be used for API calls anywhere in the code.
+    """
+    global auth_cache
+    if exchange in auth_cache:
+        if name is None:
+            first_key = list(auth_cache[exchange])[0]
+            return auth_cache[exchange][first_key]
+        else:
+            return auth_cache[exchange][name]
+    else:
+        raise KeyError("Request exchange invalid or does not exist")
+
 
 def load_json(keys_file):
     try:
diff --git a/blankly/exchanges/exchange.py b/blankly/exchanges/exchange.py
index 82e3b308..35569c02 100644
--- a/blankly/exchanges/exchange.py
+++ b/blankly/exchanges/exchange.py
@@ -20,32 +20,54 @@
 
 import blankly
 from blankly.exchanges.abc_exchange import ABCExchange
-from blankly.exchanges.auth.auth_constructor import write_auth_cache
-from blankly.exchanges.auth.auth_factory import AuthFactory
+from blankly.exchanges.auth.utils import write_auth_cache
 from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface
-from blankly.exchanges.interfaces.binance.binance_interface import BinanceInterface
 from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_interface import CoinbaseProInterface
-from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory
+from blankly.exchanges.interfaces.oanda.oanda_interface import OandaInterface
+from blankly.exchanges.interfaces.ftx.ftx_interface import FTXInterface
+from blankly.exchanges.interfaces.alpaca.alpaca_interface import AlpacaInterface
+from blankly.exchanges.interfaces.binance.binance_interface import BinanceInterface
+from blankly.exchanges.interfaces.kucoin.kucoin_interface import KucoinInterface
 
 
 class Exchange(ABCExchange, abc.ABC):
     interface: ABCExchangeInterface
 
-    def __init__(self, exchange_type, portfolio_name, keys_path, preferences_path):
-        self.__type = exchange_type  # coinbase_pro, binance, alpaca, oanda
+    def __init__(self, exchange_type, portfolio_name, preferences_path):
+        self.__type = exchange_type  # coinbase_pro, binance, alpaca, oanda, ftx
         self.__name = portfolio_name  # my_cool_portfolio
-        self.__factory = AuthFactory()
 
-        self.__auth = self.__factory.create_auth(keys_path, self.__type, self.__name)
-        self.__direct_calls_factory = DirectCallsFactory()
-        self.calls, self.interface = self.__direct_calls_factory.create(self.__type, self.__auth, preferences_path)
-        write_auth_cache(exchange_type, portfolio_name, self.calls)
+        # Make a public version of portfolio name
+        self.portfolio_name = self.__name
 
         self.preferences = blankly.utils.load_user_preferences(preferences_path)
 
-        # Create the model container
         self.models = {}
 
+        # Fill this in the method below
+        self.calls = None
+
+    def construct_interface_and_cache(self, calls):
+        """
+        If you are a contributor, you need to modify this function to add exchanges
+        The core functions that creates the interface based on the exchange type & automatically caches
+        """
+        self.calls = calls
+        if self.__type == "coinbase_pro":
+            self.interface = CoinbaseProInterface(self.__type, calls)
+        elif self.__type == "binance":
+            self.interface = BinanceInterface(self.__type, calls)
+        elif self.__type == "alpaca":
+            self.interface = AlpacaInterface(self.__type, calls)
+        elif self.__type == "ftx":
+            self.interface = FTXInterface(self.__type, calls)
+        elif self.__type == "oanda":
+            self.interface = OandaInterface(self.__type, calls)
+        elif self.__type == "kucoin":
+            self.interface = KucoinInterface(self.__type, calls)
+
+        write_auth_cache(self.__type, self.__name, calls)
+
     def get_name(self):
         return self.__name
 
@@ -55,13 +77,6 @@ def get_type(self):
     def get_preferences(self):
         return self.preferences
 
-    # Can be removed?
-    def construct_interface(self, calls):
-        if self.__type == "coinbase_pro":
-            self.interface = CoinbaseProInterface(self.__type, calls)
-        elif self.__type == "binance":
-            self.interface = BinanceInterface(self.__type, calls)
-
     def get_interface(self) -> ABCExchangeInterface:
         """
         Get the the authenticated interface for the object. This will provide authenticated API calls.
diff --git a/blankly/exchanges/interfaces/abc_exchange_interface.py b/blankly/exchanges/interfaces/abc_exchange_interface.py
index 8773dfe9..a49b94eb 100644
--- a/blankly/exchanges/interfaces/abc_exchange_interface.py
+++ b/blankly/exchanges/interfaces/abc_exchange_interface.py
@@ -214,7 +214,7 @@ def get_product_history(self,
 
     @abc.abstractmethod
     def get_order_filter(self,
-                         symbol: str):
+                         symbol: str) -> dict:
         """
         Find order limits for the exchange
         Args:
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca.py b/blankly/exchanges/interfaces/alpaca/alpaca.py
index 74b508e3..c846a320 100644
--- a/blankly/exchanges/interfaces/alpaca/alpaca.py
+++ b/blankly/exchanges/interfaces/alpaca/alpaca.py
@@ -19,11 +19,21 @@
 import alpaca_trade_api
 
 from blankly.exchanges.exchange import Exchange
+from blankly.exchanges.auth.auth_constructor import AuthConstructor
+from blankly.exchanges.interfaces.alpaca.alpaca_api import create_alpaca_client
 
 
 class Alpaca(Exchange):
     def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
-        Exchange.__init__(self, 'alpaca', portfolio_name, keys_path, settings_path)
+        Exchange.__init__(self, "alpaca", portfolio_name, settings_path)
+
+        # Load the auth from the keys file
+        auth = AuthConstructor(keys_path, portfolio_name, 'alpaca', ['API_KEY', 'API_SECRET'])
+
+        calls = create_alpaca_client(auth, self.preferences["settings"]["use_sandbox"])
+
+        # Always finish the method with this function
+        super().construct_interface_and_cache(calls)
 
     def get_exchange_state(self):
         return self.interface.get_fees()
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_api.py b/blankly/exchanges/interfaces/alpaca/alpaca_api.py
index 7de0bd08..ee9e555d 100644
--- a/blankly/exchanges/interfaces/alpaca/alpaca_api.py
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_api.py
@@ -16,34 +16,29 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-import os
-
 import alpaca_trade_api
 
-from blankly.exchanges.auth.abc_auth import ABCAuth
+from blankly.exchanges.auth.auth_constructor import AuthConstructor
 
-APCA_API_LIVE_URL = "https://api.alpaca.markets"
-APCA_API_PAPER_URL = "https://paper-api.alpaca.markets"
+live_url = "https://api.alpaca.markets"
+paper_url = "https://paper-api.alpaca.markets"
 
 
-def create_alpaca_client(auth: ABCAuth, sandbox_mode=True):
+def create_alpaca_client(auth: AuthConstructor, sandbox_mode=True):
     if sandbox_mode:
-        api_url = APCA_API_PAPER_URL
+        api_url = paper_url
     else:
-        api_url = APCA_API_LIVE_URL
+        api_url = live_url
 
     return alpaca_trade_api.REST(auth.keys['API_KEY'], auth.keys['API_SECRET'], api_url, 'v2', raw_data=True)
 
 
-class API:
-    def __init__(self, auth: ABCAuth, paper_trading=True):
-        if paper_trading:
-            self.__api_url = APCA_API_PAPER_URL
-        else:
-            self.__api_url = APCA_API_LIVE_URL
-
-        self.alp_client = alpaca_trade_api.REST(auth.keys['API_KEY'], auth.keys['API_SECRET'], self.__api_url, 'v2')
-
-
-api = os.getenv("ALPACA_PUBLIC")
-secret = os.getenv("ALPACA_PRIVATE")
+# class API:
+#     def __init__(self, auth: AuthConstructor, paper_trading=True):
+#         if paper_trading:
+#             self.__api_url = APCA_API_PAPER_URL
+#         else:
+#             self.__api_url = APCA_API_LIVE_URL
+#
+#         self.alp_client = alpaca_trade_api.REST(auth.keys['API_KEY'], auth.keys['API_SECRET'], self.__api_url, 'v2')
+#
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_auth.py b/blankly/exchanges/interfaces/alpaca/alpaca_auth.py
deleted file mode 100644
index bb1de963..00000000
--- a/blankly/exchanges/interfaces/alpaca/alpaca_auth.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""
-    alpaca authentication base class
-    Copyright (C) 2021  Arun Annamalai, Emerson Dove
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Lesser General Public License as published
-    by the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Lesser General Public License for more details.
-
-    You should have received a copy of the GNU Lesser General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-"""
-
-from blankly.exchanges.auth.abc_auth import ABCAuth
-
-
-class AlpacaAuth(ABCAuth):
-    def __init__(self, keys_file, portfolio_name):
-        super().__init__(keys_file, portfolio_name, 'alpaca')
-        needed_keys = ['API_KEY', 'API_SECRET']
-        self.validate_credentials(needed_keys)
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_interface.py b/blankly/exchanges/interfaces/alpaca/alpaca_interface.py
index 476344a1..8a75ebde 100644
--- a/blankly/exchanges/interfaces/alpaca/alpaca_interface.py
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_interface.py
@@ -26,21 +26,21 @@
 from alpaca_trade_api.rest import APIError as AlpacaAPIError, TimeFrame
 from dateutil import parser
 
-from blankly.exchanges.interfaces.alpaca.alpaca_api import API
 from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
 from blankly.exchanges.orders.limit_order import LimitOrder
 from blankly.exchanges.orders.market_order import MarketOrder
 from blankly.utils import utils as utils
 from blankly.utils.exceptions import APIException
-from blankly.utils.time_builder import build_minute, time_interval_to_seconds
+from blankly.utils.time_builder import build_minute, time_interval_to_seconds, number_interval_to_string
 
 NY = 'America/New_York'
 
 
 class AlpacaInterface(ExchangeInterface):
-    def __init__(self, authenticated_api: API, preferences_path: str):
+    def __init__(self, exchange_name, authenticated_api):
         self.__unique_assets = None
-        super().__init__('alpaca', authenticated_api, preferences_path, valid_resolutions=[60, 60*5, 60*15, 60*60*24])
+        super().__init__(exchange_name, authenticated_api, valid_resolutions=[60, 60*5, 60*15,
+                                                                              60*60*24])
         assert isinstance(self.calls, alpaca_trade_api.REST)
 
     def init_exchange(self):
@@ -199,6 +199,18 @@ def get_account(self, symbol=None):
 
         return positions_dict
 
+    @staticmethod
+    def __parse_iso(response):
+        try:
+            response['created_at'] = parser.isoparse(response['created_at']).timestamp()
+        except ValueError as e:
+            if str(e) == 'Unused components in ISO string':
+                response['created_at'] = parser.parse(response['created_at']).timestamp()
+            else:
+                raise e
+
+        return response
+
     @utils.order_protection
     def market_order(self, symbol, side, size) -> MarketOrder:
         assert isinstance(self.calls, alpaca_trade_api.REST)
@@ -215,7 +227,9 @@ def market_order(self, symbol, side, size) -> MarketOrder:
             'type': 'market'
         }
         response = self.calls.submit_order(symbol, side=side, type='market', time_in_force='day', qty=size)
-        response['created_at'] = parser.isoparse(response['created_at']).timestamp()
+
+        response = self.__parse_iso(response)
+
         response = utils.rename_to(renames, response)
         response = utils.isolate_specific(needed, response)
         return MarketOrder(order, response, self)
@@ -243,7 +257,8 @@ def limit_order(self, symbol: str, side: str, price: float, size: float) -> Limi
                                            qty=size,
                                            limit_price=price)
 
-        response['created_at'] = parser.isoparse(response['created_at']).timestamp()
+        response = self.__parse_iso(response)
+
         response = utils.rename_to(renames, response)
         response = utils.isolate_specific(needed, response)
         response['time_in_force'] = response['time_in_force'].upper()
@@ -309,7 +324,8 @@ def homogenize_order(self, order):
             ]
             order = utils.rename_to(renames, order)
 
-        order['created_at'] = parser.isoparse(order['created_at']).timestamp()
+        order = self.__parse_iso(order)
+
         needed = self.choose_order_specificity(order['type'])
 
         order = utils.isolate_specific(needed, order)
@@ -323,60 +339,66 @@ def get_fees(self):
         }
 
     def get_product_history(self, symbol: str, epoch_start: float, epoch_stop: float, resolution: int):
-        assert isinstance(self.calls, alpaca_trade_api.REST)
+        if not self.user_preferences['settings']['alpaca']['use_yfinance']:
+            assert isinstance(self.calls, alpaca_trade_api.REST)
 
-        resolution = time_interval_to_seconds(resolution)
+            resolution = time_interval_to_seconds(resolution)
 
-        supported_multiples = [60, 3600, 86400]
-        if resolution not in supported_multiples:
-            utils.info_print("Granularity is not an accepted granularity...rounding to nearest valid value.")
-            resolution = supported_multiples[min(range(len(supported_multiples)),
-                                             key=lambda i: abs(supported_multiples[i] - resolution))]
+            supported_multiples = [60, 3600, 86400]
+            if resolution not in supported_multiples:
+                utils.info_print("Granularity is not an accepted granularity...rounding to nearest valid value.")
+                resolution = supported_multiples[min(range(len(supported_multiples)),
+                                                 key=lambda i: abs(supported_multiples[i] - resolution))]
 
-        found_multiple, row_divisor = super().evaluate_multiples(supported_multiples, resolution)
+            found_multiple, row_divisor = super().evaluate_multiples(supported_multiples, resolution)
 
-        if found_multiple == 60:
-            time_interval = TimeFrame.Minute
-        elif found_multiple == 3600:
-            time_interval = TimeFrame.Hour
-        else:
-            time_interval = TimeFrame.Day
+            if found_multiple == 60:
+                time_interval = TimeFrame.Minute
+            elif found_multiple == 3600:
+                time_interval = TimeFrame.Hour
+            else:
+                time_interval = TimeFrame.Day
 
-        epoch_start_str = dt.fromtimestamp(epoch_start, tz=timezone.utc).isoformat()
-        epoch_stop_str = dt.fromtimestamp(epoch_stop, tz=timezone.utc).isoformat()
+            epoch_start_str = dt.fromtimestamp(epoch_start, tz=timezone.utc).isoformat()
+            epoch_stop_str = dt.fromtimestamp(epoch_stop, tz=timezone.utc).isoformat()
 
-        try:
             try:
-                bars = self.calls.get_bars(symbol, time_interval, epoch_start_str, epoch_stop_str, adjustment='raw').df
-            except TypeError:
-                # If you query a timeframe with no data the API throws a Nonetype issue so just return something
-                #  empty if that happens
-                return pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
-        except AlpacaAPIError as e:
-            if e.code == 42210000:
-                warning_string = "Your alpaca subscription does not permit querying data from the last 15 minutes. " \
-                                 "Blankly is adjusting your query."
-                utils.info_print(warning_string)
-                epoch_stop = time.time() - (build_minute() * 15)
-                if epoch_stop >= epoch_start:
-                    try:
-                        return self.get_product_history(symbol, epoch_start, epoch_stop, resolution)
-                    except TypeError:
-                        # If you query a timeframe with no data the API throws a Nonetype issue so just return something
-                        #  empty if that happens
+                try:
+                    bars = self.calls.get_bars(symbol, time_interval, epoch_start_str, epoch_stop_str,
+                                               adjustment='raw').df
+                except TypeError:
+                    # If you query a timeframe with no data the API throws a Nonetype issue so just return something
+                    #  empty if that happens
+                    return pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
+            except AlpacaAPIError as e:
+                if e.code == 42210000:
+                    warning_string = "Your alpaca subscription does not permit querying data from the last 15 minutes. " \
+                                     "Blankly is adjusting your query."
+                    utils.info_print(warning_string)
+                    epoch_stop = time.time() - (build_minute() * 15)
+                    if epoch_stop >= epoch_start:
+                        try:
+                            return self.get_product_history(symbol, epoch_start, epoch_stop, resolution)
+                        except TypeError:
+                            # If you query a timeframe with no data the API throws a Nonetype issue so just
+                            #  return something
+                            #  empty if that happens
+                            return pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
+                    else:
+                        warning_string = "No data range queried after time adjustment."
+                        utils.info_print(warning_string)
                         return pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
                 else:
-                    warning_string = "No data range queried after time adjustment."
-                    utils.info_print(warning_string)
-                    return pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
-            else:
-                raise e
-        bars.rename(columns={"t": "time", "o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"},
-                    inplace=True)
+                    raise e
+            bars.rename(columns={"t": "time", "o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"},
+                        inplace=True)
 
-        if bars.empty:
-            return pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
-        return utils.get_ohlcv(bars, row_divisor, from_zero=False)
+            if bars.empty:
+                return pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
+            return utils.get_ohlcv(bars, row_divisor, from_zero=False)
+        else:
+            # This runs yfinance on the symbol
+            return self.parse_yfinance(symbol, epoch_start, epoch_stop, resolution)
 
     # def history(self,
     #             symbol: str,
@@ -446,45 +468,53 @@ def overridden_history(self, symbol, epoch_start, epoch_stop, resolution_seconds
         if to is not None and isinstance(to, str):
             to = None
         if to:
-            resolution_seconds = self.valid_resolutions[min(range(len(self.valid_resolutions)),
-                                                            key=lambda j: abs(self.valid_resolutions[j] -
-                                                                              resolution_seconds))]
-            resolution_lookup = {
-                60: '1Min',
-                300: '5Min',
-                900: '15Min',
-                86400: '1D'
-            }
-            time_interval = resolution_lookup[resolution_seconds]
-
-            frames = []
-
-            while to > 1000:
-                # Create an end time by moving after the start time by 1000 datapoints
-                epoch_start += resolution_seconds * 1000
-
-                frames.append(self.calls.get_barset(symbol, time_interval, limit=1000,
-                                                    end=utils.ISO8601_from_epoch(epoch_start))[symbol])
-                to -= 1000
-
-            frames.append(self.calls.get_barset(symbol, time_interval, limit=to,
-                                                end=utils.ISO8601_from_epoch(epoch_stop))[symbol])
-
-            for i in range(len(frames)):
-                frames[i] = pd.DataFrame(frames[i])
-
-            response = pd.concat(frames, ignore_index=True)
-            response.rename(columns={"t": "time", "o": "open", "h": "high", "l": "low", "c": "close", "v":
-                            "volume"}, inplace=True)
+            if not self.user_preferences['settings']['alpaca']['use_yfinance']:
+                resolution_seconds = self.valid_resolutions[min(range(len(self.valid_resolutions)),
+                                                                key=lambda j: abs(self.valid_resolutions[j] -
+                                                                                  resolution_seconds))]
+                resolution_lookup = {
+                    60: '1Min',
+                    300: '5Min',
+                    900: '15Min',
+                    86400: '1D'
+                }
+                time_interval = resolution_lookup[resolution_seconds]
+
+                frames = []
+
+                while to > 1000:
+                    # Create an end time by moving after the start time by 1000 datapoints
+                    epoch_stop -= resolution_seconds * 1000
+
+                    frames.append(self.calls.get_barset(symbol, time_interval, limit=1000,
+                                                        end=utils.iso8601_from_epoch(epoch_stop))[symbol])
+                    to -= 1000
+
+                frames.append(self.calls.get_barset(symbol, time_interval, limit=to,
+                                                    end=utils.iso8601_from_epoch(epoch_stop))[symbol])
+
+                for i in range(len(frames)):
+                    frames[i] = pd.DataFrame(frames[i])
+
+                response = pd.concat(frames, ignore_index=True)
+                response.rename(columns={"t": "time", "o": "open", "h": "high", "l": "low", "c": "close", "v":
+                                "volume"}, inplace=True)
+
+                response = response.sort_values(by=['time'], ignore_index=True)
+
+                response = response.astype({
+                    'time': int,
+                    'open': float,
+                    'high': float,
+                    'low': float,
+                    'close': float,
+                    'volume': float,
+                })
+            else:
+                # Overestimate the difference for yfinance
+                epoch_start = epoch_stop - (epoch_stop - epoch_start) * 1.5
 
-            response = response.astype({
-                'time': int,
-                'open': float,
-                'high': float,
-                'low': float,
-                'close': float,
-                'volume': float,
-            })
+                return self.parse_yfinance(symbol, epoch_start, epoch_stop, resolution_seconds).tail(to).reset_index()
         else:
             response = self.get_product_history(symbol,
                                                 epoch_start,
@@ -534,6 +564,10 @@ def get_order_filter(self, symbol: str):
             # base_increment = product['base_increment']
             min_price = 0.0001
             max_price = 10000000000
+
+            # Guaranteed nano share if fractionable
+            base_min_size = 1e-9
+            base_increment = 1e-9
         else:
             quote_increment = current_price
             min_funds_buy = current_price
@@ -545,6 +579,10 @@ def get_order_filter(self, symbol: str):
             min_price = 0.0001
             max_price = 10000000000
 
+            # Always 1 if not fractionable
+            base_min_size = 1
+            base_increment = 1
+
         max_funds = current_price * 10000000000
 
         return {
@@ -565,12 +603,9 @@ def get_order_filter(self, symbol: str):
             'market_order': {
                 "fractionable": fractionable,
 
-                # TODO alpaca is very broken right now and is only allowing whole number quantity so these are
-                #  overridden. If I see anyone creating an issue about this it better be that I can finally allow
-                #  fractional back
-                "base_min_size": 1,  # Minimum size to buy
+                "base_min_size": base_min_size,  # Minimum size to buy
                 "base_max_size": base_max_size,  # Maximum size to buy
-                "base_increment": 1,  # Specifies the minimum increment for the base_asset.
+                "base_increment": base_increment,  # Specifies the minimum increment for the base_asset.
 
                 "quote_increment": quote_increment,  # Specifies the min order price as well as the price increment.
                 "buy": {
@@ -599,3 +634,31 @@ def get_price(self, symbol) -> float:
         assert isinstance(self.calls, alpaca_trade_api.REST)
         response = self.calls.get_last_trade(symbol=symbol)
         return float(response['price'])
+
+    @staticmethod
+    def parse_yfinance(symbol: str, epoch_start: [int, float], epoch_stop: [int, float], resolution: int):
+        try:
+            import yfinance
+
+            start_date = dt.fromtimestamp(epoch_start, tz=timezone.utc)
+            stop_date = dt.fromtimestamp(epoch_stop, tz=timezone.utc)
+            ticker = yfinance.Ticker(symbol)
+            result = ticker.history(start=start_date, end=stop_date, interval=number_interval_to_string(resolution))
+
+            result['time'] = result.index.astype(int) // 10 ** 9
+            result = result[['Open', 'High', 'Low', 'Close', 'Volume', 'time']].reset_index()
+
+            result = result.rename(columns={
+                'time': 'time',
+                'Open': 'open',
+                'High': 'high',
+                'Low': 'low',
+                'Close': 'close',
+                'Volume': 'volume'
+            })
+
+            result = result[['time', 'open', 'high', 'low', 'close', 'volume']]
+
+            return result
+        except ImportError:
+            raise ImportError("To use yfinance to download data please pip install yfinance")
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py b/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py
index 3080629d..c0c590d4 100644
--- a/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py
@@ -28,7 +28,7 @@
 import blankly
 from blankly.utils.utils import info_print
 from blankly.exchanges.abc_exchange_websocket import ABCExchangeWebsocket
-from blankly.exchanges.auth.auth_constructor import load_auth
+from blankly.exchanges.auth.utils import load_auth
 from blankly.exchanges.interfaces.alpaca.alpaca_websocket_utils import parse_alpaca_timestamp, switch_type
 
 
diff --git a/blankly/exchanges/interfaces/binance/binance.py b/blankly/exchanges/interfaces/binance/binance.py
index 7e4f10ae..e4798afa 100644
--- a/blankly/exchanges/interfaces/binance/binance.py
+++ b/blankly/exchanges/interfaces/binance/binance.py
@@ -19,12 +19,27 @@
 from binance.client import Client
 
 from blankly.exchanges.exchange import Exchange
+from blankly.exchanges.auth.auth_constructor import AuthConstructor
 from blankly.utils import utils
 
 
 class Binance(Exchange):
     def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
-        Exchange.__init__(self, "binance", portfolio_name, keys_path, settings_path)
+        Exchange.__init__(self, "binance", portfolio_name, settings_path)
+
+        # Load the auth from the keys file
+        auth = AuthConstructor(keys_path, portfolio_name, 'binance', ['API_KEY', 'API_SECRET'])
+
+        if self.preferences["settings"]["use_sandbox"]:
+            calls = Client(api_key=auth.keys['API_KEY'], api_secret=auth.keys['API_SECRET'],
+                           tld=self.preferences["settings"]['binance']["binance_tld"],
+                           testnet=True)
+        else:
+            calls = Client(api_key=auth.keys['API_KEY'], api_secret=auth.keys['API_SECRET'],
+                           tld=self.preferences["settings"]['binance']["binance_tld"])
+
+        # Always finish the method with this function
+        super().construct_interface_and_cache(calls)
 
     """
     Builds information about the symbol on this exchange by making particular API calls
diff --git a/blankly/exchanges/interfaces/binance/binance_auth.py b/blankly/exchanges/interfaces/binance/binance_auth.py
deleted file mode 100644
index e6236589..00000000
--- a/blankly/exchanges/interfaces/binance/binance_auth.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""
-    binance authentication base class
-    Copyright (C) 2021  Arun Annamalai, Emerson Dove
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Lesser General Public License as published
-    by the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Lesser General Public License for more details.
-
-    You should have received a copy of the GNU Lesser General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-"""
-
-from blankly.exchanges.auth.abc_auth import ABCAuth
-
-
-class BinanceAuth(ABCAuth):
-    def __init__(self, keys_file, portfolio_name):
-        super().__init__(keys_file, portfolio_name, 'binance')
-        needed_keys = ['API_KEY', 'API_SECRET']
-        self.validate_credentials(needed_keys)
diff --git a/blankly/exchanges/interfaces/binance/binance_interface.py b/blankly/exchanges/interfaces/binance/binance_interface.py
index afa81ce2..f114be7b 100644
--- a/blankly/exchanges/interfaces/binance/binance_interface.py
+++ b/blankly/exchanges/interfaces/binance/binance_interface.py
@@ -43,8 +43,9 @@ def init_exchange(self):
         except binance.exceptions.BinanceAPIException:
             raise exceptions.APIException("Invalid API Key, IP, or permissions for action - are you trying "
                                           "to use your normal exchange keys while in sandbox mode? "
-                                          "\nTry toggling the \'use_sandbox\' setting in your settings.json or check "
-                                          "if the keys were input correctly into your keys.json.")
+                                          "\nTry toggling the \'use_sandbox\' setting in your settings.json, check "
+                                          "if the keys were input correctly into your keys.json or ensure you have set "
+                                          "the correct binance_tld in settings.json.")
 
         symbols = self.calls.get_exchange_info()["symbols"]
         assets = []
diff --git a/blankly/exchanges/interfaces/binance/binance_websocket.py b/blankly/exchanges/interfaces/binance/binance_websocket.py
index abe184e4..ff375ae8 100644
--- a/blankly/exchanges/interfaces/binance/binance_websocket.py
+++ b/blankly/exchanges/interfaces/binance/binance_websocket.py
@@ -16,20 +16,16 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-import collections
 import json
-import threading
 import traceback
 
-import websocket
-
 import blankly
 import blankly.exchanges.interfaces.binance.binance_websocket_utils as websocket_utils
-from blankly.exchanges.abc_exchange_websocket import ABCExchangeWebsocket
+from blankly.exchanges.interfaces.websocket import Websocket
 from blankly.utils.utils import info_print
 
 
-class Tickers(ABCExchangeWebsocket):
+class Tickers(Websocket):
     def __init__(self, symbol, stream, log=None, initially_stopped=False,
                  WEBSOCKET_URL="wss://stream.binance.{}:9443/ws"):
         """
@@ -40,64 +36,23 @@ def __init__(self, symbol, stream, log=None, initially_stopped=False,
             log: Fill this with a path to a log file that should be created
             WEBSOCKET_URL: Default websocket URL feed.
         """
-        self.__id = symbol
-        self.__stream = stream
-        self.__logging_callback, self.__interface_callback, log_message = websocket_utils.switch_type(stream)
-
-        # Initialize log file
-        if log is not None:
-            self.__log = True
-            self.__filePath = log
-            try:
-                self.__file = open(log, 'x+')
-                self.__file.write(log_message)
-            except FileExistsError:
-                self.__file = open(log, 'a')
-        else:
-            self.__log = False
-
         # Reload preferences
         self.__preferences = blankly.utils.load_user_preferences()
 
-        # Add the TLD into the URL if necessary
-        self.URL = WEBSOCKET_URL.format(self.__preferences['settings']['binance']['binance_tld'])
-        self.ws = None
-        self.__response = None
-        self.__most_recent_tick = None
-        self.__most_recent_time = None
-        # Create the thread object here
-        self.__thread = threading.Thread(target=self.read_websocket)
-        self.__callbacks = []
-        # This is created so that we know when a message has come back that we're waiting for
-        self.__message_count = 0
-
-        buffer_size = self.__preferences["settings"]["websocket_buffer_size"]
-        self.__ticker_feed = collections.deque(maxlen=buffer_size)
-        self.__time_feed = collections.deque(maxlen=buffer_size)
+        self.__logging_callback, self.__interface_callback, log_message = websocket_utils.switch_type(stream)
+        url = WEBSOCKET_URL.format(self.__preferences['settings']['binance']['binance_tld'])
+
+        super().__init__(symbol, stream, log, log_message, url, None)
 
         # Start the websocket
         if not initially_stopped:
-            self.start_websocket()
-
-    def start_websocket(self):
-        """
-        Restart websocket if it was asked to stop.
-        """
-        if self.ws is None:
-            self.ws = websocket.WebSocketApp(self.URL,
-                                             on_open=self.on_open,
-                                             on_message=self.on_message,
-                                             on_error=self.on_error,
-                                             on_close=self.on_close)
-            self.__thread = threading.Thread(target=self.read_websocket)
-            self.__thread.start()
-        else:
-            if self.__thread.is_alive():
-                info_print("Already running...")
-            else:
-                # Use recursion to restart, continue appending to time feed and ticker feed
-                self.ws = None
-                self.start_websocket()
+            self.start_websocket(
+                self.on_open,
+                self.on_message,
+                self.on_error,
+                self.on_close,
+                self.read_websocket
+            )
 
     def read_websocket(self):
         # Main thread to sit here and run
@@ -105,31 +60,28 @@ def read_websocket(self):
         # This repeats the close behavior just in case something happens
 
     def on_message(self, ws, message):
-        self.__message_count += 1
+        """
+        Exchange specific actions to perform when receiving a message
+        """
+        self.message_count += 1
         message = json.loads(message)
         try:
-            self.__most_recent_time = message['E']
-            self.__time_feed.append(self.__most_recent_time)
+            self.most_recent_time = message['E']
+            self.time_feed.append(self.most_recent_time)
 
-            # Run callbacks on message
-            if self.__log:
-                if self.__message_count % 100 == 0:
-                    self.__file.close()
-                    self.__file = open(self.__filePath, 'a')
-                line = self.__logging_callback(message)
-                self.__file.write(line)
+            self.log_response(self.__logging_callback, message)
 
             interface_message = self.__interface_callback(message)
-            self.__ticker_feed.append(interface_message)
-            self.__most_recent_tick = interface_message
-            for i in self.__callbacks:
+            self.ticker_feed.append(interface_message)
+            self.most_recent_tick = interface_message
+            for i in self.callbacks:
                 i(interface_message)
         except KeyError:
             # If the try below figures this out then we don't have to traceback
             error_found = False
             try:
                 if message['result'] is None:
-                    self.__response = message
+                    self.response = message
                     error_found = True
             except KeyError:
                 traceback.print_exc()
@@ -138,68 +90,25 @@ def on_message(self, ws, message):
                 traceback.print_exc()
 
     def on_error(self, ws, error):
-        print(error)
+        info_print(error)
 
     def on_close(self, ws):
         # This repeats the close behavior just in case something happens
         pass
 
     def on_open(self, ws):
-        request = '{"method": ' \
-                  '"SUBSCRIBE","params": ' \
-                  '["' + self.__id + '@' + self.__stream + \
-                  '"],"id": 1}'
+        request = json.dumps({
+            'method': 'SUBSCRIBE',
+            'params': [f'{self.symbol}@{self.stream}'],
+            'id': 1
+        })
         ws.send(request)
 
-    """ Required in manager """
-
-    def is_websocket_open(self):
-        return self.__thread.is_alive()
-
-    def get_currency_id(self):
-        return self.__id
-
-    """ Required in manager """
-
-    def append_callback(self, obj):
-        self.__callbacks.append(obj)
-
-    """ Define a variable each time so there is no array manipulation """
-    """ Required in manager """
-
-    def get_most_recent_tick(self):
-        return self.__most_recent_tick
-
-    """ Required in manager """
-
-    def get_most_recent_time(self):
-        return self.__most_recent_time
-
-    """ Required in manager """
-
-    def get_time_feed(self):
-        return list(self.__time_feed)
-
-    """ Parallel with time feed """
-    """ Required in manager """
-
-    def get_feed(self):
-        return list(self.__ticker_feed)
-
-    """ Required in manager """
-
-    def get_response(self):
-        return self.__response
-
-    """ Required in manager """
-
-    def close_websocket(self):
-        if self.__thread.is_alive():
-            self.ws.close()
-        else:
-            print("Websocket for " + self.__id + '@' + self.__stream + " is already closed")
-
-    """ Required in manager """
-
     def restart_ticker(self):
-        self.start_websocket()
+        self.start_websocket(
+            self.on_open,
+            self.on_message,
+            self.on_error,
+            self.on_close,
+            self.read_websocket
+        )
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
index c97d981c..d83ad983 100644
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
@@ -16,14 +16,32 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
+from blankly.exchanges.auth.auth_constructor import AuthConstructor
 from blankly.exchanges.exchange import Exchange
 from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_api import API as CoinbaseProAPI
 
 
 class CoinbasePro(Exchange):
     def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
-        # Giving the preferences path as none allows us to create a default
-        Exchange.__init__(self, "coinbase_pro", portfolio_name, keys_path, settings_path)
+        Exchange.__init__(self, "coinbase_pro", portfolio_name, settings_path)
+
+        # Load the auth from the keys file
+        auth = AuthConstructor(keys_path, portfolio_name, 'coinbase_pro', ['API_KEY', 'API_SECRET', 'API_PASS'])
+
+        keys = auth.keys
+        if self.preferences["settings"]["use_sandbox"]:
+            calls = CoinbaseProAPI(api_key=keys['API_KEY'],
+                                   api_secret=keys['API_SECRET'],
+                                   api_pass=keys['API_PASS'],
+                                   API_URL="https://api-public.sandbox.pro.coinbase.com/")
+        else:
+            # Create the authenticated object
+            calls = CoinbaseProAPI(api_key=keys['API_KEY'],
+                                   api_secret=keys['API_SECRET'],
+                                   api_pass=keys['API_PASS'])
+
+        # Always finish the method with this function
+        super().construct_interface_and_cache(calls)
 
     """
     Builds information about the asset on this exchange by making particular API calls
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_api.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_api.py
index de3e525f..319a363d 100644
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_api.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_api.py
@@ -23,16 +23,15 @@
 from requests.auth import AuthBase
 
 # Create custom authentication for Exchange
-from blankly.exchanges.auth.abc_auth import ABCAuth
 from blankly.utils.utils import info_print
 
 
 class CoinbaseExchangeAuth(AuthBase):
     # Provided by CBPro: https://docs.pro.coinbase.com/#signing-a-message
-    def __init__(self, cb_auth: ABCAuth):
-        self.api_key = cb_auth.keys['API_KEY']
-        self.secret_key = cb_auth.keys['API_SECRET']
-        self.passphrase = cb_auth.keys['API_PASS']
+    def __init__(self, api_key, api_secret, api_pass):
+        self.api_key = api_key
+        self.secret_key = api_secret
+        self.passphrase = api_pass
 
     def __call__(self, request):
         timestamp = str(time.time())
@@ -60,8 +59,8 @@ def get_auth_headers(timestamp, message, api_key, secret_key, passphrase):
 
 
 class API:
-    def __init__(self, cb_auth: ABCAuth, API_URL='https://api.pro.coinbase.com/'):
-        self.__auth = CoinbaseExchangeAuth(cb_auth)
+    def __init__(self, api_key: str, api_secret: str, api_pass: str, API_URL: str='https://api.pro.coinbase.com/'):
+        self.__auth = CoinbaseExchangeAuth(api_key, api_secret, api_pass)
         self.__api_url = API_URL
         self.session = requests.Session()
 
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_auth.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_auth.py
deleted file mode 100644
index 5bdcd76c..00000000
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_auth.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""
-    Coinbase authentication base class
-    Copyright (C) 2021  Arun Annamalai, Emerson Dove
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Lesser General Public License as published
-    by the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Lesser General Public License for more details.
-
-    You should have received a copy of the GNU Lesser General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-"""
-
-from blankly.exchanges.auth.abc_auth import ABCAuth
-
-
-class CoinbaseProAuth(ABCAuth):
-    def __init__(self, keys_file, portfolio_name):
-        super().__init__(keys_file, portfolio_name, 'coinbase_pro')
-        needed_keys = ['API_KEY', 'API_SECRET', 'API_PASS']
-        self.validate_credentials(needed_keys)
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
index 5275a7ba..8e430a58 100644
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
@@ -166,7 +166,7 @@ def market_order(self, symbol, side, size) -> MarketOrder:
         response = self.calls.place_market_order(symbol, side, size=size)
         if "message" in response:
             raise InvalidOrder("Invalid Order: " + response["message"])
-        response["created_at"] = utils.epoch_from_ISO8601(response["created_at"])
+        response["created_at"] = utils.epoch_from_iso8601(response["created_at"])
         response["symbol"] = response.pop('product_id')
         response = utils.isolate_specific(needed, response)
         return MarketOrder(order, response, self)
@@ -211,7 +211,7 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
         response = self.calls.place_limit_order(symbol, side, price, size=size)
         if "message" in response:
             raise InvalidOrder("Invalid Order: " + response["message"])
-        response["created_at"] = utils.epoch_from_ISO8601(response["created_at"])
+        response["created_at"] = utils.epoch_from_iso8601(response["created_at"])
         response["symbol"] = response.pop('product_id')
         response = utils.isolate_specific(needed, response)
         return LimitOrder(order, response, self)
@@ -324,7 +324,7 @@ def get_open_orders(self, symbol=None):
             raise InvalidOrder("Invalid Order: " + str(orders))
 
         for i in range(len(orders)):
-            orders[i]["created_at"] = utils.epoch_from_ISO8601(orders[i]["created_at"])
+            orders[i]["created_at"] = utils.epoch_from_iso8601(orders[i]["created_at"])
             needed = self.choose_order_specificity(orders[i]['type'])
             orders[i]["symbol"] = orders[i].pop('product_id')
             orders[i] = utils.isolate_specific(needed, orders[i])
@@ -386,7 +386,7 @@ def get_order(self, symbol, order_id) -> dict:
             #         raise APIException("Invalid: " + str(response['message']) + ", was the order canceled?")
             # else:
             raise APIException("Invalid: " + str(response['message']) + ", was the order canceled?")
-        response["created_at"] = utils.epoch_from_ISO8601(response["created_at"])
+        response["created_at"] = utils.epoch_from_iso8601(response["created_at"])
 
         if response['type'] == 'market':
             needed = self.needed['market_order']
@@ -458,8 +458,8 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
         while need > 300:
             # Close is always 300 points ahead
             window_close = window_open + 300 * resolution
-            open_iso = utils.ISO8601_from_epoch(window_open)
-            close_iso = utils.ISO8601_from_epoch(window_close)
+            open_iso = utils.iso8601_from_epoch(window_open)
+            close_iso = utils.iso8601_from_epoch(window_close)
             response = self.calls.get_product_historic_rates(symbol, open_iso, close_iso, resolution)
             if isinstance(response, dict):
                 raise APIException(response['message'])
@@ -471,8 +471,8 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
             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)
+        open_iso = utils.iso8601_from_epoch(window_open)
+        close_iso = utils.iso8601_from_epoch(epoch_stop)
         response = self.calls.get_product_historic_rates(symbol, open_iso, close_iso, resolution)
         if isinstance(response, dict):
             raise APIException(response['message'])
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py
index d58da01c..b725e789 100644
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py
@@ -16,22 +16,18 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-import collections
 import json
-import ssl
-import threading
 import time
 import traceback
 
-from websocket import create_connection
-
 import blankly
 import blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket_utils as websocket_utils
-from blankly.exchanges.abc_exchange_websocket import ABCExchangeWebsocket
+from blankly.exchanges.interfaces.websocket import Websocket
+from blankly.utils.utils import info_print
 
 
 def create_ticker_connection(id, url, channel):
-    ws = create_connection(url, sslopt={"cert_reqs": ssl.CERT_NONE})
+    ws = None  # create_connection(url, sslopt={"cert_reqs": ssl.CERT_NONE})
     request = """{
     "type": "subscribe",
     "product_ids": [
@@ -54,7 +50,7 @@ def create_ticker_connection(id, url, channel):
 # ]
 
 
-class Tickers(ABCExchangeWebsocket):
+class Tickers(Websocket):
     def __init__(self, symbol, stream, log=None,
                  pre_event_callback=None, initially_stopped=False, WEBSOCKET_URL="wss://ws-feed.pro.coinbase.com"):
         """
@@ -64,58 +60,33 @@ def __init__(self, symbol, stream, log=None,
             log: Fill this with a path to a log file that should be created
             WEBSOCKET_URL: Default websocket URL feed.
         """
-        self.__id = symbol
-        self.__stream = stream
         self.__logging_callback, self.__interface_callback, log_message = websocket_utils.switch_type(stream)
 
-        # Initialize log file
-        if log is not None:
-            self.__log = True
-            self.__filePath = log
-            try:
-                self.__file = open(log, 'x+')
-                self.__file.write(log_message)
-            except FileExistsError:
-                self.__file = open(log, 'a')
-        else:
-            self.__log = False
-
-        self.URL = WEBSOCKET_URL
-        self.ws = None
-        self.__response = None
-        self.__most_recent_tick = None
-        self.__most_recent_time = None
-        self.__callbacks = []
-        self.__pre_event_callback = pre_event_callback
-
-        # Reload preferences
-        self.__preferences = blankly.utils.load_user_preferences()
-        buffer_size = self.__preferences["settings"]["websocket_buffer_size"]
-        self.__ticker_feed = collections.deque(maxlen=buffer_size)
-        self.__time_feed = collections.deque(maxlen=buffer_size)
+        super().__init__(symbol, stream, log, log_message, WEBSOCKET_URL, pre_event_callback)
+
+        self.__pre_event_callback_filled = False
 
         # Start the websocket
         if not initially_stopped:
-            self.start_websocket()
-
-    def start_websocket(self):
+            self.start_websocket(
+                self.on_open,
+                self.on_message,
+                self.on_error,
+                self.on_close,
+                self.run_forever
+            )
+
+    def run_forever(self):
         """
-        Restart websocket if it was asked to stop.
+        This is the target from the super
+
+        It's renamed to run_forever in coinbase pro because I think that ancient code below is cool
         """
-        if self.ws is None:
-            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()
-        else:
-            if self.ws.connected:
-                print("Already running...")
-                pass
-            else:
-                # Use recursion to restart, continue appending to time feed and ticker feed
-                self.ws = None
-                self.start_websocket()
+        self.ws.run_forever()
 
+    """
+    This function has some of the oldest code in the entire package
+    """
     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":
@@ -135,7 +106,7 @@ def read_websocket(self):
                 received_string = self.ws.recv()
                 received = json.loads(received_string)
                 # Modify time to use epoch
-                self.__most_recent_time = blankly.utils.epoch_from_ISO8601(received["time"])
+                self.__most_recent_time = blankly.utils.epoch_from_iso8601(received["time"])
                 received["time"] = self.__most_recent_time
                 self.__time_feed.append(self.__most_recent_time)
 
@@ -169,59 +140,73 @@ def read_websocket(self):
                     # 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.__stream)
                     # Update response
-                    self.__response = self.ws.recv()
-
-    """ Required in manager """
-
-    def is_websocket_open(self):
-        return self.ws.connected
-
-    def get_currency_id(self):
-        return self.__id
-
-    """ Required in manager """
+                    self.response = self.ws.recv()
 
-    def append_callback(self, obj):
-        self.__callbacks.append(obj)
+    def on_message(self, ws, message):
+        received_string = message
+        received = json.loads(received_string)
 
-    """ Define a variable each time so there is no array manipulation """
-    """ Required in manager """
+        if received['type'] == 'subscriptions':
+            info_print(f"Subscribed to {received['channels']}")
+            return
 
-    def get_most_recent_tick(self):
-        return self.__most_recent_tick
-
-    """ Required in manager """
-
-    def get_most_recent_time(self):
-        return self.__most_recent_time
+        # This is unique because coinbase first sends the entire orderbook to use
+        if (self.pre_event_callback is not None) and (not self.__pre_event_callback_filled) and \
+                (self.stream == "level2"):
+            if received['type'] == 'snapshot':
+                try:
+                    self.pre_event_callback(received)
+                except Exception:
+                    traceback.print_exc()
 
-    """ Required in manager """
+            self.__pre_event_callback_filled = True
+            return
 
-    def get_time_feed(self):
-        return list(self.__time_feed)
+        # 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)
 
-    """ Parallel with time feed """
-    """ Required in manager """
+        self.log_response(self.__logging_callback, received)
 
-    def get_feed(self):
-        return list(self.__ticker_feed)
+        # Manage price events and fire for each manager attached
+        interface_message = self.__interface_callback(received)
+        self.ticker_feed.append(interface_message)
+        self.most_recent_tick = interface_message
 
-    """ Required in manager """
+        try:
+            for i in self.callbacks:
+                i(interface_message)
+        except Exception:
+            traceback.print_exc()
 
-    def get_response(self):
-        return self.__response
+        self.message_count += 1
 
-    """ Required in manager """
+    def on_error(self, ws, error):
+        info_print(error)
 
-    def close_websocket(self):
-        if self.ws.connected:
-            self.ws.close()
-        else:
-            print("Websocket for " + self.__id + ' on channel ' + self.__stream + " is already closed")
+    def on_close(self, ws):
+        # This repeats the close behavior just in case something happens
+        pass
 
-    """ Required in manager """
+    def on_open(self, ws):
+        request = json.dumps({
+            'type': 'subscribe',
+            'product_ids': [self.symbol],
+            'channels': [self.stream]
+        })
+        ws.send(request)
 
     def restart_ticker(self):
-        self.start_websocket()
+        """
+        This is the only abstract function that should be placed individually
+        into each websocket file
+        """
+        self.start_websocket(
+            self.on_open,
+            self.on_message,
+            self.on_error,
+            self.on_close,
+            self.read_websocket
+        )
diff --git a/blankly/exchanges/interfaces/direct_calls_factory.py b/blankly/exchanges/interfaces/direct_calls_factory.py
deleted file mode 100644
index 11ca4142..00000000
--- a/blankly/exchanges/interfaces/direct_calls_factory.py
+++ /dev/null
@@ -1,64 +0,0 @@
-"""
-    Final constructor for authentication objects
-    Copyright (C) 2021  Arun Annamalai, Emerson Dove
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Lesser General Public License as published
-    by the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Lesser General Public License for more details.
-
-    You should have received a copy of the GNU Lesser General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-"""
-
-from binance.client import Client
-
-import blankly.utils.utils as utils
-from blankly.exchanges.auth.abc_auth import ABCAuth
-from blankly.exchanges.interfaces.alpaca.alpaca_api import create_alpaca_client
-from blankly.exchanges.interfaces.alpaca.alpaca_interface import AlpacaInterface
-from blankly.exchanges.interfaces.binance.binance_interface import BinanceInterface
-from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_api import API as CoinbaseProAPI
-from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_interface import CoinbaseProInterface
-from blankly.exchanges.interfaces.oanda.oanda_api import OandaAPI
-from blankly.exchanges.interfaces.oanda.oanda_interface import OandaInterface
-
-
-class DirectCallsFactory:
-    @staticmethod
-    def create(exchange_name: str, auth: ABCAuth, preferences_path: str = None):
-        preferences = utils.load_user_preferences(preferences_path)
-        if exchange_name == 'coinbase_pro':
-            if preferences["settings"]["use_sandbox"]:
-                calls = CoinbaseProAPI(auth, API_URL="https://api-public.sandbox.pro.coinbase.com/")
-            else:
-                # Create the authenticated object
-                calls = CoinbaseProAPI(auth)
-
-            return calls, CoinbaseProInterface(exchange_name, calls)
-
-        elif exchange_name == 'binance':
-            if preferences["settings"]["use_sandbox"]:
-                calls = Client(api_key=auth.keys['API_KEY'], api_secret=auth.keys['API_SECRET'],
-                               tld=preferences["settings"]['binance']["binance_tld"],
-                               testnet=True)
-            else:
-                calls = Client(api_key=auth.keys['API_KEY'], api_secret=auth.keys['API_SECRET'],
-                               tld=preferences["settings"]['binance']["binance_tld"])
-            return calls, BinanceInterface(exchange_name, calls)
-
-        elif exchange_name == 'alpaca':
-            calls = create_alpaca_client(auth, preferences["settings"]["use_sandbox"])
-            return calls, AlpacaInterface(calls, preferences_path)
-
-        elif exchange_name == 'oanda':
-            calls = OandaAPI(auth, preferences["settings"]["use_sandbox"])
-            return calls, OandaInterface(calls, preferences_path)
-
-        elif exchange_name == 'paper_trade':
-            return None, None
diff --git a/blankly/exchanges/interfaces/exchange_interface.py b/blankly/exchanges/interfaces/exchange_interface.py
index 8047dd42..410ab557 100644
--- a/blankly/exchanges/interfaces/exchange_interface.py
+++ b/blankly/exchanges/interfaces/exchange_interface.py
@@ -199,13 +199,14 @@ def history(self,
             # Figure out the next point and then subtract to the last stamp
             most_recent_valid_resolution = utils.ceil_date(dt.now(),
                                                            seconds=resolution_seconds).timestamp() - resolution_seconds
-            # Binance is nice enough to update OHLCV data so we have to exclude that by subtracting a resolution
+            # Binance is nice enough to update OHLCV data, so we have to exclude that by subtracting a resolution
             epoch_stop = most_recent_valid_resolution - resolution_seconds
             count_from = most_recent_valid_resolution
         else:
             if isinstance(end_date, str):
                 parsed_date = parser.parse(end_date)
-            elif isinstance(end_date, float) or isinstance(end_date, numpy.int64) or isinstance(end_date, int):
+            elif isinstance(end_date, float) or isinstance(end_date, numpy.int64) or isinstance(end_date, int) or \
+                    isinstance(end_date, numpy.int32):
                 parsed_date = dt.fromtimestamp(end_date)
             else:
                 parsed_date = end_date
diff --git a/blankly/exchanges/interfaces/ftx/__init__.py b/blankly/exchanges/interfaces/ftx/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/blankly/exchanges/interfaces/ftx/ftx.py b/blankly/exchanges/interfaces/ftx/ftx.py
new file mode 100644
index 00000000..f934d151
--- /dev/null
+++ b/blankly/exchanges/interfaces/ftx/ftx.py
@@ -0,0 +1,68 @@
+"""
+    FTX definition & setup
+    Copyright (C) 2021 Blankly Finance
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+
+
+from blankly.exchanges.exchange import Exchange
+from blankly.exchanges.interfaces.ftx.ftx_api import FTXAPI
+from blankly.exchanges.auth.auth_constructor import AuthConstructor
+from blankly.exchanges.interfaces.paper_trade.paper_trade_interface import PaperTradeInterface
+from blankly.exchanges.interfaces.ftx.ftx_interface import FTXInterface
+from blankly.utils import utils
+
+
+class FTX(Exchange):
+    def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
+        Exchange.__init__(self, "ftx", portfolio_name, settings_path)
+
+        # Load the auth from the keys file
+        auth = AuthConstructor(keys_path, portfolio_name, 'ftx', ['API_KEY', 'API_SECRET'])
+
+        keys = auth.keys
+        sandbox = self.preferences["settings"]["use_sandbox"]
+        calls = FTXAPI(keys['API_KEY'], keys['API_SECRET'])
+
+        # Always finish the method with this function
+        super().construct_interface_and_cache(calls)
+
+        # FTX is unique because we can continue by wrapping the interface in paper trade
+        if sandbox:
+            utils.info_print('The setting use_sandbox is enabled. FTX has been created as a paper trading '
+                             'instance.')
+            self.interface = PaperTradeInterface(FTXInterface('ftx', calls))
+
+    """
+    Builds information about the asset on this exchange by making particular API calls
+    """
+
+    def get_asset_state(self, symbol):
+        """
+        This determines the internal properties of the exchange block.
+        Should be implemented per-class because it requires different types of interaction with each exchange.
+        """
+        # TODO Populate this with useful information
+        return self.interface.get_account(symbol)
+
+    def get_exchange_state(self):
+        """
+        Exchange state is the external properties for the exchange block
+        """
+        # TODO Populate this with useful information
+        return self.interface.get_fees()
+
+    def get_direct_calls(self) -> FTXAPI:
+        return self.calls
diff --git a/blankly/exchanges/interfaces/ftx/ftx_api.py b/blankly/exchanges/interfaces/ftx/ftx_api.py
new file mode 100644
index 00000000..1c0e3075
--- /dev/null
+++ b/blankly/exchanges/interfaces/ftx/ftx_api.py
@@ -0,0 +1,314 @@
+"""
+    General FTX Api definition
+    Copyright (C) 2021 Blankly Finance
+
+    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 requests
+from typing import Optional, Dict, Any, List
+import urllib.parse
+from blankly.utils.utils import epoch_from_iso8601
+import time
+import hmac
+
+
+class FTXAPI:
+    _API_URL = "https://ftx.us/api/"
+
+    # no option to instantiate with sandbox mode, unlike every other exchange
+    def __init__(self, api_key, api_secret, _subaccount_name=None):
+
+        self._ftx_session = requests.Session()
+        self._api_key = api_key
+        self._api_secret = api_secret
+        self._subaccount_name = _subaccount_name
+
+    def _signed_request(self, method: str, path: str, **kwargs):
+        request = requests.Request(method, self._API_URL + path, **kwargs)
+        self._get_signature(request)
+        result = self._ftx_session.send(request.prepare())
+        return self._handle_response(result)
+
+    def _get_signature(self, request: requests.Request):
+        ts = int(1000 * time.time())
+        prepared_request = request.prepare()
+        signed_data = f'{ts}{prepared_request.method}{prepared_request.path_url}'.encode()
+
+        if prepared_request.body:
+            signed_data += prepared_request.body
+
+        signature = hmac.new(self._api_secret.encode(), signed_data, 'sha256').hexdigest()
+
+        request.headers['FTXUS-KEY'] = self._api_key
+        request.headers['FTXUS-SIGN'] = signature
+        request.headers['FTXUS-TS'] = str(ts)
+
+        if self._subaccount_name:
+            request.headers['FTXUS-SUBACCOUNT'] = urllib.parse.quote(self._subaccount_name)
+
+    def _signed_delete(self, path: str, params: Optional[Dict[str, Any]] = None):
+        return self._signed_request('DELETE', path, json=params)
+
+    def _signed_get(self, path: str, params: Optional[Dict[str, Any]] = None):
+        return self._signed_request('GET', path, params=params)
+
+    def _signed_post(self, path: str, params: Optional[Dict[str, Any]] = None):
+        return self._signed_request('POST', path, json=params)
+
+    @staticmethod
+    def _handle_response(response: requests.Response):
+        try:
+            result = response.json()
+
+        except ValueError:
+            response.raise_for_status()
+            raise
+
+        else:
+            if not result['success']:
+                raise Exception(result['error'])
+            else:
+                return result['result']
+
+    def list_markets(self) -> List[dict]:
+
+        return self._signed_get('markets')
+
+    def get_market(self, symbol: str) -> dict:
+
+        # symbol format must be BTC/USD instead of BTC-USD
+        if '-' in symbol:
+            symbol = symbol.replace("-", "/")
+
+        return self._signed_get(f'markets/{symbol}')
+
+    def list_futures(self) -> List[dict]:
+        return self._signed_get('futures')
+
+    def get_product_history(self, symbol: str, epoch_start: float, epoch_stop: float, resolution) -> List[dict]:
+        return self._signed_get(
+            f"/markets/{symbol}/candles?resolution={resolution}&start_time={epoch_start}&end_time={epoch_stop}")
+
+    def get_orderbook(self, market: str, depth: int = None) -> dict:
+        return self._signed_get(f'markets/{market}/orderbook', {'depth': depth})
+
+    def get_trades(self, market: str) -> dict:
+        return self._signed_get(f'markets/{market}/trades')
+
+    def get_account_info(self) -> dict:
+        return self._signed_get('account')
+
+    def get_open_orders(self, market: str = None) -> List[dict]:
+        return self._signed_get('orders', {'market': market})
+
+    def get_order_by_id(self, order_id: str) -> dict:
+        return self._signed_get(f'orders/{order_id}')
+
+    def get_fills(self) -> List[dict]:
+        return self._signed_get('fills')
+
+    def get_balances(self) -> List[dict]:
+        return self._signed_get('wallet/balances')
+
+    def get_deposit_addresses(self, ticker: str) -> dict:
+        return self._signed_get(f'wallet/deposit_addresses/{ticker}')
+
+    def get_positions(self, display_price_avg: bool = False) -> List[dict]:
+        return self._signed_get('positions', {'showAvgPrice': display_price_avg})
+
+    def get_specific_position(self, pos_name: str, display_price_avg: bool = False) -> dict:
+        filtered = filter(lambda pos: pos['future'] == pos_name, self.get_positions(display_price_avg))
+        return next(filtered, None)
+
+    def get_order_history(
+            self,
+            market: str = None,
+            side: str = None,
+            order_type: str = None,
+            interval_start: float = None,
+            interval_end: float = None
+    ) -> List[dict]:
+        return self._signed_get('orders/history', {
+            'market': market,
+            'side': side,
+            'orderType': order_type,
+            'start_time': interval_start,
+            'end_time': interval_end})
+
+    def get_conditional_order_history(
+            self,
+            market: str = None,
+            side: str = None,
+            type_: str = None,
+            order_type: str = None,
+            interval_start: float = None,
+            interval_end: float = None
+    ) -> List[dict]:
+        return self._signed_get(f'conditional_orders/history', {
+            'market': market,
+            'side': side,
+            'type': type_,
+            'orderType': order_type,
+            'start_time': interval_start,
+            'end_time': interval_end
+        })
+
+    def get_conditional_orders(self, market: str = None) -> List[dict]:
+        return self._signed_get(f'conditional_orders', {'market': market})
+
+    def place_order(
+            self,
+            market: str,
+            side: str,
+            price: float,
+            size: float,
+            order_type: str = 'limit',
+            reduce_only: bool = False,
+            ioc: bool = False,
+            post_only: bool = False,
+            client_id: str = None
+    ) -> dict:
+        return self._signed_post('orders', {
+            'market': market,
+            'side': side,
+            'price': price,
+            'size': size,
+            'type': order_type,
+            'reduceOnly': reduce_only,
+            'ioc': ioc,
+            'postOnly': post_only,
+            'clientId': client_id
+        })
+
+    def cancel_order(self, order_id: str) -> dict:
+        return self._signed_delete(f'orders/{order_id}')
+
+    """
+        MUST SPECIFY EXACTLY 1 OF THE FOLLOWING
+        order_id XOR existing_client_order_id
+    """
+
+    def modify_order(
+            self,
+            order_id: Optional[str] = None,
+            existing_client_order_id: Optional[str] = None,
+            client_order_id: Optional[float] = None,
+            price: Optional[float] = None,
+            size: Optional[float] = None,
+    ) -> dict:
+
+        assert (price is None) or (size is None), \
+            'You must modify the price or the size'
+
+        assert (existing_client_order_id is None) ^ (order_id is None), \
+            'Specify exactly one ID to modify'
+
+        if order_id:
+            endpoint_path = f"orders/{order_id}/modify"
+        elif existing_client_order_id:
+            endpoint_path = f"orders/by_client_id/{existing_client_order_id}/modify"
+        else:
+            raise AttributeError('Either client_order_id or existing_client_order_id is required')
+
+        if size is None:
+            size_json = {}
+        else:
+            size_json = {'size': size}
+
+        if price is None:
+            price_json = {}
+        else:
+            price_json = {'price': price}
+
+        if client_order_id is None:
+            client_order_id_json = {}
+        else:
+            client_order_id_json = {'clientId': client_order_id}
+
+        return self._signed_post(endpoint_path, {
+            **size_json,
+            **price_json,
+            **client_order_id_json
+        })
+
+    """
+    to send...
+    stop market order: set type = "stop" and specify a trigger price
+    stop limit order: also supply a limit price
+    take profit market order: set type = "trailing_stop" and specify a trigger price
+    """
+
+    def place_conditional_order(
+            self,
+            market: str,
+            side: str,
+            size: float,
+            order_type: str = "stop",
+            limit_price: float = "None",
+            reduce_only: bool = False,
+            cancel_limit_on_trigger: bool = True,
+            trigger_price: float = None,
+            trail_value: float = None
+    ) -> dict:
+        assert type in ['stop', 'take_profit', 'trailing_stop']
+        assert type not in ['stop',
+                            'take_profit'] or trigger_price is not None, "stop losses and take profits requires a " \
+                                                                         "trigger price"
+        assert type not in ['trailing stop'] or (
+                    trail_value is not None and trigger_price is not None), "must specify trail value for trailing " \
+                                                                            "stop, cannot use trigger price"
+        return self._signed_post(
+            'conditional_orders',
+            {
+                'market': market,
+                'side': side,
+                'triggerPrice': trigger_price,
+                'size': size,
+                'reduceOnly': reduce_only,
+                'type': order_type,
+                'cancelLimitOnTrigger': cancel_limit_on_trigger,
+                'orderPrice': limit_price
+            }
+        )
+
+    def get_all_trades(self, market: str, interval_start: float = None, interval_end: float = None, lim=100) -> List:
+
+        to_return = []
+        set_of_ids = set()
+
+        while True:
+            resp = self._signed_get(f'markets/{market}/trades', {
+                'end_time': interval_end,
+                'start_time': interval_start
+            })
+
+            dd_trades = []
+
+            for elem in resp:
+                if elem['id'] not in set_of_ids:
+                    dd_trades.append(elem)
+
+            to_return.extend(dd_trades)
+
+            set_of_ids |= {elem['id'] for elem in dd_trades}
+
+            if len(resp) == 0:
+                break
+            interval_end = min(epoch_from_iso8601(time_['time']) for time_ in resp)
+            if lim > len(resp):
+                break
+
+        return to_return
diff --git a/blankly/exchanges/interfaces/ftx/ftx_interface.py b/blankly/exchanges/interfaces/ftx/ftx_interface.py
new file mode 100644
index 00000000..8f7d4183
--- /dev/null
+++ b/blankly/exchanges/interfaces/ftx/ftx_interface.py
@@ -0,0 +1,571 @@
+"""
+    Interface definition for homogenous calls
+    Copyright (C) 2021 Blankly Finance
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+
+
+import pandas as pd
+import time
+from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
+from blankly.exchanges.orders.market_order import MarketOrder
+from blankly.exchanges.orders.limit_order import LimitOrder
+from blankly.exchanges.interfaces.ftx.ftx_api import FTXAPI
+import blankly.utils.utils as utils
+import copy
+from typing import List
+
+
+class FTXInterface(ExchangeInterface):
+
+    # note, FTX has no sandbox mode
+    def __init__(self, exchange_name, authenticated_api: FTXAPI):
+        super().__init__(exchange_name, authenticated_api, valid_resolutions=None)
+
+    def init_exchange(self):
+        """
+            Attempts an API call. If API key is invalid, this 
+            method will throw another error specifying that it 
+            was due to invalid FTX API keys 
+        """
+        try:
+            self.get_calls().get_account_info()
+        except Exception as e:
+            if str(e) == "Not logged in: Invalid API key":
+                raise LookupError("Unable to connect to FTX US: Invalid API Key")
+            else:
+                raise e
+
+    """
+    needed:
+    ---
+    'get_products': [
+                ["symbol", str],
+                ["base_asset", str],
+                ["quote_asset", str],
+                ["base_min_size", float],
+                ["base_max_size", float],
+                ["base_increment", float]
+            ],
+
+    provided:
+    ---
+    {
+            "name": "ABNB/USD",
+            "enabled": true,
+            "postOnly": false,
+            "priceIncrement": 0.005, //NOTE: api call returns both price and size increment. base_increment is currently
+             set to size, however this may not be the expected result
+            "sizeIncrement": 0.025,
+            "minProvideSize": 0.025,
+            "last": 169.09,
+            "bid": 170.68,
+            "ask": 171.205,
+            "price": 170.68,
+            "type": "spot",
+            "baseCurrency": "ABNB",
+            "quoteCurrency": "USD",
+            "underlying": null,
+            "restricted": true,
+            "tokenizedEquity": true,
+            "highLeverageFeeExempt": true,
+            "change1h": -0.003590297440088736,
+            "change24h": 0.00014649438926489115,
+            "changeBod": -0.00367754363434709,
+            "quoteVolume24h": 12.76575,
+            "volumeUsd24h": 12.76575
+        },
+    """
+
+    # only includes markets of type "spot" (i.e. excludes futures)
+    def get_products(self) -> list:
+
+        needed = self.needed['get_products']
+
+        products: List[dict] = self.get_calls().list_markets()
+
+        end_products = []
+
+        for index, product in enumerate(products):
+            if product['type'] == "spot":
+                product['symbol'] = utils.to_blankly_symbol(product.pop('name'), 'ftx')
+                product['base_asset'] = product.pop('baseCurrency')
+                product['quote_asset'] = product.pop('quoteCurrency')
+                product['base_min_size'] = product.pop('minProvideSize')
+                product['base_max_size'] = 99999999
+                product['base_increment'] = product.pop('sizeIncrement')
+
+                product = utils.isolate_specific(needed, product)
+
+                end_products.append(product)
+
+            else:
+                continue
+
+        return end_products
+
+    def get_account(self, symbol: str = None) -> utils.AttributeDict:
+        """
+        Get all assets in an account, or sort by symbol/account_id
+        Args:
+            symbol (Optional): Filter by particular symbol
+
+            These arguments are mutually exclusive
+
+        TODO add return example
+        """
+
+        symbol = super().get_account(symbol=symbol)
+
+        needed = self.needed['get_account']
+        parsed_dictionary = utils.AttributeDict({})
+        balances: List[dict] = self.get_calls().get_balances()
+
+        if symbol is not None:
+            for account in balances:
+                if account["coin"] == symbol:
+                    account['hold'] = account['total'] - account['free']
+                    account['currency'] = account.pop('coin')
+                    account['available'] = account.pop('free')
+                    parsed_value = utils.isolate_specific(needed, account)
+                    requested_account = utils.AttributeDict({
+                        'available': parsed_value['available'],
+                        'hold': parsed_value['hold']
+                    })
+                    return requested_account
+            raise ValueError("Symbol not found")
+
+        else:
+            for account in balances:
+                account['hold'] = account['total'] - account['free']
+                account['currency'] = account.pop('coin')
+                account['available'] = account.pop('free')
+                parsed_dictionary[account['currency']] = utils.AttributeDict({
+                    'available': float(account['available']),
+                    'hold': float(account['hold'])
+                })
+
+            return utils.add_all_products(parsed_dictionary, self.get_products())
+
+    """
+    needed:
+
+    'market_order': [
+                ["symbol", str],
+                ["id", str],
+                ["created_at", float],
+                ["size", float],
+                ["status", str],
+                ["type", str],
+                ["side", str]
+            ],
+
+    response: 
+
+    {
+    "createdAt": "2019-03-05T09:56:55.728933+00:00",
+    "filledSize": 0,
+    "future": "XRP-PERP",
+    "id": 9596912,
+    "market": "XRP-PERP",
+    "price": 0.306525,
+    "remainingSize": 31431,
+    "side": "sell",
+    "size": 31431,
+    "status": "open",
+    "type": "limit",
+    "reduceOnly": false,
+    "ioc": false,
+    "postOnly": false,
+    "clientId": null,
+    }
+    """
+
+    @utils.order_protection
+    def market_order(self, symbol: str, side: str, size: float) -> MarketOrder:
+        """
+        Used for buying or selling market orders
+        Args:
+            symbol: asset to buy
+            side: buy/sell
+            size: desired amount of base asset to use
+        """
+        needed = self.needed['market_order']
+
+        response = self.get_calls().place_order(symbol, side, None, size, order_type="market")
+
+        response["symbol"] = utils.to_blankly_symbol(response.pop("market"), 'ftx')
+        response["created_at"] = utils.epoch_from_iso8601(response.pop("createdAt"))
+
+        response = utils.isolate_specific(needed, response)
+
+        order = {
+            'size': size,
+            'side': side,
+            'symbol': symbol,
+            'type': 'market'
+        }
+
+        return MarketOrder(order, response, self)
+
+    """
+    needed
+
+    'limit_order': [
+                ["symbol", str],
+                ["id", str],
+                ["created_at", float],
+                ["price", float],
+                ["size", float],
+                ["status", str],
+                ["time_in_force", str],
+                ["type", str],
+                ["side", str]
+                ],
+
+    response
+
+    {
+    "success": true,
+    "result": {
+        "createdAt": "2019-03-05T09:56:55.728933+00:00",
+        "filledSize": 0,
+        "future": "XRP-PERP",
+        "id": 9596912,
+        "market": "XRP-PERP",
+        "price": 0.306525,
+        "remainingSize": 31431,
+        "side": "sell",
+        "size": 31431,
+        "status": "open",
+        "type": "limit",
+        "reduceOnly": false,
+        "ioc": false,
+        "postOnly": false,
+        "clientId": null,
+        }
+    }
+    """
+
+    @utils.order_protection
+    def limit_order(self,
+                    symbol: str,
+                    side: str,
+                    price: float,
+                    size: float) -> LimitOrder:
+        """
+        Used for buying or selling limit orders
+        Args:
+            symbol: asset to buy
+            side: buy/sell
+            price: price to set limit order
+            size: amount of asset (like BTC) for the limit to be valued
+        """
+        needed = self.needed['limit_order']
+        response = self.get_calls().place_order(symbol, side, price, size, order_type="limit")
+
+        order = {
+            'size': size,
+            'side': side,
+            'price': price,
+            'symbol': symbol,
+            'type': 'limit'
+        }
+
+        response["symbol"] = utils.to_blankly_symbol(response.pop("market"), 'ftx')
+        response["created_at"] = utils.epoch_from_iso8601(response.pop("createdAt"))
+        response["time_in_force"] = "GTC"
+        response = utils.isolate_specific(needed, response)
+
+        return LimitOrder(order, response, self)
+
+    def cancel_order(self, symbol: str, order_id: str) -> dict:
+        """
+        Cancel an order on a particular symbol & order id
+
+        Args:
+            symbol: This is the asset id that the order is under
+            order_id: The unique ID of the order.
+
+        """
+        self.get_calls().cancel_order(order_id)
+        return {'order_id': order_id}
+
+    """
+    [
+        {
+        "createdAt": "2019-03-05T09:56:55.728933+00:00",
+        "filledSize": 10,
+        "future": "XRP-PERP",
+        "id": 9596912,
+        "market": "XRP-PERP",
+        "price": 0.306525,
+        "avgFillPrice": 0.306526,
+        "remainingSize": 31421,
+        "side": "sell",
+        "size": 31431,
+        "status": "open",
+        "type": "limit",
+        "reduceOnly": false,
+        "ioc": false,
+        "postOnly": false,
+        "clientId": null
+        },
+        {
+            ...
+        },
+        ...
+        {
+            ...
+        }
+
+    ]
+    """
+
+    def get_open_orders(self, symbol: str = None) -> list:
+        """
+        List open orders.
+        Args:
+            symbol (optional) (str): Asset such as BTC-USD
+        """
+        response = self.get_calls().get_open_orders(symbol)
+        response_needed_fulfilled = []
+        if len(response) == 0:
+            return []
+
+        for open_order in response:
+            open_order['symbol'] = utils.to_blankly_symbol(open_order.pop('market'), 'ftx')
+            if open_order["type"] == "limit":
+                needed = self.choose_order_specificity("limit")
+                open_order['time_in_force'] = "GTC"
+            elif open_order["type"] == "market":
+                needed = self.choose_order_specificity("market")
+            else:
+                print(f"Order type {open_order['type']} is not supported.")
+                continue
+
+            open_order['created_at'] = utils.epoch_from_iso8601(open_order.pop('createdAt'))
+            open_order = utils.isolate_specific(needed, open_order)
+            response_needed_fulfilled.append(open_order)
+
+        return response
+
+    """
+    {
+        "createdAt": "2019-03-05T09:56:55.728933+00:00",
+        "filledSize": 10,
+        "future": "XRP-PERP",
+        "id": 9596912,
+        "market": "XRP-PERP",
+        "price": 0.306525,
+        "avgFillPrice": 0.306526,
+        "remainingSize": 31421,
+        "side": "sell",
+        "size": 31431,
+        "status": "open",
+        "type": "limit",
+        "reduceOnly": false,
+        "ioc": false,
+        "postOnly": false,
+        "clientId": "your_client_order_id"
+    }
+    """
+
+    def get_order(self, symbol: str, order_id: str) -> dict:
+        """
+        Get a certain order
+        Args:
+            symbol: Asset that the order is under
+            order_id: The unique ID of the order.
+        """
+        response = self.get_calls().get_order_by_id(order_id)
+
+        needed = self.choose_order_specificity(response["type"])
+
+        response['symbol'] = utils.to_blankly_symbol(response.pop('market'), 'ftx')
+        response['created_at'] = utils.epoch_from_iso8601(response.pop('createdAt'))
+
+        if response["type"] == "limit":
+            response['time_in_force'] = "GTC"
+
+        response = utils.isolate_specific(needed, response)
+        return response
+
+    def get_fees(self) -> dict:
+        """
+        Get market fees
+
+        lot of unnecessary info in exchange-specific section
+
+        TODO: remove everything truly irrelevant to fees
+        """
+
+        needed = self.needed['get_fees']
+        account_info = self.get_calls().get_account_info()
+        account_info['maker_fee_rate'] = account_info.pop('makerFee')
+        account_info['taker_fee_rate'] = account_info.pop('takerFee')
+        account_info = utils.isolate_specific(needed, account_info)
+
+        return account_info
+
+    def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
+        """
+        Returns the product history from an exchange
+        Args:
+            symbol: blankly product ID format (BTC-USD)
+            epoch_start: Time to begin download
+            epoch_stop: Time to stop download
+            resolution: Resolution in seconds between tick (ex: 60 = 1 per minute)
+        Returns:
+            Dataframe with *at least* 'time (epoch)', 'low', 'high', 'open', 'close', 'volume' as columns.
+        """
+
+        if "-" in symbol:
+            symbol = symbol.replace("-", "/")
+
+        # epoch_start, epoch_stop = super().get_product_history(symbol, epoch_start, epoch_stop, resolution)
+        epoch_start = utils.convert_epochs(epoch_start)
+        epoch_stop = utils.convert_epochs(epoch_stop)
+
+        accepted_grans = [15, 60, 300, 900, 3600, 14400, 86400]
+
+        # ftx accepts the above resolutions plus any multiple of 86400 up to 30 * 86400
+        for i in range(2, 31):
+            accepted_grans.append(i * 86400)
+
+        if resolution not in accepted_grans:
+            utils.info_print("Granularity is not an accepted granularity...rounding to nearest valid value.")
+            resolution = accepted_grans[min(range(len(accepted_grans)),
+                                            key=lambda j: abs(accepted_grans[j] - resolution))]
+
+        # Figure out how many points are needed
+        need = int((epoch_stop - epoch_start) / resolution)
+        initial_need = need
+        window_open = epoch_start
+        history = []
+        # Iterate while it's more than max
+        while need > 1500:
+            # Close is always 1500 points ahead
+            window_close = window_open + 1500 * resolution
+
+            response = self.get_calls().get_product_history(symbol, window_open, window_close, resolution)
+
+            history = history + response
+
+            window_open = window_close
+            need -= 1500
+            time.sleep(.2)
+            utils.update_progress((initial_need - need) / initial_need)
+
+        # Fill the remainder
+        window_close = epoch_stop
+        response = self.get_calls().get_product_history(symbol, window_open, window_close, resolution)
+        history_block = history + response
+        # print(history_block)
+        history_block.sort(key=lambda x: x["time"])
+
+        df = pd.DataFrame(history_block, columns=['time', 'low', 'high', 'open', 'close', 'volume'])
+        # df[['time']] = df[['time']].astype(int)
+
+        df["time"] = df["time"].apply(lambda x: x / 1000)
+        df[['time']] = df[['time']].astype(int)
+
+        df[['low', 'high', 'open', 'close', 'volume']] = df[['low', 'high', 'open', 'close', 'volume']].astype(float)
+
+        return df
+
+    def get_order_filter(self, symbol: str):
+        """
+        Find order limits for the exchange
+        Args:
+            symbol: The asset such as (BTC-USD, or MSFT)
+
+            {
+                "name": "BTC/USD", <---needed
+                "enabled": true,
+                "postOnly": false,
+                "priceIncrement": 1.0, <------ needed
+                "sizeIncrement": 0.0001, <------ needed
+                "minProvideSize": 0.0001, <------ needed
+                "last": 47534.0,
+                "bid": 47536.0,
+                "ask": 47540.0,
+                "price": 47536.0,
+                "type": "spot",
+                "baseCurrency": "BTC", <------ needed
+                "quoteCurrency": "USD", <------ needed
+                "underlying": null,
+                "restricted": false,
+                "highLeverageFeeExempt": true,
+                "change1h": -0.007184628237259816,
+                "change24h": -0.003438155136268344,
+                "changeBod": 0.02373260972563208,
+                "quoteVolume24h": 57477234.2424,
+                "volumeUsd24h": 57477234.2424
+            }
+
+        """
+        if "-" in symbol:
+            symbol = symbol.replace("-", "/")
+
+        market_info = self.get_calls().get_market(symbol)
+
+        exchange_specific_keys = copy.deepcopy(market_info)
+
+        keys_to_remove = ["name", "baseCurrency", "quoteCurrency", "minProvideSize", "sizeIncrement", "priceIncrement",
+                          "price"]
+
+        for key in keys_to_remove:
+            exchange_specific_keys.pop(key)
+
+        return {
+            "symbol": utils.to_blankly_symbol(market_info["name"], 'ftx'),  # make function in utils (or static)
+                                                                            # to switch slash to dash
+            "base_asset": market_info["baseCurrency"],
+            "quote_asset": market_info["quoteCurrency"],
+            "max_orders": 99999999,
+            "limit_order": {
+                "base_min_size": market_info["minProvideSize"],
+                "base_max_size": 99999999,
+                "base_increment": market_info["sizeIncrement"],
+                "price_increment": market_info["priceIncrement"],
+                "min_price": .75 * market_info["price"],
+                "max_price": 1.25 * market_info["price"]
+            },
+            "market_order": {
+                "base_min_size": market_info["minProvideSize"],
+                "base_max_size": 99999999,
+                "base_increment": market_info["sizeIncrement"],
+                "quote_increment": market_info["priceIncrement"],
+                "buy": {
+                    "min_funds": market_info["sizeIncrement"],
+                    "max_funds": market_info["sizeIncrement"]
+                },
+                "sell": {
+                    "min_funds": market_info["sizeIncrement"],
+                    "max_funds": market_info["sizeIncrement"]
+                },
+
+            },
+            "exchange_specific": exchange_specific_keys
+        }
+
+    def get_price(self, symbol: str) -> float:
+        """
+        Returns just the price of a symbol.
+        Args:
+            symbol: The asset such as (BTC-USD, or MSFT)
+        """
+        return float(self.get_calls().get_market(symbol)['price'])
diff --git a/blankly/exchanges/interfaces/ftx/ftx_websocket.py b/blankly/exchanges/interfaces/ftx/ftx_websocket.py
new file mode 100644
index 00000000..41d9dbfe
--- /dev/null
+++ b/blankly/exchanges/interfaces/ftx/ftx_websocket.py
@@ -0,0 +1,132 @@
+"""
+    FTX websocket definitions
+    Copyright (C) 2021 Blankly Finance
+
+    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 json
+import traceback
+
+import blankly.exchanges.interfaces.ftx.ftx_websocket_utils as websocket_utils
+from blankly.exchanges.interfaces.websocket import Websocket
+from blankly.utils.utils import info_print
+
+
+# def create_ticker_connection(id_, url, channel):
+#     ws = create_connection(url, sslopt={"cert_reqs": ssl.CERT_NONE})
+#     request = json.dumps({
+#         "op": "subscribe",
+#         "channel": channel,
+#         "market": id_
+#     })
+#     ws.send(request)
+#     return ws
+
+
+# This could be needed:
+# "channels": [
+#     {
+#         "name": "ticker",
+#         "product_ids": [
+#             \"""" + id + """\"
+#         ]
+#     }
+# ]
+
+
+class Tickers(Websocket):
+    def __init__(self, symbol, stream, log=None,
+                 pre_event_callback=None, initially_stopped=False, WEBSOCKET_URL="wss://ftx.com/ws/"):
+        """
+        Create and initialize the ticker
+        Args:
+            symbol: Currency to initialize on such as "BTC-USD"
+            log: Fill this with a path to a log file that should be created
+            WEBSOCKET_URL: Default websocket URL feed.
+        """
+        self.__logging_callback, self.__interface_callback, log_message = websocket_utils.switch_type(stream)
+
+        super().__init__(symbol, stream, log, log_message, WEBSOCKET_URL, pre_event_callback)
+
+        # Start the websocket
+        if not initially_stopped:
+            self.start_websocket(
+                self.on_open,
+                self.on_message,
+                self.on_error,
+                self.on_close,
+                self.read_websocket
+            )
+
+    def read_websocket(self):
+        """
+        This is the target that runs
+        """
+        self.ws.run_forever()
+
+    def on_message(self, ws, message):
+        """
+        Behavior for this exchange
+        """
+        received_dict = json.loads(message)
+        if received_dict['type'] == 'subscribed':
+            info_print(f"Subscribed to {received_dict['channel']}")
+            return
+
+        parsed_received_trades = websocket_utils.process_trades(received_dict)
+        for received in parsed_received_trades:
+            # ISO8601 is converted to epoch in process_trades
+            self.most_recent_time = received["time"]
+            self.time_feed.append(self.most_recent_time)
+
+            self.log_response(self.__logging_callback, received)
+
+            # Manage price events and fire for each manager attached
+            interface_message = self.__interface_callback(received)
+            self.ticker_feed.append(interface_message)
+            self.most_recent_tick = interface_message
+
+            try:
+                for i in self.callbacks:
+                    i(interface_message)
+            except Exception as e:
+                info_print(e)
+                traceback.print_exc()
+
+            self.message_count += 1
+
+    def on_error(self, ws, error):
+        info_print(error)
+
+    def on_close(self, ws):
+        # This repeats the close behavior just in case something happens
+        pass
+
+    def on_open(self, ws):
+        request = json.dumps({
+            "op": "subscribe",
+            "channel": self.stream,
+            "market": self.symbol
+        })
+        ws.send(request)
+
+    def restart_ticker(self):
+        self.start_websocket(
+            self.on_open,
+            self.on_message,
+            self.on_error,
+            self.on_close,
+            self.read_websocket
+        )
diff --git a/blankly/exchanges/interfaces/ftx/ftx_websocket_utils.py b/blankly/exchanges/interfaces/ftx/ftx_websocket_utils.py
new file mode 100644
index 00000000..b5fa5368
--- /dev/null
+++ b/blankly/exchanges/interfaces/ftx/ftx_websocket_utils.py
@@ -0,0 +1,78 @@
+"""
+    Utilities for defining how websockets should integrate
+    Copyright (C) 2021 Blankly Finance
+
+    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
+from typing import List
+
+
+def switch_type(stream):
+    if stream == "ticker":
+        return trade, \
+               process_trades, \
+               "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
+
+
+"""
+Homogenizes response with uhh coinbase and binance
+"""
+
+
+def process_trades(response: dict) -> List[dict]:
+    list_trades: List = []
+    num_trades: int = len(response['data'])
+    trades_data: List[dict] = response['data']
+
+    needed = [
+        ["symbol", str],
+        ["price", float],
+        ["time", float],
+        ["trade_id", int],
+        ["size", float]
+    ]
+
+    for trade_ in trades_data:
+        trade_['symbol'] = response['market']
+        trade_['trade_id'] = trade_.pop('id')
+        trade_['time'] = utils.epoch_from_iso8601(trade_['time'])
+        trade_['symbol'] = trade_['symbol'].replace('/', '-')
+
+        list_trades.append(utils.isolate_specific(needed, trade_))
+
+    assert (num_trades == len(list_trades))
+
+    return list_trades
+
+
+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
diff --git a/blankly/exchanges/interfaces/kucoin/__init__.py b/blankly/exchanges/interfaces/kucoin/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/blankly/exchanges/interfaces/kucoin/kucoin.py b/blankly/exchanges/interfaces/kucoin/kucoin.py
new file mode 100644
index 00000000..825d9cbf
--- /dev/null
+++ b/blankly/exchanges/interfaces/kucoin/kucoin.py
@@ -0,0 +1,67 @@
+"""
+    Kucoin interface definition
+    Copyright (C) 2021  Emerson Dove
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+
+
+from blankly.exchanges.exchange import Exchange
+from blankly.exchanges.auth.auth_constructor import AuthConstructor
+
+
+class Kucoin(Exchange):
+    def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
+        Exchange.__init__(self, "kucoin", portfolio_name, settings_path)
+
+        # Load the auth from the keys file
+        auth = AuthConstructor(keys_path, portfolio_name, 'kucoin', ['API_KEY', 'API_SECRET', 'API_PASS'])
+
+        sandbox = self.preferences["settings"]["use_sandbox"]
+        try:
+            from kucoin import client as KucoinAPI
+        except ImportError:
+            raise ImportError("Please \"pip install kucoin-python\" to use kucoin with blankly.")
+        # Kucoin has a bunch of calls types, so we will index them in a single calls dictionary
+        calls = {
+            'market': KucoinAPI.Market(auth.keys['API_KEY'], auth.keys['API_SECRET'], auth.keys['API_PASS'], sandbox),
+            'user': KucoinAPI.User(auth.keys['API_KEY'], auth.keys['API_SECRET'], auth.keys['API_PASS'], sandbox),
+            'trade': KucoinAPI.Trade(auth.keys['API_KEY'], auth.keys['API_SECRET'], auth.keys['API_PASS'], sandbox)
+        }
+
+        # Always finish the method with this function
+        super().construct_interface_and_cache(calls)
+
+    """
+    Builds information about the asset on this exchange by making particular API calls
+    """
+
+    def get_asset_state(self, symbol):
+        """
+        This determines the internal properties of the exchange block.
+        Should be implemented per-class because it requires different types of interaction with each exchange.
+        """
+        # TODO Populate this with useful information
+        return self.interface.get_account(symbol)
+
+    def get_exchange_state(self):
+        """
+        Exchange state is the external properties for the exchange block
+        """
+        # TODO Populate this with useful information
+        return self.interface.get_fees()
+
+    def get_direct_calls(self) -> dict:
+        return self.calls
+
diff --git a/blankly/exchanges/interfaces/kucoin/kucoin_interface.py b/blankly/exchanges/interfaces/kucoin/kucoin_interface.py
new file mode 100644
index 00000000..4611b4c7
--- /dev/null
+++ b/blankly/exchanges/interfaces/kucoin/kucoin_interface.py
@@ -0,0 +1,643 @@
+"""
+    Base authentication for kucoin
+    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 pandas as pd
+
+import blankly.utils.time_builder
+import blankly.utils.utils as utils
+from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
+from blankly.exchanges.orders.limit_order import LimitOrder
+from blankly.exchanges.orders.market_order import MarketOrder
+from blankly.utils.exceptions import APIException, InvalidOrder
+
+
+class KucoinInterface(ExchangeInterface):
+    def __init__(self, exchange_name, authenticated_api):
+
+        super().__init__(exchange_name, authenticated_api, valid_resolutions=[60, 180, 300, 900, 1800,
+                                                                              3600, 7200, 14400, 21600,
+                                                                              28800, 43200, 86400, 604800])
+
+        try:
+            from kucoin import client as KucoinAPI
+        except ImportError:
+            raise ImportError("Please \"pip install kucoin-python\" to use kucoin with blankly.")
+
+        self._market: KucoinAPI.Market = self.calls['market']
+        self._trade: KucoinAPI.Trade = self.calls['trade']
+        self._user: KucoinAPI.User = self.calls['user']
+
+    def init_exchange(self):
+        fees = self.calls['user'].get_base_fee()
+        try:
+            if fees['msg'] == "Invalid API Key":
+                raise LookupError("Invalid API Key - are you trying to use your normal exchange keys "
+                                  "while in sandbox mode? \nTry toggling the \'use_sandbox\' setting "
+                                  "in your settings.json or check if the keys were input correctly into your "
+                                  "keys.json.")
+        except KeyError:
+            pass
+
+    def get_products(self):
+        needed = self.needed['get_products']
+        """
+        [
+          {
+            "symbol": "BTC-USDT",
+            "name": "BTC-USDT",
+            "baseCurrency": "BTC",
+            "quoteCurrency": "USDT",
+            "baseMinSize": "0.00000001",
+            "quoteMinSize": "0.01",
+            "baseMaxSize": "10000",
+            "quoteMaxSize": "100000",
+            "baseIncrement": "0.00000001",
+            "quoteIncrement": "0.01",
+            "priceIncrement": "0.00000001",
+            "feeCurrency": "USDT",
+            "enableTrading": true,
+            "isMarginEnabled": true,
+            "priceLimitRate": "0.1"
+          }
+        ]
+        """
+        products = self._market.get_symbol_list()
+        for i in range(len(products)):
+            products[i]["base_asset"] = products[i].pop("baseCurrency")
+            products[i]["quote_asset"] = products[i].pop("quoteCurrency")
+            products[i]["base_min_size"] = products[i].pop("baseMinSize")
+            products[i]["base_max_size"] = products[i].pop("baseMaxSize")
+            products[i]["base_increment"] = products[i].pop("baseIncrement")
+            products[i] = utils.isolate_specific(needed, products[i])
+        return products
+
+    def get_account(self, symbol=None) -> utils.AttributeDict:
+        """
+            Get all currencies in an account, or sort by symbol
+            Args:
+                symbol (Optional): Filter by particular symbol
+                These arguments are mutually exclusive
+        """
+        symbol = super().get_account(symbol=symbol)
+
+        """
+        [
+              {
+                "id": "5bd6e9286d99522a52e458de",  //accountId
+                "currency": "BTC",  //Currency
+                "type": "main",     //Account type, including main and trade
+                "balance": "237582.04299",  //Total assets of a currency
+                "available": "237582.032",  //Available assets of a currency
+                "holds": "0.01099" //Hold assets of a currency
+              },
+              {
+                ...
+              }
+        """
+        accounts = self._user.get_account_list()
+        trade_accounts = []
+        # Filter for only the trade accounts
+        for i in accounts:
+            if i['type'] == 'trade':
+                trade_accounts.append(i)
+
+        # If we have no positions this is a case that things can go wrong
+        if isinstance(trade_accounts, dict):
+            trade_accounts = []
+
+        # We have to sort through it if the accounts are none
+        if symbol is not None:
+            for i in trade_accounts:
+                if i["currency"] == symbol:
+                    return utils.AttributeDict({
+                        'available': float(i['available']),
+                        'hold': float(i['holds'])
+                    })
+
+        parsed_dictionary = utils.AttributeDict({})
+
+        # If the first loop didn't find it we'll try again here
+        for i in range(len(trade_accounts)):
+            parsed_dictionary[trade_accounts[i]['currency']] = utils.AttributeDict({
+                'available': float(trade_accounts[i]['available']),
+                'hold': float(trade_accounts[i]['holds'])
+            })
+
+        parsed_dictionary = utils.add_all_products(parsed_dictionary, self.get_products())
+
+        if symbol is not None:
+            if symbol in parsed_dictionary:
+                return parsed_dictionary[symbol]
+            else:
+                raise KeyError(f'Could not find account for asset {symbol}')
+
+        return parsed_dictionary
+
+    @utils.order_protection
+    def market_order(self, symbol, side, size) -> MarketOrder:
+        """
+            Used for buying or selling market orders
+            Args:
+                symbol: currency to buy
+                side: buy/sell
+                size: desired amount of base currency to buy or sell
+        """
+        needed = self.needed['market_order']
+        """
+        {
+            "orderId": "5bd6e9286d99522a52e458de"
+        }
+        """
+        order = {
+            'symbol': symbol,
+            'side': side,
+            'size': size,
+            'type': 'market',
+        }
+
+        response = self._trade.create_market_order(symbol, side, size=size)
+        if "msg" in response:
+            raise InvalidOrder("Invalid Order: " + response["msg"])
+
+        response_details = self._trade.get_order_details(response['orderId'])
+        """
+        {
+            "id": "5c35c02703aa673ceec2a168", //
+            "symbol": "BTC-USDT", //
+            "opType": "DEAL",
+            "type": "limit", //
+            "side": "buy", //
+            "price": "10",
+            "size": "2", //
+            "funds": "0",
+            "dealFunds": "0.166",
+            "dealSize": "2",
+            "fee": "0",
+            "feeCurrency": "USDT",
+            "stp": "",
+            "stop": "",
+            "stopTriggered": false,
+            "stopPrice": "0",
+            "timeInForce": "GTC",
+            "postOnly": false,
+            "hidden": false,
+            "iceberg": false,
+            "visibleSize": "0",
+            "cancelAfter": 0,
+            "channel": "IOS",
+            "clientOid": "",
+            "remark": "",
+            "tags": "",
+            "isActive": false, //
+            "cancelExist": false,
+            "createdAt": 1547026471000, //
+            "tradeType": "TRADE"
+        }
+        """
+
+        response_details["created_at"] = response_details.pop('createdAt')
+        response_details["status"] = response_details.pop('isActive')
+        response_details = utils.isolate_specific(needed, response_details)
+        return MarketOrder(order, response_details, self)
+
+    @utils.order_protection
+    def limit_order(self, symbol, side, price, size) -> LimitOrder:
+        """
+            Used for buying or selling limit orders
+            Args:
+                symbol: currency to buy
+                side: buy/sell
+                price: price to set limit order
+                size: amount of currency (like BTC) for the limit to be valued
+        """
+        needed = self.needed['limit_order']
+
+        order = {
+            'symbol': symbol,
+            'side': side,
+            'size': size,
+            'price': price,
+            'type': 'limit',
+        }
+        response = self._trade.create_limit_order(symbol, side, size, price)
+        if "msg" in response:
+            raise InvalidOrder("Invalid Order: " + response["msg"])
+
+        response_details = self._trade.get_order_details(response['orderId'])
+        """
+               {
+                   "id": "5c35c02703aa673ceec2a168", //
+                   "symbol": "BTC-USDT", //
+                   "opType": "DEAL",
+                   "type": "limit", //
+                   "side": "buy", //
+                   "price": "10",
+                   "size": "2", //
+                   "funds": "0",
+                   "dealFunds": "0.166",
+                   "dealSize": "2",
+                   "fee": "0",
+                   "feeCurrency": "USDT",
+                   "stp": "",
+                   "stop": "",
+                   "stopTriggered": false,
+                   "stopPrice": "0",
+                   "timeInForce": "GTC", //
+                   "postOnly": false,
+                   "hidden": false,
+                   "iceberg": false,
+                   "visibleSize": "0",
+                   "cancelAfter": 0,
+                   "channel": "IOS",
+                   "clientOid": "",
+                   "remark": "",
+                   "tags": "",
+                   "isActive": false, //
+                   "cancelExist": false,
+                   "createdAt": 1547026471000, //
+                   "tradeType": "TRADE"
+               }
+               """
+
+        response_details["created_at"] = response_details.pop('createdAt')
+        response_details["time_in_force"] = response_details.pop('timeInForce')
+        response_details["status"] = response_details.pop('isActive')
+        if response_details["status"]:
+            response_details["status"] = 'pending'
+        else:
+            response_details["status"] = 'done'
+        response_details = utils.isolate_specific(needed, response_details)
+        return LimitOrder(order, response_details, self)
+
+    def cancel_order(self, symbol, order_id) -> dict:
+        """
+        Returns:
+           dict: Containing the order_id of cancelled order. Example::
+           { "cancelledOrderIds": ["c5ab5eae-76be-480e-8961-00792dc7e138"]}
+        """
+        return {"order_id": self._trade.cancel_order(order_id)['cancelledOrderIds'][0]}
+
+    def get_open_orders(self, symbol=None):
+        """
+        List open orders.
+        """
+        """
+        {
+            "currentPage": 1,
+            "pageSize": 1,
+            "totalNum": 153408,
+            "totalPage": 153408,
+            "items": [
+              {
+                "id": "5c35c02703aa673ceec2a168",   //orderid
+                "symbol": "BTC-USDT",   //symbol
+                "opType": "DEAL",      // operation type: DEAL
+                "type": "limit",       // order type,e.g. limit,market,stop_limit.
+                "side": "buy",         // transaction direction,include buy and sell
+                "price": "10",         // order price
+                "size": "2",           // order quantity
+                "funds": "0",          // order funds
+                "dealFunds": "0.166",  // deal funds
+                "dealSize": "2",       // deal quantity
+                "fee": "0",            // fee
+                "feeCurrency": "USDT", // charge fee currency
+                "stp": "",             // self trade prevention,include CN,CO,DC,CB
+                "stop": "",            // stop type
+                "stopTriggered": false,  // stop order is triggered
+                "stopPrice": "0",      // stop price
+                "timeInForce": "GTC",  // time InForce,include GTC,GTT,IOC,FOK
+                "postOnly": false,     // postOnly
+                "hidden": false,       // hidden order
+                "iceberg": false,      // iceberg order
+                "visibleSize": "0",    // display quantity for iceberg order
+                "cancelAfter": 0,      // cancel orders time，requires timeInForce to be GTT
+                "channel": "IOS",      // order source
+                "clientOid": "",       // user-entered order unique mark
+                "remark": "",          // remark
+                "tags": "",            // tag order source
+                "isActive": false,     // status before unfilled or uncancelled
+                "cancelExist": false,   // order cancellation transaction record
+                "createdAt": 1547026471000,  // create time
+                "tradeType": "TRADE"
+              }
+            ]
+         }
+        """
+        if symbol is None:
+            open_orders = list(self._trade.get_order_list(status='active')["items"])
+        else:
+            open_orders = list(self._trade.get_order_list(status='active', symbol=symbol)["items"])
+
+        if len(open_orders) == 0:
+            return []
+        if open_orders[0] == 'msg':
+            raise APIException("Error: " + str(open_orders))
+
+        for i in range(len(open_orders)):
+            # open_orders[i]["createdAt"] = utils.epoch_from_ISO8601(open_orders[i]["createdAt"])
+            needed = self.choose_order_specificity(open_orders[i]['type'])
+            open_orders[i]["time_in_force"] = open_orders[i].pop('timeInForce')
+            open_orders[i]["created_at"] = open_orders[i].pop('createdAt')
+            open_orders[i]["status"] = open_orders[i].pop('isActive')
+            if open_orders[i]["status"]:
+                open_orders[i]["status"] = 'pending'
+            else:
+                open_orders[i]["status"] = 'done'
+
+            open_orders[i] = utils.isolate_specific(needed, open_orders[i])
+
+        return open_orders
+
+    def get_order(self, symbol, order_id) -> dict:
+        """
+        {
+            "id": "5c35c02703aa673ceec2a168",
+            "symbol": "BTC-USDT",
+            "opType": "DEAL",
+            "type": "limit",
+            "side": "buy",
+            "price": "10",
+            "size": "2",
+            "funds": "0",
+            "dealFunds": "0.166",
+            "dealSize": "2",
+            "fee": "0",
+            "feeCurrency": "USDT",
+            "stp": "",
+            "stop": "",
+            "stopTriggered": false,
+            "stopPrice": "0",
+            "timeInForce": "GTC",
+            "postOnly": false,
+            "hidden": false,
+            "iceberg": false,
+            "visibleSize": "0",
+            "cancelAfter": 0,
+            "channel": "IOS",
+            "clientOid": "",
+            "remark": "",
+            "tags": "",
+            "isActive": false,
+            "cancelExist": false,
+            "createdAt": 1547026471000,
+            "tradeType": "TRADE"
+        }
+        """
+        response = self._trade.get_order_details(order_id)
+
+        if 'msg' in response:
+            raise APIException("Invalid: " + str(response['msg']) + ", was the order canceled?")
+
+        if isinstance(response['createdAt'], str):
+            response["createdAt"] = utils.epoch_from_iso8601(response["createdAt"])
+
+        if response['type'] == 'market':
+            needed = self.needed['market_order']
+        elif response['type'] == 'limit':
+            needed = self.needed['limit_order']
+        else:
+            needed = self.needed['market_order']
+
+        response["created_at"] = response.pop('createdAt')
+        response["status"] = response.pop('isActive')
+        if response["status"]:
+            response["status"] = 'pending'
+        else:
+            response["status"] = 'done'
+        response["time_in_force"] = response.pop('timeInForce')
+
+        return utils.isolate_specific(needed, response)
+
+    def get_fees(self) -> dict:
+        needed = self.needed['get_fees']
+        """
+            {
+                "code": "200000",
+                "data": {
+                    "takerFeeRate": "0.001",
+                    "makerFeeRate": "0.001"
+                }
+            }
+        """
+        fees = self._user.get_base_fee()
+        fees["taker_fee_rate"] = fees.pop('takerFeeRate')
+        fees["maker_fee_rate"] = fees.pop('makerFeeRate')
+        return utils.isolate_specific(needed, fees)
+
+    def overridden_history(self, symbol, epoch_start, epoch_stop, resolution, **kwargs) -> pd.DataFrame:
+        """
+        Kucoin is strange because it's exclusive instead of inclusive. This generally invovles adding an extra
+        datapoint so this is here to do some of that work
+        """
+        to = kwargs['to']
+
+        if isinstance(to, str):
+            epoch_start -= resolution
+        elif isinstance(to, int):
+            epoch_start = epoch_stop - (to * resolution)
+
+        return self.get_product_history(symbol, epoch_start, epoch_stop, resolution)
+
+    def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
+        """
+        Returns the product history from an exchange
+        Args:
+            symbol: blankly product ID format (BTC-USD)
+            epoch_start: Time to begin download
+            epoch_stop: Time to stop download
+            resolution: Resolution in seconds between tick (ex: 60 = 1 per minute)
+        Returns:
+            Dataframe with *at least* 'time (epoch)', 'low', 'high', 'open', 'close', 'volume' as columns.
+        """
+        resolution = blankly.time_builder.time_interval_to_seconds(resolution)
+
+        epoch_start = int(utils.convert_epochs(epoch_start))
+        epoch_stop = int(utils.convert_epochs(epoch_stop))
+
+        accepted_grans = [60, 180, 300, 900, 1800, 3600, 7200, 14400, 21600, 28800, 43200, 86400, 604800]
+
+        if resolution not in accepted_grans:
+            utils.info_print("Granularity is not an accepted granularity...rounding to nearest valid value.")
+            resolution = accepted_grans[min(range(len(accepted_grans)),
+                                            key=lambda i: abs(accepted_grans[i] - resolution))]
+
+        resolution = int(resolution)
+
+        epoch_start -= resolution
+
+        lookup_dict = {
+            60: '1min',
+            180: '3min',
+            300: '5min',
+            900: '15min',
+            1800: '30min',
+            3600: '1hour',
+            7200: '2hour',
+            14400: '4hour',
+            21600: '6hour',
+            28800: '8hour',
+            43200: '12hour',
+            86400: '1day',
+            604800: '1week',
+        }
+        gran_string = lookup_dict[resolution]
+
+        # Figure out how many points are needed
+        need = int((epoch_stop - epoch_start) / resolution)
+        initial_need = need
+        window_open = epoch_start
+        history = []
+
+        while need > 1500:
+            # Close is always 300 points ahead
+            window_close = int(window_open + 1500 * resolution)
+            response = self._market.get_kline(symbol, gran_string,
+                                              startAt=window_open, endAt=window_close)
+            if isinstance(response, dict):
+                raise APIException(response['msg'])
+            history = history + response
+
+            window_open = window_close
+            need -= 1500
+            time.sleep(1)
+            utils.update_progress((initial_need - need) / initial_need)
+
+        # Fill the remainder
+        response = self._market.get_kline(symbol, gran_string,
+                                          startAt=window_open, endAt=epoch_stop)
+        if isinstance(response, dict):
+            raise APIException(response['msg'])
+
+        history = history + response
+        history.sort(key=lambda x: x[0])
+
+        df = pd.DataFrame(history, columns=['time', 'open', 'close', 'high', 'low', 'volume', 'turnover'])
+        del df['turnover']
+
+        # Have to cast this for some reason
+        # df[['time']] = df[['time']].astype(float)
+        df[['time']] = df[['time']].astype(int)
+        df[['open', 'close', 'high', 'low', 'volume']] = df[['open', 'close', 'high', 'low', 'volume']].astype(float)
+
+        try:
+            epoch_start += resolution
+            df = (df.iloc[int(-(epoch_stop-epoch_start)/resolution):]).reset_index(drop=True)
+        except Exception:
+            pass
+
+        return df.reindex(columns=['time', 'low', 'high', 'open', 'close', 'volume'])
+
+    def get_order_filter(self, symbol: str):
+        """
+        Returns:
+        list: List of available currency pairs. Example::
+           [
+              {
+                "symbol": "BTC-USDT",
+                "name": "BTC-USDT",
+                "baseCurrency": "BTC",
+                "quoteCurrency": "USDT",
+                "baseMinSize": "0.00000001",
+                "quoteMinSize": "0.01",
+                "baseMaxSize": "10000",
+                "quoteMaxSize": "100000",
+                "baseIncrement": "0.00000001",
+                "quoteIncrement": "0.01",
+                "priceIncrement": "0.00000001",
+                "feeCurrency": "USDT",
+                "enableTrading": true,
+                "isMarginEnabled": true,
+                "priceLimitRate": "0.1"
+              }
+           ]
+        """
+        response = self._market.get_symbol_list()
+        products = None
+        for i in response:
+            if i["symbol"] == symbol:
+                products = i
+                break
+
+        if products is None:
+            raise LookupError("Specified market not found")
+
+        base_min_size = float(products.pop('baseMinSize'))
+        base_max_size = float(products.pop('baseMaxSize'))
+        base_increment = float(products.pop('baseIncrement'))
+
+        return {
+            "symbol": products.pop('symbol'),
+            "base_asset": products.pop('baseCurrency'),
+            "quote_asset": products.pop('quoteCurrency'),
+            "max_orders": 1000000000000,
+            "limit_order": {
+                "base_min_size": base_min_size,  # Minimum size to buy
+                "base_max_size": base_max_size,  # Maximum size to buy
+                "base_increment": base_increment,  # Specifies the minimum increment
+                # for the base_asset.
+                "price_increment": float(products['quoteIncrement']),
+
+                "min_price": float(products['quoteIncrement']),
+                "max_price": 9999999999,
+            },
+            'market_order': {
+                "fractionable": True,
+
+                "base_min_size": base_min_size,  # Minimum size to buy
+                "base_max_size": base_max_size,  # Maximum size to buy
+                "base_increment": base_increment,  # Specifies the minimum increment
+
+                "quote_increment": float(products.pop('quoteIncrement')),  # Specifies the min order price as well
+                # as the price increment.
+                "buy": {
+                    "min_funds": float(products['quoteMinSize']),
+                    "max_funds": float(products['quoteMaxSize']),
+                },
+                "sell": {
+                    "min_funds": float(products.pop('quoteMinSize')),
+                    "max_funds": float(products.pop('quoteMaxSize')),
+                },
+            },
+            "exchange_specific": {**products}
+        }
+
+    def get_price(self, symbol) -> float:
+        """
+        Returns the best bid price and size,
+        the best ask price and size as well
+        as the last traded price and the last traded size
+        """
+        """
+         {
+            "sequence": "1550467636704",
+            "bestAsk": "0.03715004",
+            "size": "0.17",
+            "price": "0.03715005",
+            "bestBidSize": "3.803",
+            "bestBid": "0.03710768",
+            "bestAskSize": "1.788",
+            "time": 1550653727731
+        }       
+        """
+        response = self._market.get_ticker(symbol)
+        if 'msg' in response:
+            raise APIException("Error: " + response['msg'])
+        return float(response['price'])
diff --git a/blankly/exchanges/interfaces/oanda/oanda.py b/blankly/exchanges/interfaces/oanda/oanda.py
index 80afc956..ef0b15bb 100644
--- a/blankly/exchanges/interfaces/oanda/oanda.py
+++ b/blankly/exchanges/interfaces/oanda/oanda.py
@@ -1,10 +1,22 @@
 from blankly.exchanges.exchange import Exchange
 from blankly.exchanges.interfaces.oanda.oanda_api import OandaAPI
+from blankly.exchanges.auth.auth_constructor import AuthConstructor
 
 
 class Oanda(Exchange):
     def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
-        Exchange.__init__(self, 'oanda', portfolio_name, keys_path, settings_path)
+        Exchange.__init__(self, "oanda", portfolio_name, settings_path)
+
+        # Load the auth from the keys file
+        auth = AuthConstructor(keys_path, portfolio_name, 'oanda', ['PERSONAL_ACCESS_TOKEN', 'ACCOUNT_ID'])
+
+        keys = auth.keys
+        calls = OandaAPI(personal_access_token=keys['PERSONAL_ACCESS_TOKEN'],
+                         account_id=keys['ACCOUNT_ID'],
+                         sandbox=self.preferences["settings"]["use_sandbox"])
+
+        # Always finish the method with this function
+        super().construct_interface_and_cache(calls)
 
     def get_exchange_state(self):
         return self.interface.get_fees()
diff --git a/blankly/exchanges/interfaces/oanda/oanda_api.py b/blankly/exchanges/interfaces/oanda/oanda_api.py
index 0b1d102b..165245e6 100644
--- a/blankly/exchanges/interfaces/oanda/oanda_api.py
+++ b/blankly/exchanges/interfaces/oanda/oanda_api.py
@@ -19,7 +19,6 @@
 
 import json
 
-from blankly.exchanges.auth.abc_auth import ABCAuth
 import requests
 from collections import OrderedDict
 
@@ -40,9 +39,9 @@ class OandaAPI:
     API_URL = 'https://api-fxtrade.oanda.com'
     API_PRACTICE_URL = 'https://api-fxpractice.oanda.com'
 
-    def __init__(self, auth: ABCAuth, sandbox: bool = False):
-        self.__api_key = auth.keys['PERSONAL_ACCESS_TOKEN']
-        self.default_account = auth.keys['ACCOUNT_ID']
+    def __init__(self, personal_access_token: str, account_id: str, sandbox: bool = False):
+        self.__api_key = personal_access_token
+        self.default_account = account_id
 
         if not sandbox:
             self.__api_url = self.API_URL
diff --git a/blankly/exchanges/interfaces/oanda/oanda_auth.py b/blankly/exchanges/interfaces/oanda/oanda_auth.py
deleted file mode 100644
index e3c53c85..00000000
--- a/blankly/exchanges/interfaces/oanda/oanda_auth.py
+++ /dev/null
@@ -1,26 +0,0 @@
-"""
-    Coinbase authentication base class
-    Copyright (C) 2021  Arun Annamalai, Emerson Dove
-
-    This program is free software: you can redistribute it and/or modify
-    it under the terms of the GNU Lesser General Public License as published
-    by the Free Software Foundation, either version 3 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU Lesser General Public License for more details.
-
-    You should have received a copy of the GNU Lesser General Public License
-    along with this program.  If not, see <https://www.gnu.org/licenses/>.
-"""
-
-from blankly.exchanges.auth.abc_auth import ABCAuth
-
-
-class OandaAuth(ABCAuth):
-    def __init__(self, keys_file, portfolio_name):
-        super().__init__(keys_file, portfolio_name, 'oanda')
-        needed_keys = ['PERSONAL_ACCESS_TOKEN', 'ACCOUNT_ID']
-        self.validate_credentials(needed_keys)
diff --git a/blankly/exchanges/interfaces/oanda/oanda_interface.py b/blankly/exchanges/interfaces/oanda/oanda_interface.py
index 94d25823..1591e721 100644
--- a/blankly/exchanges/interfaces/oanda/oanda_interface.py
+++ b/blankly/exchanges/interfaces/oanda/oanda_interface.py
@@ -32,14 +32,14 @@
 
 
 class OandaInterface(ExchangeInterface):
-    def __init__(self, authenticated_api: OandaAPI, preferences_path: str):
+    def __init__(self, exchange_name: str, authenticated_api: OandaAPI):
         self.default_trunc = None
-        super().__init__('oanda', authenticated_api, preferences_path, valid_resolutions=[5, 10, 15, 30, 60,
-                                                                                          60 * 2, 60 * 4, 60 * 5,
-                                                                                          60 * 10, 60 * 15, 60 * 30,
-                                                                                          60 * 60, 60 * 60 * 24,
-                                                                                          60 * 60 * 24 * 7,
-                                                                                          60 * 60 * 24 * 30])
+        super().__init__(exchange_name, authenticated_api, valid_resolutions=[5, 10, 15, 30, 60,
+                                                                              60 * 2, 60 * 4, 60 * 5,
+                                                                              60 * 10, 60 * 15, 60 * 30,
+                                                                              60 * 60, 60 * 60 * 24,
+                                                                              60 * 60 * 24 * 7,
+                                                                              60 * 60 * 24 * 30])
         self.supported_multiples = {5: "S5", 10: "S10", 15: "S15", 30: "S30", 60: "M1", 60 * 2: "M2",
                                     60 * 4: "M4", 60 * 5: "M5", 60 * 10: "M10", 60 * 15: "M15", 60 * 30: "M30",
                                     60 * 60: "H1", 60 * 60 * 24: "D", 60 * 60 * 24 * 7: "W", 60 * 60 * 24 * 30: "M"}
diff --git a/blankly/exchanges/interfaces/paper_trade/metrics.py b/blankly/exchanges/interfaces/paper_trade/metrics.py
index 263ed765..6c7ebba3 100644
--- a/blankly/exchanges/interfaces/paper_trade/metrics.py
+++ b/blankly/exchanges/interfaces/paper_trade/metrics.py
@@ -66,25 +66,23 @@ def calmar(backtest_data, trading_period=86400):
     return round(metrics.calmar(returns, ppy), 2)
 
 
-def volatility(backtest_data):
+def volatility(backtest_data, trading_period=86400):
     returns = backtest_data['returns']['value']
-    return round(metrics.volatility(returns), 2)
-
-
-def variance(backtest_data):
-    returns = backtest_data['returns']['value'] * 100
-    return round(metrics.variance(returns), 2)
+    ppy = periods_per_year(trading_period)
+    return round(metrics.volatility(returns, ppy), 2)
 
 
-def beta(backtest_data):
-    # TODO: Need to pass in the specific resolution
-    # Defaulting to 1d
-    # Need to get some sort of baseline for this one...
-    # Use SP500 as default for all of them (can we get this data?)
-    # Or pick one of the assets as a baseline
+def variance(backtest_data, trading_period=86400):
     returns = backtest_data['returns']['value']
-    return round(metrics.beta(returns), 2)
+    ppy = periods_per_year(trading_period)
+    return round(100.0 * metrics.variance(returns, ppy), 2)
 
+def beta(backtest_data, trading_period=86400):
+    # Drop the first index because it is NaN
+    returns = backtest_data['returns']['value'][1:]
+    benchmark = backtest_data['benchmark_returns']['value'][1:]
+    ppy = periods_per_year(trading_period)
+    return round(100.0 * metrics.beta(returns, benchmark, ppy), 2)
 
 def var(backtest_data):
     returns = backtest_data['returns']['value']
diff --git a/blankly/exchanges/interfaces/paper_trade/paper_trade.py b/blankly/exchanges/interfaces/paper_trade/paper_trade.py
index 163649d7..5d3f2d62 100644
--- a/blankly/exchanges/interfaces/paper_trade/paper_trade.py
+++ b/blankly/exchanges/interfaces/paper_trade/paper_trade.py
@@ -23,8 +23,9 @@
 class PaperTrade(Exchange):
     def __init__(self, authenticated_exchange: Exchange, initial_account_values: dict = None):
         # Giving the preferences path as none allows us to create a default
-        Exchange.__init__(self, "paper_trade", "", None, None)
+        Exchange.__init__(self, "paper_trade", authenticated_exchange.portfolio_name, None)
 
+        # This construction is unique because it doesn't make the interface super call for construction
         self.interface = PaperTradeInterface(authenticated_exchange.get_interface(),
                                              initial_account_values)
         self.calls = authenticated_exchange.get_direct_calls()
diff --git a/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py b/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
index ace418dd..9e2359ea 100644
--- a/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
+++ b/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
@@ -33,7 +33,14 @@
 
 class PaperTradeInterface(ExchangeInterface, BacktestingWrapper):
     def __init__(self, derived_interface: ABCExchangeInterface, initial_account_values: dict = None):
+        # This paper trade orders keeps a live track of the orders
         self.paper_trade_orders = []
+        # These two keep track of which limit orders and when the order finishes
+        self.canceled_orders = []
+        self.executed_orders = []
+
+        # Save the market order execution details for later
+        self.market_order_execution_details = []
 
         self.get_products_cache = None
         self.get_fees_cache = None
@@ -120,15 +127,18 @@ def init_exchange(self):
             traceback.print_exc()
             raise AttributeError("Are you passing a non-exchange object into the paper trade constructor?")
 
-        self.__exchange_properties = {
-            "maker_fee_rate": fees['maker_fee_rate'],
-            "taker_fee_rate": fees['taker_fee_rate']
-        }
+        try:
+            self.__exchange_properties = {
+                "maker_fee_rate": fees['maker_fee_rate'],
+                "taker_fee_rate": fees['taker_fee_rate']
+            }
+        except KeyError:
+            raise KeyError(f'Invalid exchange response: {fees}')
 
     """ Needs to be overridden here """
 
     def start_paper_trade_watchdog(self):
-        # TODO, this process could use variable update time/websocket usage, poll time and a variety of settings
+        # TODO, this process could use variable update time/websocket usage, poll `time` and a variety of settings
         #  to create a robust trading system
         # Create the watchdog for watching limit orders
         self.__thread = threading.Thread(target=self.__paper_trade_watchdog(), daemon=True)
@@ -251,6 +261,12 @@ def evaluate_limits(self):
                         order['status'] = 'done'
                         order['settled'] = 'true'
 
+                        # Add this to the executed orders
+                        self.executed_orders.append({
+                            'id': index['id'],
+                            'executed_time': self.time(),
+                        })
+
                         self.paper_trade_orders[i] = order
                 elif index['side'] == 'sell':
                     if index['price'] < prices[index['symbol']]:
@@ -281,6 +297,12 @@ def evaluate_limits(self):
                         order['status'] = 'done'
                         order['settled'] = 'true'
 
+                        # Add this to the executed orders
+                        self.executed_orders.append({
+                            'id': index['id'],
+                            'executed_time': self.time(),
+                        })
+
                         self.paper_trade_orders[i] = order
 
     def evaluate_paper_trade(self, order, current_price):
@@ -344,7 +366,7 @@ def market_order(self, symbol, side, size) -> MarketOrder:
             raise InvalidOrder(f"Size is too small. Minimum is: {min_size}. You requested {size}.")
 
         if size > max_size:
-            raise InvalidOrder(f"Size is too large. Maximum is: {min_size}. You requested {size}.")
+            raise InvalidOrder(f"Size is too large. Maximum is: {max_size}. You requested {size}.")
 
         base_increment = market_limits['market_order']['base_increment']
         base_decimals = self.__get_decimals(base_increment)
@@ -426,6 +448,11 @@ def market_order(self, symbol, side, size) -> MarketOrder:
             raise APIException("Invalid trade side: " + str(side))
 
         self.__check_trading_assets(symbol)
+
+        self.market_order_execution_details.append({
+            'id': response['id'],
+            'executed_price': price
+        })
         return MarketOrder(order, response, self)
 
     def limit_order(self, symbol, side, price, size) -> LimitOrder:
@@ -548,7 +575,7 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
             # Loose the size when selling
             self.local_account.update_available(base, available - size)
 
-            # Gain size on hold when buying
+            # Gain size on hold when selling
             hold = self.local_account.get_account(base)['hold']
             self.local_account.update_hold(base, hold + size)
         return LimitOrder(order, response, self)
@@ -562,9 +589,17 @@ def cancel_order(self, symbol, order_id) -> dict:
             index = self.paper_trade_orders[i]
             if index['status'] == 'pending' and index['id'] == order_id:
                 order_index = i
+                break
 
         if order_index is not None:
             order_id = self.paper_trade_orders[order_index]['id']
+            # Make sure to save this as a canceled order just before closing it
+            # Make sure to write in the time also
+            self.canceled_orders.append({
+                'id': self.paper_trade_orders[order_index]['id'],
+                'canceled_time': self.time()
+            })
+
             del self.paper_trade_orders[order_index]
             return {"order_id": order_id}
 
diff --git a/blankly/exchanges/interfaces/websocket.py b/blankly/exchanges/interfaces/websocket.py
new file mode 100644
index 00000000..f51208ea
--- /dev/null
+++ b/blankly/exchanges/interfaces/websocket.py
@@ -0,0 +1,162 @@
+"""
+    Core logic to reduce duplication across websocket implementations
+    Copyright (C) 2022  Emerson Dove
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+import abc
+import collections
+import threading
+
+import websocket
+
+import blankly.utils.utils
+from blankly.exchanges.abc_exchange_websocket import ABCExchangeWebsocket
+from blankly.utils.utils import info_print
+
+
+class Websocket(ABCExchangeWebsocket, abc.ABC):
+    def __init__(self, symbol, stream, log, log_message, url, pre_event_callback):
+        self.symbol = symbol
+        self.stream = stream
+
+        # Initialize log file
+        if log is not None:
+            self.log = True
+            self.file_path = log
+            try:
+                self.__file = open(log, 'x+')
+                self.__file.write(log_message)
+            except FileExistsError:
+                self.__file = open(log, 'a')
+        else:
+            self.log = False
+
+        self.url = url
+
+        self.thread = None
+        self.message_count = 0
+
+        self.response = None
+        self.most_recent_tick = None
+        self.most_recent_time = None
+        self.callbacks = []
+        self.pre_event_callback = pre_event_callback
+
+        # Reload preferences
+        self.preferences = blankly.utils.load_user_preferences()
+        buffer_size = self.preferences['settings']['websocket_buffer_size']
+        self.ticker_feed = collections.deque(maxlen=buffer_size)
+        self.time_feed = collections.deque(maxlen=buffer_size)
+
+        self.ws = None
+
+    def start_websocket(self, on_open: callable, on_message: callable, on_error: callable, on_close: callable,
+                        target: callable):
+        """
+        Restart websocket if it was asked to stop.
+        """
+        if self.ws is None:
+            self.ws = websocket.WebSocketApp(self.url,
+                                             on_open=on_open,
+                                             on_message=on_message,
+                                             on_error=on_error,
+                                             on_close=on_close)
+            self.thread = threading.Thread(target=target)
+            self.thread.start()
+        else:
+            if self.thread.is_alive():
+                info_print("Already running...")
+            else:
+                # Use recursion to restart, continue appending to time feed and ticker feed
+                self.ws = None
+                self.start_websocket(on_open, on_message, on_error, on_close, target)
+
+    def log_response(self, logging_callback: callable, message: dict):
+        # Run callbacks on message
+        if self.log:
+            if self.message_count % 100 == 0:
+                self.__file.close()
+                self.__file = open(self.file_path, 'a')
+            line = logging_callback(message)
+            self.__file.write(line)
+    """
+    The are access functions
+    """
+    """ Required in manager """
+
+    def is_websocket_open(self):
+        if self.thread is not None:
+            return self.thread.is_alive()
+        else:
+            return False
+
+    def get_currency_id(self):
+        return self.symbol
+
+    """ Required in manager """
+
+    def append_callback(self, obj):
+        self.callbacks.append(obj)
+
+    """ Define a variable each time so there is no array manipulation """
+    """ Required in manager """
+
+    def get_most_recent_tick(self):
+        return self.most_recent_tick
+
+    """ Required in manager """
+
+    def get_most_recent_time(self):
+        return self.most_recent_time
+
+    """ Required in manager """
+
+    def get_time_feed(self):
+        return list(self.time_feed)
+
+    """ Parallel with time feed """
+    """ Required in manager """
+
+    def get_feed(self):
+        return list(self.ticker_feed)
+
+    """ Required in manager """
+
+    def get_response(self):
+        return self.response
+
+    """ Required in manager """
+
+    def close_websocket(self):
+        if self.thread is not None and self.thread.is_alive():
+            self.ws.close()
+        else:
+            print("Websocket for " + self.symbol + '@' + self.stream + " is already closed")
+
+    @abc.abstractmethod
+    def on_open(self, ws):
+        pass
+
+    @abc.abstractmethod
+    def on_error(self, ws, error):
+        pass
+
+    @abc.abstractmethod
+    def on_message(self, ws, message):
+        pass
+
+    @abc.abstractmethod
+    def on_close(self, ws):
+        pass
diff --git a/blankly/exchanges/managers/orderbook_manager.py b/blankly/exchanges/managers/orderbook_manager.py
index be50adc9..801767c9 100644
--- a/blankly/exchanges/managers/orderbook_manager.py
+++ b/blankly/exchanges/managers/orderbook_manager.py
@@ -21,7 +21,7 @@
 
 import requests
 
-import blankly.exchanges.auth.auth_constructor
+import blankly.exchanges.auth.utils
 import blankly.utils.utils
 from blankly.exchanges.interfaces.alpaca.alpaca_websocket import Tickers as Alpaca_Websocket
 from blankly.exchanges.interfaces.binance.binance_websocket import Tickers as Binance_Orderbook
@@ -81,17 +81,13 @@ def __init__(self, default_exchange, default_symbol):
         self.__default_exchange = default_exchange
         self.__default_currency = default_symbol
 
-        self.__orderbooks = {}
-        self.__orderbooks[default_exchange] = {}
+        self.__orderbooks = {default_exchange: {}}
 
-        self.__websockets = {}
-        self.__websockets[default_exchange] = {}
+        self.__websockets = {default_exchange: {}}
 
-        self.__websockets_callbacks = {}
-        self.__websockets_callbacks[default_exchange] = {}
+        self.__websockets_callbacks = {default_exchange: {}}
 
-        self.__websockets_kwargs = {}
-        self.__websockets_kwargs[default_exchange] = {}
+        self.__websockets_kwargs = {default_exchange: {}}
 
         # Create the abstraction for adding many managers
         super().__init__(self.__websockets, default_symbol, default_exchange)
@@ -124,6 +120,10 @@ def create_orderbook(self, callback,
             # Write this value so it can be used later
             exchange_name = override_exchange
 
+        # Ensure that we always have a key the relevant orderbook
+        if exchange_name not in self.__orderbooks:
+            self.__orderbooks[exchange_name] = {}
+
         if exchange_name == "coinbase_pro":
             if override_symbol is None:
                 override_symbol = self.__default_currency
diff --git a/blankly/exchanges/managers/ticker_manager.py b/blankly/exchanges/managers/ticker_manager.py
index ae60e05f..cd3dff79 100644
--- a/blankly/exchanges/managers/ticker_manager.py
+++ b/blankly/exchanges/managers/ticker_manager.py
@@ -19,6 +19,8 @@
 from blankly.exchanges.interfaces.alpaca.alpaca_websocket import Tickers as Alpaca_Ticker
 from blankly.exchanges.interfaces.binance.binance_websocket import Tickers as Binance_Ticker
 from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket import Tickers as Coinbase_Pro_Ticker
+from blankly.exchanges.interfaces.ftx.ftx_websocket import Tickers as FTX_Ticker
+
 from blankly.exchanges.managers.websocket_manager import WebsocketManager
 
 
@@ -35,13 +37,13 @@ def __init__(self, default_exchange: str, default_symbol: str):
             default_symbol = blankly.utils.to_exchange_symbol(default_symbol, "binance").lower()
         elif default_exchange == "alpaca":
             default_symbol = blankly.utils.to_exchange_symbol(default_symbol, "alpaca")
-
+        elif default_exchange == "ftx":
+            default_symbol = blankly.utils.to_exchange_symbol(default_symbol, "ftx")
         self.__default_symbol = default_symbol
 
-        self.__tickers = {}
-        self.__tickers[default_exchange] = {}
+        self.__tickers = {default_exchange: {}}
 
-        # Create abstraction for writing many different managers
+        # Create abstraction for writing many managers
         super().__init__(self.__tickers, default_symbol, default_exchange)
 
     """ 
@@ -99,6 +101,7 @@ def create_ticker(self, callback, log: str = None, override_symbol: str = None,
                                         "aggTrade",
                                         log=log)
             ticker.append_callback(callback)
+            override_symbol = override_symbol.upper()
             self.__tickers['binance'][override_symbol] = ticker
             return ticker
         elif exchange_name == "alpaca":
@@ -121,5 +124,21 @@ def create_ticker(self, callback, log: str = None, override_symbol: str = None,
             self.__tickers['alpaca'][override_symbol] = ticker
             return ticker
 
+        elif exchange_name == "ftx":
+            if override_symbol is None:
+                override_symbol = self.__default_symbol
+
+            override_symbol = blankly.utils.to_exchange_symbol(override_symbol, "ftx")
+
+            if sandbox_mode:
+                raise ValueError("Error: FTX does not have a sandbox mode")
+            else:
+                ticker = FTX_Ticker(override_symbol, "trades", log=log)
+
+            ticker.append_callback(callback)
+            # Store this object
+            self.__tickers['ftx'][override_symbol] = ticker
+            return ticker
+
         else:
             print(exchange_name + " ticker not supported, skipping creation")
diff --git a/blankly/exchanges/managers/websocket_manager.py b/blankly/exchanges/managers/websocket_manager.py
index 3a552af3..f96a13ec 100644
--- a/blankly/exchanges/managers/websocket_manager.py
+++ b/blankly/exchanges/managers/websocket_manager.py
@@ -57,6 +57,8 @@ def __evaluate_overrides(self, override_symbol, override_exchange):
 
         if exchange == "binance":
             currency_id = blankly.utils.to_exchange_symbol(currency_id, "binance")
+        if exchange == "ftx":
+            currency_id = blankly.utils.to_exchange_symbol(currency_id, "ftx")
         return self.websockets[exchange][currency_id]
 
     def get_ticker(self, symbol, override_exchange=None):
diff --git a/blankly/exchanges/orders/order.py b/blankly/exchanges/orders/order.py
index 3f25e020..e11e5df5 100644
--- a/blankly/exchanges/orders/order.py
+++ b/blankly/exchanges/orders/order.py
@@ -15,7 +15,7 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-from blankly.utils.utils import pretty_print_JSON
+from blankly.utils.utils import pretty_print_json
 
 
 class Order:
@@ -62,10 +62,10 @@ def __str__(self):
         return_string = self.add_new_line(return_string, "General Order Parameters: ")
 
         return_string = self.add_new_line(return_string, "Response: ")
-        return_string = self.add_new_line(return_string, pretty_print_JSON(self.get_response(), actually_print=False))
+        return_string = self.add_new_line(return_string, pretty_print_json(self.get_response(), actually_print=False))
 
         return_string = self.add_new_line(return_string, "Full Status: ")
-        return_string = self.add_new_line(return_string, pretty_print_JSON(self.get_status(full=True),
+        return_string = self.add_new_line(return_string, pretty_print_json(self.get_status(full=True),
                                                                            actually_print=False))
 
         return_string = self.add_new_line(return_string, "ID: ", self.get_id())
diff --git a/blankly/exchanges/strategy_logger.py b/blankly/exchanges/strategy_logger.py
index 85ecd9d1..8f499b91 100644
--- a/blankly/exchanges/strategy_logger.py
+++ b/blankly/exchanges/strategy_logger.py
@@ -43,7 +43,7 @@ def get_account(self, symbol: str = None) -> AttributeDict:
         out = self.interface.get_account(symbol)
 
         # Log this with the account info as well as the account it was attached to
-        blankly.reporter.log_strategy_event(self.strategy, 'get_account', out, args=symbol)
+        blankly.reporter.log_strategy_event(self.strategy, 'get_account', out, args=args)
         return out
 
     """
@@ -81,7 +81,7 @@ def market_order(self, symbol: str, side: str, size: float) -> MarketOrder:
         out = self.interface.market_order(symbol, side, size)
 
         # Record this market order along with the arguments
-        blankly.reporter.log_strategy_event(self.strategy, 'market_order', out, args=args)
+        blankly.reporter.log_strategy_event(self.strategy, 'market_order', out.get_response(), args=args)
         return out
     
     def limit_order(self, symbol: str, side: str, price: float, size: float) -> LimitOrder:
@@ -89,7 +89,7 @@ def limit_order(self, symbol: str, side: str, price: float, size: float) -> Limi
         out = self.interface.limit_order(symbol, side, price, size)
 
         # Record limit order along with the arguments
-        blankly.reporter.log_strategy_event(self.strategy, 'limit_order', out, args=args)
+        blankly.reporter.log_strategy_event(self.strategy, 'limit_order', out.get_response(), args=args)
         return out
     
     def cancel_order(self, symbol: str, order_id: str) -> dict:
diff --git a/blankly/frameworks/strategy/strategy.py b/blankly/frameworks/strategy/strategy.py
new file mode 100644
index 00000000..b4385709
--- /dev/null
+++ b/blankly/frameworks/strategy/strategy.py
@@ -0,0 +1,246 @@
+"""
+This is a commentated but cleaner implementation of strategy_base.py
+
+Largely this project is going on the backburner
+"""
+
+# """
+#     Abstraction for creating interval driven user strategies
+#     Copyright (C) 2021  Emerson Dove, Brandon Fan
+#
+#     This program is free software: you can redistribute it and/or modify
+#     it under the terms of the GNU Lesser General Public License as published
+#     by the Free Software Foundation, either version 3 of the License, or
+#     (at your option) any later version.
+#
+#     This program is distributed in the hope that it will be useful,
+#     but WITHOUT ANY WARRANTY; without even the implied warranty of
+#     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#     GNU Lesser General Public License for more details.
+#
+#     You should have received a copy of the GNU Lesser General Public License
+#     along with this program.  If not, see <https://www.gnu.org/licenses/>.
+# """
+#
+# import datetime
+# import threading
+# import time
+# import typing
+# import warnings
+#
+# import pandas as pd
+#
+# import blankly
+# from blankly.exchanges.exchange import Exchange
+# from blankly.exchanges.interfaces.paper_trade.backtest_controller import BackTestController
+# from blankly.exchanges.interfaces.paper_trade.backtest_result import BacktestResult
+# from blankly.frameworks.strategy.strategy_state import StrategyState
+# from blankly.utils.time_builder import time_interval_to_seconds
+# from blankly.utils.utils import AttributeDict, info_print
+# from blankly.utils.utils import get_ohlcv_from_list
+# from blankly.exchanges.strategy_logger import StrategyLogger
+#
+#
+# class Strategy:
+#     def __init__(self, exchange: Exchange):
+#         """
+#         Create a new strategy object. A strategy can be used to run your code live while be backtestable and modular
+#          across exchanges.
+#          Args:
+#              exchange: An exchange object. This can be created by doing something similar to exchange = blankly.Alpaca()
+#
+#         Function Signatures:
+#         init(symbol: str, state: blankly.StrategyState)
+#         price_event(price: float, symbol: str, state: blankly.StrategyState)
+#         orderbook_event(tick: dict, symbol: str, state: blankly.StrategyState)
+#         bar_event(bar: dict, symbol: str, state: blankly.StrategyState)
+#         teardown(blankly.StrategyState)
+#         """
+#         self.__exchange = exchange
+#         self.__backtest_scheduler: dict = {}
+#
+#         self.interface = StrategyLogger(interface=exchange.get_interface(), strategy=self)
+#
+#         self.__schedulers = []
+#
+#         self.ticker_manager = blankly.TickerManager(self.__exchange.get_type(), '')
+#
+#     def __idle_event(self, *args, **kwargs):
+#         """
+#         Function to skip & ignore callbacks. Mainly used for websockets
+#         """
+#         pass
+#
+#     def add_price_event(self, callback: typing.Callable, symbol: str, resolution: typing.Union[str, float],
+#                         init: typing.Callable = None, teardown: typing.Callable = None, synced: bool = False,
+#                         variables: dict = None):
+#         """
+#         Add Price Event. This will provide you with an updated price every time the callback is run
+#         Args:
+#             callback: The price event callback that will be added to the current ticker and run at the proper resolution
+#             symbol: Currency pair to create the price event for
+#             resolution: The resolution that the callback will be run - in seconds
+#             init: Callback function to allow a setup for the strategy variable. Example usages include
+#                 downloading price data before usage
+#             teardown: A function to run when the strategy is stopped or interrupted. Example usages include liquidating
+#                 positions, writing or cleaning up data or anything else useful
+#             synced: Sync the function to
+#             variables: A dictionary to initialize the state's internal values
+#         """
+#         self.__custom_price_event(callback=callback, symbol=symbol, resolution=resolution, init=init, synced=synced,
+#                                   teardown=teardown, variables=variables, type_='price_event')
+#
+#     def add_bar_event(self, callback: typing.Callable, symbol: str, resolution: typing.Union[str, float],
+#                       init: typing.Callable = None, teardown: typing.Callable = None, variables: dict = None):
+#         """
+#         The bar event sends a dictionary of {open, high, low, close, volume} which has occurred in the interval.
+#         Args:
+#             callback: The price event callback that will be added to the current ticker and run at the proper resolution
+#             symbol: Currency pair to create the price event for
+#             resolution: The resolution that the callback will be run - in seconds
+#             init: Callback function to allow a setup for the strategy variable. Example usages include
+#                 downloading price data before usage
+#             teardown: A function to run when the strategy is stopped or interrupted. Example usages include liquidating
+#                 positions, writing or cleaning up data or anything else useful
+#             variables: A dictionary to initialize the state's internal values
+#         """
+#         self.__custom_price_event(type_='bar', synced=True, bar=True, callback=callback, symbol=symbol,
+#                                   resolution=resolution, init=init, teardown=teardown, variables=variables)
+#
+#     def __custom_price_event(self,
+#                              type_: str,
+#                              callback: typing.Callable = None,
+#                              symbol: str = None,
+#                              resolution: typing.Union[str, float] = None,
+#                              init: typing.Callable = None,
+#                              synced: bool = False,
+#                              bar: bool = False,
+#                              teardown: typing.Callable = None, variables: dict = None):
+#         """
+#         Add Price Event
+#         Args:
+#             type_: The type of event that the price event refers to
+#             callback: The price event callback that will be added to the current ticker and run at the proper
+#              resolution
+#             symbol: Currency pair to create the price event for
+#             resolution: The resolution that the callback will be run - in seconds
+#             init: Callback function to allow a setup for the strategy variable. Example usages include
+#                 downloading price data before usage
+#             teardown: A function to run when the strategy is stopped or interrupted. Example usages include
+#              liquidating
+#                 positions, writing or cleaning up data or anything else useful
+#             synced: Sync the function to
+#             bar: Get the OHLCV data for a valid exchange interval
+#             variables: Initial dictionary to write into the state variable
+#         """
+#         # Make sure variables is always an empty dictionary if None
+#         if variables is None:
+#             variables = {}
+#
+#         resolution = time_interval_to_seconds(resolution)
+#
+#         # Make sure the type is always added to the price events
+#         if type_ not in self.__backtest_scheduler:
+#             self.__backtest_scheduler[type_] = []
+#
+#         self.__backtest_scheduler[type_] = {
+#             'symbol': symbol,
+#             'resolution': resolution
+#         }
+#
+#         variables_ = AttributeDict(variables)
+#         state = StrategyState(self, variables_, symbol, resolution=resolution)
+#
+#         scheduler_kwargs = {
+#             'initially_stopped': True,
+#             'callback': callback,
+#             'resolution': resolution,
+#             'variables': variables_,
+#             'state_object': state,
+#             'synced': synced,
+#             'init': init,
+#             'teardown': teardown,
+#             'ohlc': bar,
+#             'symbol': symbol
+#         }
+#
+#         if resolution < 10:
+#             self.ticker_manager.create_ticker(self.__idle_event, override_symbol=symbol)
+#             self.__schedulers.append(
+#                 blankly.Scheduler(self.__price_event_websocket, resolution,
+#                                   **scheduler_kwargs)
+#             )
+#             exchange_type = self.__exchange.get_type()
+#
+#     def __price_event_rest(self, **kwargs):
+#         callback = kwargs['callback']
+#         symbol = kwargs['symbol']
+#         resolution = kwargs['resolution']
+#         variables = kwargs['variables']
+#         ohlc = kwargs['ohlc']
+#         state = kwargs['state_object']  # type: StrategyState
+#
+#         state.variables = variables
+#         state.resolution = resolution
+#
+#         if ohlc:
+#             ohlcv_time = kwargs['ohlcv_time']
+#             while True:
+#                 # Sometimes coinbase doesn't download recent data correctly
+#                 try:
+#                     data = self.interface.history(symbol=symbol, to=1, resolution=resolution).iloc[-1].to_dict()
+#                     if data['time'] + resolution == ohlcv_time:
+#                         break
+#                 except IndexError:
+#                     pass
+#                 time.sleep(.5)
+#             data['price'] = self.interface.get_price(symbol)
+#         else:
+#             data = self.interface.get_price(symbol)
+#
+#     def __price_event_websocket(self, **kwargs):
+#         callback = kwargs['callback']
+#         symbol = kwargs['symbol']
+#         resolution = kwargs['resolution']
+#         variables = kwargs['variables']
+#         ohlc = kwargs['ohlc']
+#         state = kwargs['state_object']  # type: StrategyState
+#
+#         if ohlc:
+#             close_time = kwargs['ohlcv_time']
+#             open_time = close_time-resolution
+#             ticker_feed = list(reversed(self.ticker_manager.get_feed(override_symbol=symbol)))
+#             #     tick        tick
+#             #      |    ohlcv close                            ohlcv open
+#             # 0    |   -20          -40            -60        -80
+#             # newest, newest - 1, newest - 2
+#             count = 0
+#             while ticker_feed[count]['time'] > close_time:
+#                 count += 1
+#
+#             close_index = count
+#
+#             # Start at the close index to save some iterations
+#             count = close_index
+#             while ticker_feed[count]['time'] < open_time:
+#                 count += 1
+#             # Subtract 1 to put it back inside the range
+#             count -= 1
+#             open_index = count
+#
+#             # Get the latest price that isn't past the timeframe
+#             last_price = ticker_feed[close_index:][-1]['price']
+#
+#             data = get_ohlcv_from_list(list(reversed(ticker_feed[close_index:open_index])), last_price)
+#
+#         else:
+#             try:
+#                 data = self.ticker_manager.get_most_recent_tick(override_symbol=symbol)['price']
+#             except TypeError:
+#                 info_print("No valid data yet - using rest.")
+#                 data = self.interface.get_price(symbol)
+#
+#         state.variables = variables
+#         state.resolution = resolution
+#
+#         callback(data, symbol, state)
diff --git a/blankly/frameworks/strategy/strategy_base.py b/blankly/frameworks/strategy/strategy_base.py
index bcb461a6..d285d95a 100644
--- a/blankly/frameworks/strategy/strategy_base.py
+++ b/blankly/frameworks/strategy/strategy_base.py
@@ -36,10 +36,12 @@
 
 
 class Strategy:
-    def __init__(self, exchange: Exchange, currency_pair='BTC-USD'):
+    def __init__(self, exchange: Exchange):
         """
         Create a new strategy object. A strategy can be used to run your code live while be backtestable and modular
          across exchanges.
+         Args:
+             exchange: An exchange object. This can be created by doing something similar to exchange = blankly.Alpaca()
 
         Function Signatures:
         init(symbol: str, state: blankly.StrategyState)
@@ -50,10 +52,13 @@ def __init__(self, exchange: Exchange, currency_pair='BTC-USD'):
         """
         self.__remote_backtesting = blankly._backtesting
         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]
+        blankly.reporter.export_used_exchange(self.__exchange.get_type())
+
+        self.ticker_manager = blankly.TickerManager(self.__exchange.get_type(), '')
+        self.orderbook_manager = blankly.OrderbookManager(self.__exchange.get_type(), '')
+
+        self.__scheduling_pair = []  # Object to hold a currency and the resolution it's pulled at: ["BTC-USD", 60]
         self.interface = StrategyLogger(interface=exchange.get_interface(), strategy=self)
 
         # Create a cache for the current interface, and a wrapped paper trade object for user backtesting
@@ -89,6 +94,15 @@ def modify_variable(self, callable_: typing.Callable, key, value):
         hashed = hash(callable_)
         self.__variables[hashed][key] = value
 
+    # # TODO these could have some parameters assigned by a super class
+    # def add_arbitrage_event(self, callback: typing.Callable, symbols: list, resolution: typing.Union[str, float],
+    #                         init: typing.Callable = None, teardown: typing.Callable = None, synced: bool = False,
+    #                         variables: dict = None):
+    #     """
+    #     Add Arbitrage Event - This allows periodic events where prices are gathered asynchronously. When run live at a
+    #      small interval, this allows minimal latency when gathering the price of the requested symbols.
+    #     """
+
     def add_price_event(self, callback: typing.Callable, symbol: str, resolution: typing.Union[str, float],
                         init: typing.Callable = None, teardown: typing.Callable = None, synced: bool = False,
                         variables: dict = None):
@@ -105,7 +119,8 @@ def add_price_event(self, callback: typing.Callable, symbol: str, resolution: ty
             synced: Sync the function to
             variables: A dictionary to initialize the state's internal values
         """
-        self.__custom_price_event(callback, symbol, resolution, init, synced, teardown=teardown, variables=variables)
+        self.__custom_price_event(callback=callback, symbol=symbol, resolution=resolution, init=init, synced=synced,
+                                  teardown=teardown, variables=variables, type_='price_event')
 
     def add_bar_event(self, callback: typing.Callable, symbol: str, resolution: typing.Union[str, float],
                       init: typing.Callable = None, teardown: typing.Callable = None, variables: dict = None):
@@ -121,15 +136,22 @@ def add_bar_event(self, callback: typing.Callable, symbol: str, resolution: typi
                 positions, writing or cleaning up data or anything else useful
             variables: A dictionary to initialize the state's internal values
         """
-        self.__custom_price_event(callback, symbol, resolution, init, synced=True, bar=True, teardown=teardown,
-                                  variables=variables)
-
-    def __custom_price_event(self, callback: typing.Callable, symbol: str, resolution: typing.Union[str, float],
-                             init: typing.Callable = None, synced: bool = False, bar: bool = False,
+        self.__custom_price_event(type_='bar', synced=True, bar=True, callback=callback, symbol=symbol,
+                                  resolution=resolution, init=init, teardown=teardown, variables=variables)
+
+    def __custom_price_event(self,
+                             type_: str,
+                             callback: typing.Callable = None,
+                             symbol: str = None,
+                             resolution: typing.Union[str, float] = None,
+                             init: typing.Callable = None,
+                             synced: bool = False,
+                             bar: bool = False,
                              teardown: typing.Callable = None, variables: dict = None):
         """
         Add Price Event
         Args:
+            type_: The type of event that the price event refers to
             callback: The price event callback that will be added to the current ticker and run at the proper resolution
             symbol: Currency pair to create the price event for
             resolution: The resolution that the callback will be run - in seconds
@@ -139,17 +161,14 @@ def __custom_price_event(self, callback: typing.Callable, symbol: str, resolutio
                 positions, writing or cleaning up data or anything else useful
             synced: Sync the function to
             bar: Get the OHLCV data for a valid exchange interval
+            variables: Initial dictionary to write into the state variable
         """
         # Make sure variables is always an empty dictionary if None
         if variables is None:
             variables = {}
-
         resolution = time_interval_to_seconds(resolution)
 
-        if bar:
-            self.__scheduling_pair.append([symbol, resolution, 'bar'])
-        else:
-            self.__scheduling_pair.append([symbol, resolution, 'price_event'])
+        self.__scheduling_pair.append([symbol, resolution, type_])
 
         variables_ = AttributeDict(variables)
         state = StrategyState(self, variables_, symbol, resolution=resolution)
@@ -188,6 +207,9 @@ def __custom_price_event(self, callback: typing.Callable, symbol: str, resolutio
                                   symbol=symbol)
             )
 
+        # Export a new symbol to the backend
+        blankly.reporter.export_used_symbol(symbol)
+
     def __idle_event(self, *args, **kwargs):
         """
         Function to skip & ignore callbacks
@@ -374,8 +396,8 @@ def time(self) -> float:
     def backtest(self,
                  to: str = None,
                  initial_values: dict = None,
-                 start_date: str = None,
-                 end_date: str = None,
+                 start_date: typing.Union[str, float, int] = None,
+                 end_date: typing.Union[str, float, int] = None,
                  save: bool = False,
                  settings_path: str = None,
                  callbacks: list = None,
@@ -391,8 +413,10 @@ def backtest(self,
             to (str): Declare an amount of time before now to backtest from: ex: '5y' or '10h'
             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.
-            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"
+            start_date (str): Override argument "to" by specifying a start date such as "03/06/2018". This can also
+                be an epoch time as a float or int.
+            end_date (str): End the backtest at a date such as "03/06/2018". This can also be an epoch type as a float
+                or int
             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.
@@ -455,14 +479,20 @@ def backtest(self,
             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 isinstance(end_date, (int, float)):
+                start = start_date
+            else:
+                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()
+            if isinstance(end_date, (int, float)):
+                end = end_date
+            else:
+                end_date = pd.to_datetime(end_date)
+                epoch = datetime.datetime.utcfromtimestamp(0)
+                end = (end_date - epoch).total_seconds()
 
         # If start/ends are specified unevenly
         if (start_date is None and end_date is not None) or (start_date is not None and end_date is None):
diff --git a/blankly/frameworks/strategy/strategy_state.py b/blankly/frameworks/strategy/strategy_state.py
index 4dd8c545..0f15764e 100644
--- a/blankly/frameworks/strategy/strategy_state.py
+++ b/blankly/frameworks/strategy/strategy_state.py
@@ -17,7 +17,7 @@
 """
 
 from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface as Interface
-from blankly.utils.utils import AttributeDict, get_base_asset, get_quote_asset, format_with_new_line, pretty_print_JSON
+from blankly.utils.utils import AttributeDict, get_base_asset, get_quote_asset, format_with_new_line, pretty_print_json
 
 
 class StrategyState:
@@ -26,7 +26,7 @@ class StrategyState:
     resolution: int
 
     """Strategy State"""
-    def __init__(self, strategy, variables: AttributeDict, symbol, resolution: int = None):
+    def __init__(self, strategy, variables: AttributeDict, symbol, resolution: [int, float] = None):
         self.strategy = strategy
         self.variables = variables
         self.resolution = resolution
@@ -59,6 +59,6 @@ def __str__(self):
 
         output = format_with_new_line(output, "Variables: ")
 
-        output = format_with_new_line(output, pretty_print_JSON(self.variables, actually_print=False))
+        output = format_with_new_line(output, pretty_print_json(self.variables, actually_print=False))
 
         return output
diff --git a/blankly/metrics/portfolio.py b/blankly/metrics/portfolio.py
index 778af511..1fc2e352 100644
--- a/blankly/metrics/portfolio.py
+++ b/blankly/metrics/portfolio.py
@@ -65,12 +65,12 @@ def volatility(returns, n=None):
 
 
 def variance(returns, n=None):
-    return np.nanvar(returns) * np.sqrt(n) if n else np.nanvar(returns)
+    return np.nanvar(returns) * n if n else np.nanvar(returns)
 
 
-def beta(returns, market_base_returns):
+def beta(returns, market_base_returns, n=None):
     m = np.matrix([returns, market_base_returns])
-    return np.cov(m)[0][1] / np.std(market_base_returns)
+    return n * np.cov(m)[0][1] / variance(market_base_returns, n)
 
 
 def var(initial_value, returns, alpha: float):
diff --git a/blankly/utils/time_builder.py b/blankly/utils/time_builder.py
index d80b23d5..99de02cd 100644
--- a/blankly/utils/time_builder.py
+++ b/blankly/utils/time_builder.py
@@ -100,3 +100,33 @@ def time_interval_to_seconds(interval: Union[str, float]) -> float:
 
     # Scale by the magnitude
     return float(base_unit * magnitude)
+
+
+def number_interval_to_string(interval: int) -> str:
+    """
+    This function converts integer intervals into string intervals
+
+    Example: 3600 -> 1h
+
+    Args:
+        interval: An integer representing the conversion time
+    """
+    times = {
+        'mo': build_month(),
+        'wk': build_week(),
+        'd': build_day(),
+        'h': build_hour(),
+        'm': build_minute(),
+        's': build_second()
+    }
+
+    unit = None
+
+    for i in times:
+        if interval % times[i] == 0:
+            unit = i
+            break
+
+    magnitude = int(interval/times[unit])
+
+    return f'{magnitude}{unit}'
diff --git a/blankly/utils/utils.py b/blankly/utils/utils.py
index a1b3d247..558e7d1a 100644
--- a/blankly/utils/utils.py
+++ b/blankly/utils/utils.py
@@ -1,17 +1,14 @@
 """
     Utils file for assisting with trades or market analysis.
     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/>.
 """
@@ -36,7 +33,6 @@
 # Copy of settings to compare defaults vs overrides
 default_general_settings = {
     "settings": {
-        "account_update_time": 5000,
         "use_sandbox": False,
         "use_sandbox_websockets": False,
         "websocket_buffer_size": 10000,
@@ -49,10 +45,17 @@
             "cash": "USDT",
             "binance_tld": "us"
         },
+        "kucoin": {
+            "cash": "USDT"
+        },
+        "ftx": {
+            "cash": "USD"
+        },
         "alpaca": {
             "websocket_stream": "iex",
             "cash": "USD",
             "enable_shorting": True,
+            "use_yfinance": False,
         },
         "oanda": {
             "cash": "USD"
@@ -76,7 +79,8 @@
         "resample_account_value_for_metrics": "1d",
         "quote_account_value_in": "USD",
         "ignore_user_exceptions": False,
-        "risk_free_return_rate": 0.0
+        "risk_free_return_rate": 0.0,
+        "benchmark_symbol": None
     }
 }
 
@@ -106,9 +110,7 @@ class __BlanklySettings:
     def __init__(self, default_path: str, default_settings: dict, not_found_err: str):
         """
         Create a class that can manage caching for loading and writing to user preferences with a low overhead.
-
         This can dramatically accelerate instantiation of new interfaces or other objects
-
         Args:
             default_path: The default path to look for settings - such as ./settings.json
             default_settings: The default settings in which to compare the loaded settings to. This helps the user
@@ -199,7 +201,7 @@ def load_notify_preferences(override_path=None) -> dict:
     return notify_settings.load(override_path)
 
 
-def pretty_print_JSON(json_object, actually_print=True):
+def pretty_print_json(json_object, actually_print=True):
     """
     Json pretty printer for general string usage
     """
@@ -209,13 +211,13 @@ def pretty_print_JSON(json_object, actually_print=True):
     return out
 
 
-def epoch_from_ISO8601(ISO8601) -> float:
-    return dp.parse(ISO8601).timestamp()
+def epoch_from_iso8601(iso8601: str) -> float:
+    return dp.parse(iso8601).timestamp()
 
 
 def convert_input_to_epoch(value: Union[str, dt]) -> float:
     if isinstance(value, str):
-        return epoch_from_ISO8601(value)
+        return epoch_from_iso8601(value)
     elif isinstance(value, dt):
         return value.timestamp()
     elif isinstance(value, float):
@@ -223,7 +225,7 @@ def convert_input_to_epoch(value: Union[str, dt]) -> float:
     raise ValueError("Incorrect value input given, expected string or value but got: {}".format(type(value)))
 
 
-def ISO8601_from_epoch(epoch) -> str:
+def iso8601_from_epoch(epoch) -> str:
     return dt.utcfromtimestamp(epoch).isoformat() + 'Z'
 
 # Removed due to sklearn dependency
@@ -281,7 +283,7 @@ def to_blankly_symbol(symbol, exchange, quote_guess=None) -> str:
             return symbol + "-" + quote_guess
         else:
             # Try your best to try to parse anyway
-            quotes = ['BNB', 'BTC', 'TRX', 'XRP', 'ETH', 'USDT', 'BUSD', 'AUD', 'BRL', 'EUR', 'GBP', 'RUB',
+            quotes = ['BNB', 'BTC', 'TRX', 'XRP', 'ETH', 'USDT', 'USD', 'BUSD', 'AUD', 'BRL', 'EUR', 'GBP', 'RUB',
                       'TRY', 'TUSD', 'USDC', 'PAX', 'BIDR', 'DAI', 'IDRT', 'UAH', 'NGN', 'VAI', 'BVND']
             for i in quotes:
                 if __check_ending(symbol, i):
@@ -291,6 +293,9 @@ def to_blankly_symbol(symbol, exchange, quote_guess=None) -> str:
     if exchange == "coinbase_pro":
         return symbol
 
+    if exchange == "ftx":
+        return symbol.replace("/", "-")
+
 
 def __check_ending(full_string, checked_ending) -> bool:
     check_length = len(checked_ending)
@@ -302,8 +307,10 @@ def to_exchange_symbol(blankly_symbol, exchange):
         return blankly_symbol.replace('-', '')
     if exchange == "alpaca":
         return get_base_asset(blankly_symbol)
-    if exchange == "coinbase_pro":
+    if exchange == "coinbase_pro" or exchange == "kucoin":
         return blankly_symbol
+    if exchange == 'ftx':
+        return blankly_symbol.replace("-", "/")
 
 
 def get_base_asset(symbol):
@@ -543,7 +550,6 @@ def get_ohlcv(candles, n, from_zero: bool):
 def aggregate_candles(history: pd.DataFrame, aggregation_size: int):
     """
     Aggregate history data (such as turn 1m data into 15m data)
-
     Args:
         history: A blankly generated dataframe
         aggregation_size: How many rows of history to aggregate - ex: aggregation_size=15 on 1m data produces
@@ -572,7 +578,6 @@ def aggregate_candles(history: pd.DataFrame, aggregation_size: int):
 def get_ohlcv_from_list(tick_list: list, last_price: float):
     """
     Created with the purpose of parsing ticker data into a viable OHLCV pattern. The
-
     Args:
         tick_list (list): List of data containing price ticks. Needs to be at least: [{'price': 343, 'size': 3.4}, ...]
             The data must also be ordered oldest to most recent at the end
@@ -641,12 +646,9 @@ def get_estimated_start_from_limit(limit, end_epoch, resolution_str, resolution_
 class AttributeDict(dict):
     """
     This is adds functions to the dictionary class, no other modifications. This gives dictionaries abilities like:
-
     print(account.BTC) -> {'available': 1, 'hold': 0}
-
     account.BTC = "cool"
     print(account.BTC) -> cool
-
     Basically you can get and set attributes with a dot instead of [] - like dict.available rather than
      dict['available']
     """
@@ -673,7 +675,6 @@ def format_with_new_line(original_string, *components):
 def trunc(number: float, decimals: int) -> float:
     """
     Truncate a number instead of rounding (ex: trunc(9.9999999, 2) == 9.99 instead of round(9.9999999, 2) == 10.0)
-
     Args:
         number (float): Number to truncate
         decimals (int): Number of decimals to keep: trunc(9.9999999, 2) == 9.99
@@ -685,23 +686,20 @@ def trunc(number: float, decimals: int) -> float:
 def info_print(message):
     """
     This prints directly to stderr which allows a way to distinguish package info calls/errors from generic stdout
-
     Args:
         message: The message to print. INFO: will be prepended
     """
-    print('INFO: ' + message, file=sys.stderr)
+    print('INFO: ' + str(message), file=sys.stderr)
 
 
 class Email:
     """
     Object wrapper for simplifying interaction with SMTP servers & the blankly.reporter.email function.
-
     Alternatively a notify.json can be created which automatically integrates with blankly.reporter.email()
     """
     def __init__(self, smtp_server: str, sender_email: str, password: str, port: int = 465):
         """
         Create the email wrapper:
-
         Args:
             smtp_server: The address of the smtp server
             sender_email: The email attached to the smtp account
@@ -716,7 +714,6 @@ def __init__(self, smtp_server: str, sender_email: str, password: str, port: int
     def send(self, receiver_email: str, message: str):
         """
         Send an email to the receiver_email specified
-
         Args:
             receiver_email (str): The email that the message is sent to
             message (str): The body of the message
@@ -753,3 +750,20 @@ def wrapper(*args, **kwargs):
             raise Exception("Blocked attempt at live order inside backtesting environment")
         return func(*args, **kwargs)
     return wrapper
+
+
+def add_all_products(nonzero_products: dict, all_products: list):
+    base_symbols = []
+    quote_symbols = []
+    for i in all_products:
+        base_symbols.append(get_base_asset(i['symbol']))
+        quote_symbols.append(get_quote_asset(i['symbol']))
+
+    for i in (base_symbols + quote_symbols):
+        if i not in nonzero_products:
+            nonzero_products[i] = {
+                'available': 0.0,
+                'hold': 0.0
+            }
+
+    return nonzero_products
diff --git a/examples/multicore_bot.py b/examples/multicore_bot.py
index f5bcf621..f9e276fb 100644
--- a/examples/multicore_bot.py
+++ b/examples/multicore_bot.py
@@ -66,5 +66,5 @@ def price_event(self, tick):
     while True:
         # Print the state from the model we just started every second
         state = portfolio.get_full_state("BTC-USD")
-        blankly.utils.pretty_print_JSON(state)
+        blankly.utils.pretty_print_json(state)
         time.sleep(1)
diff --git a/examples/rsi.py b/examples/rsi.py
index abdcd767..917a22eb 100644
--- a/examples/rsi.py
+++ b/examples/rsi.py
@@ -7,12 +7,12 @@ def price_event(price, symbol, state: blankly.StrategyState):
     rsi = blankly.indicators.rsi(state.variables['history'])
     if rsi[-1] < 30 and not state.variables['owns_position']:
         # Dollar cost average buy
-        buy = int(state.interface.cash/price)
+        buy = blankly.trunc(state.interface.cash/price, 2)
         state.interface.market_order(symbol, side='buy', size=buy)
         state.variables['owns_position'] = True
     elif rsi[-1] > 70 and state.variables['owns_position']:
         # Dollar cost average sell
-        curr_value = int(state.interface.account[state.base_asset].available)
+        curr_value = state.interface.account[state.base_asset].available
         state.interface.market_order(symbol, side='sell', size=curr_value)
         state.variables['owns_position'] = False
 
diff --git a/examples/rsi_screener.py b/examples/rsi_screener.py
new file mode 100644
index 00000000..8332e099
--- /dev/null
+++ b/examples/rsi_screener.py
@@ -0,0 +1,31 @@
+from blankly import Signal, Alpaca, SignalState
+from blankly.indicators import rsi
+
+tickers = ['AAPL', 'GME', 'MSFT']  # any stocks that you may want
+
+
+# This function is our evaluator and runs per stock
+def is_stock_buy(symbol, state: SignalState):
+    # This runs per stock
+    prices = state.interface.history(symbol, 40, resolution=state.resolution,
+                                     return_as='list')  # get past 40 data points
+    price = state.interface.get_price(symbol)
+    rsi_values = rsi(prices['close'], 14)
+    return {'is_oversold': rsi_values[-1] < 30, 'price': price, 'symbol': symbol}
+
+
+def formatter(results, state: SignalState):
+    # results is a dictionary on a per-symbol basis
+    result_string = 'These are all the stocks that are currently oversold: \n'
+    for symbol in results:
+        if results[symbol]['is_oversold']:
+            result_string += '{} is currently oversold at a price of {}\n\n'.format(symbol, results[symbol]['price'])
+    return result_string
+
+
+if __name__ == "__main__":
+    alpaca = Alpaca()  # initialize our interface
+    signal = Signal(alpaca, is_stock_buy, symbols=tickers, formatter=formatter, resolution='1d')  # find oversold
+    # every day
+
+    print(signal.formatted_results)
diff --git a/examples/settings.json b/examples/settings.json
index b3350c88..4d053870 100644
--- a/examples/settings.json
+++ b/examples/settings.json
@@ -1,7 +1,6 @@
 {
   "settings": {
-    "account_update_time": 5000,
-    "use_sandbox": true,
+    "use_sandbox": false,
     "use_sandbox_websockets": false,
     "websocket_buffer_size": 10000,
     "test_connectivity_on_auth": true,
@@ -16,7 +15,8 @@
     "alpaca": {
       "websocket_stream": "iex",
       "cash": "USD",
-      "enable_shorting": true
+      "enable_shorting": true,
+      "use_yfinance": false
     },
     "oanda": {
       "cash": "USD"
diff --git a/setup.py b/setup.py
index e768ee20..fb6415c7 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='v1.14.0-beta',
+    version='v1.14.8-beta',
     license='lgpl-3.0',  # Licenses: https://help.github.com/articles/licensing-a-repository
     description='Rapidly build & deploy cross-exchange trading bots',  # Give a short description about your library
     long_description=long_description,
@@ -29,16 +29,16 @@
     # download_url='https://github.com/EmersonDove/Blankly/archive/v0.1.1-alpha.tar.gz',
     keywords=['Crypto', 'Stocks', 'Quantitative Finance', 'Exchanges', 'Bot'],  # Keywords
     install_requires=[
-        'alpaca-trade-api >= 1.2.3',
-        'bokeh',
-        'dateparser',
-        'newtulipy',
-        'numpy',
-        'pandas',
-        'pandas_market_calendars',
-        'python-binance',
-        'requests',
-        'websocket-client',
+        'alpaca-trade-api >= 1.4.2',
+        'bokeh >= 2.4.2',
+        'dateparser >= 1.1.0',
+        'newtulipy >= 0.4.6',
+        'numpy >= 1.21.4',
+        'pandas >= 1.1.5',
+        'pandas_market_calendars >= 3.2',
+        'python-binance >= 1.0.15',
+        'requests >= 2.26.0',
+        'websocket-client >= 1.2.1',
     ],
     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 4a0f3723..016f90ce 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,7 +10,7 @@ jobs:
     runs-on: ${{ matrix.os }}
 
     strategy:
-      max-parallel: 2
+      max-parallel: 1
       matrix:
         os: [windows-latest, ubuntu-latest, macos-latest]
         python-version: ['3.7', '3.8', '3.9', '3.10']
@@ -28,6 +28,7 @@ jobs:
         pip install -e .
         pip install pytest
         pip install pytest-mock
+        pip install kucoin-python
 
     - name: Inject Keys
       run: |
diff --git a/blankly/exchanges/interfaces/paper_trade/backtest_controller.py b/blankly/exchanges/interfaces/paper_trade/backtest_controller.py
index c08cdb96..fb810411 100644
--- a/blankly/exchanges/interfaces/paper_trade/backtest_controller.py
+++ b/blankly/exchanges/interfaces/paper_trade/backtest_controller.py
@@ -20,7 +20,9 @@
 import traceback
 import typing
 from datetime import datetime as dt
+import copy
 
+import numpy as np
 import pandas as pd
 from bokeh.layouts import column as bokeh_columns
 from bokeh.models import HoverTool
@@ -33,7 +35,7 @@
 from blankly.exchanges.interfaces.paper_trade.paper_trade_interface import PaperTradeInterface
 from blankly.utils.time_builder import time_interval_to_seconds
 from blankly.utils.utils import load_backtest_preferences, update_progress, write_backtest_preferences, \
-    get_base_asset, get_quote_asset
+    get_base_asset, get_quote_asset, info_print
 
 
 def to_string_key(separated_list):
@@ -174,7 +176,7 @@ def __init__(self, paper_trade_exchange: PaperTrade, backtest_settings_path: str
 
         self.__exchange_type = self.interface.get_exchange_type()
 
-        # Create our own traded assets dictionary because we customize it a bit
+        # Create our own traded assets' dictionary because we customize it a bit
         self.__traded_assets = []
 
         # Because the times are run in order we can use this variable to optimize account value searching
@@ -183,7 +185,23 @@ def __init__(self, paper_trade_exchange: PaperTrade, backtest_settings_path: str
         # Export a time for use in other classes
         self.time = None
 
+        # Set these when prices are added to find the first price and the last price
+        self.user_start = None
+        self.user_stop = None
+
+        self.min_resolution = None
+
     def sync_prices(self) -> dict:
+        """
+        Parse the local file cache for the requested data, if it doesn't exist, request it from the exchange
+
+        args:
+            items: list of lists organized as ['symbol', 'start_time', 'end_time', 'resolution']
+
+        returns:
+            dictionary with keys for each 'symbol'
+        """
+        
         cache_folder = self.preferences['settings']["cache_location"]
         # Make sure the cache folder exists and read files
         try:
@@ -237,23 +255,27 @@ def sync_prices(self) -> dict:
         #     return ranges
 
         # This is the data the user has requested: [asset_id, start_time, end_time, resolution]
+        items = self.__user_added_times
+
         final_prices = {}
-        for i in range(len(self.__user_added_times)):
-            if self.__user_added_times[i] is None:
+        for i in range(len(items)):
+            if items[i] is None:
                 continue
-            asset = self.__user_added_times[i][0]
-            resolution = self.__user_added_times[i][3]
-            start_time = self.__user_added_times[i][1]
-            end_time = self.__user_added_times[i][2] - resolution
+            asset = items[i][0]
+            resolution = items[i][3]
+            start_time = items[i][1]
+            end_time = items[i][2] - resolution
 
             if end_time < start_time:
-                raise RuntimeError("Must specify  a longer timeframe to run the backtest.")
+                raise RuntimeError("Must specify a longer timeframe to run the backtest.")
 
             download_ranges = []
 
             # Attempt to find the same symbol/asset possibilities in the backtest blocks
             try:
-                download_ranges = local_history_blocks[asset][resolution]
+                # Make sure to copy it because if you don't you delete this for any similar resolution
+                # If you don't copy it fails if you use two price events at the same resolution
+                download_ranges = copy.deepcopy(local_history_blocks[asset][resolution])
             except KeyError:
                 pass
 
@@ -325,12 +347,14 @@ def sync_prices(self) -> dict:
 
                 # Write the file but this time include very accurately the start and end times
                 if self.preferences['settings']['continuous_caching']:
-                    download.to_csv(os.path.join(cache_folder, f'{asset},'
-                                                               f'{j[0]},'
-                                                               f'{j[1]+resolution},'  # This adds resolution back to the
-                                                                                      #  exported time series
-                                                               f'{resolution}.csv'),
-                                    index=False)
+                    if not download.empty:
+                        download.to_csv(os.path.join(cache_folder, f'{asset},'
+                                                                   f'{j[0]},'
+                                                                   f'{j[1]+resolution},'  # This adds resolution 
+                                                                                          # back to the exported
+                                                                                          # time series
+                                                                   f'{resolution}.csv'),
+                                        index=False)
 
                 # Write these into the data array
                 if asset not in list(final_prices.keys()):
@@ -406,6 +430,27 @@ def add_prices(self, asset_id, start_time, end_time, resolution, save=False):
             self.__user_added_times.append(identifier)
             if save:
                 self.queue_backtest_write = True
+
+            # This makes sure that we keep track of our bounds which is just generally kind of useful
+            # Any time a new price event is added we check this
+            if self.user_start is None:
+                self.user_start = start_time
+            else:
+                if start_time < self.user_start:
+                    self.user_start = start_time
+
+            if self.user_stop is None:
+                self.user_stop = end_time
+            else:
+                if end_time > self.user_stop:
+                    self.user_stop = end_time
+
+            # Now keep track of the smallest price event added
+            if self.min_resolution is None:
+                self.min_resolution = resolution
+            else:
+                if resolution < self.min_resolution:
+                    self.min_resolution = resolution
         else:
             print("already identified")
 
@@ -500,7 +545,7 @@ def format_account_data(self, local_time) -> typing.Tuple[typing.Dict[typing.Uni
     def __account_was_used(self, column) -> bool:
         show_zero_delta = self.preferences['settings']['show_tickers_with_zero_delta']
 
-        # Just check if its in the traded assets or if the zero delta is enabled
+        # Just check if it's in the traded assets or if the zero delta is enabled
         used = self.__traded_assets
         is_used = column in used or 'Account Value (' + self.quote_currency + ')' == column
 
@@ -509,7 +554,7 @@ def __account_was_used(self, column) -> bool:
         return output
 
     def __next_color(self):
-        # This should be a generator but it doesn't work without doing a foreach loop
+        # This should be a generator, but it doesn't work without doing a foreach loop
         try:
             return next(self.__color_generator)
         except StopIteration:
@@ -532,6 +577,13 @@ def run(self) -> BacktestResult:
         #  don't need to happen for every single key type
         self.quote_currency = self.preferences['settings']['quote_account_value_in']
 
+        # Get the symbol used for the benchmark
+        benchmark_symbol = self.preferences["settings"]["benchmark_symbol"]
+
+        if benchmark_symbol is not None:
+            # Check locally for the data and add to price_cache if we do not have it
+            self.add_prices(benchmark_symbol, self.user_start, self.user_stop, self.min_resolution)
+
         prices = self.sync_prices()
 
         # Organize each price into this structure: [epoch, "BTC-USD", price, open, high, low, close, volume]
@@ -547,9 +599,22 @@ def run(self) -> BacktestResult:
             try:
                 self.interface.receive_price(k, v[use_price].iloc[0])
             except IndexError:
-                raise IndexError('No cached or downloaded data available. Try adding arguments such as to="1y" '
-                                 'in the backtest command. If there should be data downloaded, try deleting your'
-                                 ' ./price_caches folder.')
+                def check_if_any_column_has_prices(price_dict: dict) -> bool:
+                    """
+                    In dictionary of symbols, check if at least one key has data
+                    """
+                    for j in price_dict:
+                        if not price_dict[j].empty:
+                            return True
+                    return False
+
+                if not check_if_any_column_has_prices(prices):
+                    raise IndexError('No cached or downloaded data available. Try adding arguments such as to="1y" '
+                                     'in the backtest command. If there should be data downloaded, try deleting your'
+                                     ' ./price_caches folder.')
+                else:
+                    raise IndexError(f"Data for symbol {k} is empty. Are you using a symbol that is incompatible "
+                                     f"with this exchange?")
 
             # Be sure to send in the initial time
             self.interface.receive_time(v['time'].iloc[0])
@@ -566,6 +631,11 @@ def run(self) -> BacktestResult:
         # pushing these prices together makes the time go weird
         self.prices = sorted(self.prices)
 
+        if prices == {} or self.price_events == []:
+            raise ValueError("Either no price data or backtest events given. "
+                             "Try setting an argument such as to='1y' in the .backtest() command.\n"
+                             "Example: strategy.backtest(to='1y')")
+
         self.current_time = self.prices[0][0]
 
         self.initial_time = self.current_time
@@ -583,11 +653,6 @@ def run(self) -> BacktestResult:
                 'teardown': self.price_events[i][6]
             }
 
-        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.")
-
         # Initialize this before the callbacks so it works in the initialization functions
         self.time = self.initial_time
 
@@ -754,9 +819,19 @@ def run(self) -> BacktestResult:
             # display a tooltip whenever the cursor is vertically in line with a glyph
             mode='vline'
         )
-
-        # Back up the epoch list so that it can be used later for re-sampling
-        epoch_backup = cycle_status['time'].tolist()
+        
+        # Define a helper function to avoid repeating code
+        def add_trace(self_, figure_, time_, data_, label):
+            source = ColumnDataSource(data=dict(
+                        time=time_,
+                        value=data_.values.tolist()
+                    ))
+            figure_.step('time', 'value',
+                         source=source,
+                         line_width=2,
+                         color=self_.__next_color(),
+                         legend_label=label,
+                         mode="after")
 
         if self.preferences['settings']['GUI_output']:
             global_x_range = None
@@ -766,31 +841,34 @@ def run(self) -> BacktestResult:
             for column in cycle_status:
                 if column != 'time' and self.__account_was_used(column):
                     p = figure(plot_width=900, plot_height=200, x_axis_type='datetime')
-                    source = ColumnDataSource(data=dict(
-                        time=time,
-                        value=cycle_status[column].tolist()
-                    ))
-                    p.step('time', 'value',
-                           source=source,
-                           line_width=2,
-                           color=self.__next_color(),
-                           legend_label=column,
-                           mode="after",
-                           )
-
-                    # Replica of whats above to add the no-trade line to the backtest
-                    if column == 'Account Value (' + self.quote_currency + ')':
-                        source = ColumnDataSource(data=dict(
-                            time=time,
-                            value=no_trade_cycle_status['Account Value (No Trades)'].tolist()
-                        ))
-                        p.step('time', 'value',
-                               source=source,
-                               line_width=2,
-                               color=self.__next_color(),
-                               legend_label='Account Value (No Trades)',
-                               mode="after")
+                    add_trace(self, p, time, cycle_status[column], column)
 
+                    # Add the no-trade line to the backtest
+                    if column == 'Account Value (' + self.quote_currency + ')':
+                        add_trace(self, p, time, no_trade_cycle_status['Account Value (No Trades)'],
+                                  'Account Value (No Trades)')
+
+                        # Add the benchmark, if requested
+                        if benchmark_symbol is not None:
+                            # This normalizes the benchmark value
+                            initial_account_value = cycle_status['Account Value (' + self.quote_currency + ')'].iloc[0]
+                            initial_benchmark_value = prices[benchmark_symbol][use_price].iloc[0]
+
+                            # This multiplier brings the initial asset price to the initial account value
+                            # initial_account_value = initial_benchmark_value * x
+                            multiplier = initial_account_value / initial_benchmark_value
+
+                            normalized_compare_series = prices[benchmark_symbol][use_price].multiply(multiplier)
+                            normalized_compare_time_series = prices[benchmark_symbol]['time']
+
+                            # We need to also cast the time series that is needed to compare
+                            # because it's only been done for the cycle status time
+                            normalized_compare_time_series = [dt.fromtimestamp(ts) for ts in
+                                                              normalized_compare_time_series]
+                            add_trace(self, p, normalized_compare_time_series,
+                                      normalized_compare_series,
+                                      f'Normalized Benchmark ({benchmark_symbol})')
+                            
                     p.add_tools(hover)
 
                     # Format graph
@@ -815,128 +893,127 @@ def is_number(s):
             except ValueError:
                 return False
 
-        dataframes = {
+        history_and_returns = {
             'history': cycle_status
         }
         metrics_indicators = {}
         user_callbacks = {}
 
-        # Check if it needs resampling
+        result_object = BacktestResult(history_and_returns, {
+            'created': self.interface.paper_trade_orders,
+            'limits_executed': self.interface.executed_orders,
+            'limits_canceled': self.interface.canceled_orders,
+            'executed_market_orders': self.interface.market_order_execution_details
+        }, self.pd_prices, self.initial_time, self.interface.time(), self.quote_currency, self.price_events)
+
+        # If they set resampling we use resampling for everything
         resample_setting = self.preferences['settings']['resample_account_value_for_metrics']
         if isinstance(resample_setting, str) or is_number(resample_setting):
-            # Backing arrays. We can't append directly to the dataframe so array has to also be made
-
-            def search_price(asset_id, epoch):
-                # In this case because each asset is called individually
-                def search(arr, size, x):
-                    while True:
-                        if self.__current_search_index == size:
-                            # Must be the last one in the list
-                            return self.__current_search_index - 1
-
-                        if arr[self.__current_search_index] <= x <= arr[self.__current_search_index + 1]:
-                            # Found it in this range
-                            return self.__current_search_index
-                        else:
-                            self.__current_search_index += 1
-                try:
-                    prices_ = self.pd_prices[asset_id][self.use_price]  # type: pd.Series
-                    times = self.pd_prices[asset_id]['time']  # type: pd.Series
-
-                    # Iterate and find the correct quote price
-                    index_ = search(times, times.size, epoch)
-                    # print('Found price for', asset_id, prices[index])
-                    return prices_[index_]
-                except KeyError:
-                    # Not a currency that we have data for at all
-                    return 0
-
-            resampled_returns = pd.DataFrame(columns=['time', 'value'])
-            resampled_backing_array = []
-
-            # Find the interval second value
-            interval_value = time_interval_to_seconds(resample_setting)
-
-            # Assign start and stop limits
-            epoch_start = epoch_backup[0]
-            epoch_max = epoch_backup[len(epoch_backup) - 1]
-
-            # Reset the current search index to zero for re-searching
-            self.__current_search_index = 0
-            # Going to push this in as a single column version of our price data so that __determine_price can handle it
-            self.pd_prices['Account Value (' + self.quote_currency + ')'] = pd.DataFrame()
-            self.pd_prices['Account Value (' + self.quote_currency + ')'][use_price] = \
-                cycle_status['Account Value (' + self.quote_currency + ')']
-
-            self.pd_prices['Account Value (' + self.quote_currency + ')']['time'] = cycle_status['time']
+            resample_to = resample_setting
+        else:
+            info_print('Resampling value not set, defaulting to 1 day.')
+            resample_to = '1d'
 
-            while epoch_start <= epoch_max:
-                # Append this dict to the array
-                resampled_backing_array.append({
-                    'time': epoch_start,
-                    'value': search_price('Account Value (' + self.quote_currency + ')', epoch_start)
-                })
+        interval_value = time_interval_to_seconds(resample_to)
 
-                # Increase the epoch value
-                epoch_start += interval_value
+        # This is where we run the actual resample
+        resampled_account_data_frame = result_object.resample_account('Account Value (' + self.quote_currency + ')',
+                                                                      interval_value)
 
-            # Put this in the dataframe
-            resampled_returns = resampled_returns.append(resampled_backing_array, ignore_index=True)
+        history_and_returns['resampled_account_value'] = resampled_account_data_frame
 
-            # This is the resampled version
-            dataframes['resampled_account_value'] = resampled_returns
+        returns = resampled_account_data_frame.copy(deep=True)
 
-            # Now we need to copy it and find the differences
-            returns = resampled_returns.copy(deep=True)
+        # Default diff parameters should do it
+        returns['value'] = returns['value'].pct_change()
 
-            # Default diff parameters should do it
-            returns['value'] = returns['value'].pct_change()
+        # Now write it to our dictionary
+        history_and_returns['returns'] = returns
 
-            # Now write it to our dictionary
-            dataframes['returns'] = returns
+        # -----=====*****=====-----
+        metrics_indicators['Compound Annual Growth Rate (%)'] = metrics.cagr(history_and_returns)
+        try:
+            metrics_indicators['Cumulative Returns (%)'] = metrics.cum_returns(history_and_returns)
+        except ZeroDivisionError:
+            raise ZeroDivisionError("Division by zero when calculating cumulative returns. "
+                                    "Are there valid account datapoints?")
 
-            # -----=====*****=====-----
-            metrics_indicators['Compound Annual Growth Rate (%)'] = metrics.cagr(dataframes)
+        def attempt(math_callable: typing.Callable, dict_of_dataframes: dict, kwargs: dict = None):
             try:
-                metrics_indicators['Cumulative Returns (%)'] = metrics.cum_returns(dataframes)
-            except ZeroDivisionError:
-                raise ZeroDivisionError("Division by zero when calculating cum returns. "
-                                        "Are there valid account datapoints?")
-
-            def attempt(math_callable: typing.Callable, dict_of_dataframes: dict, kwargs: dict = None):
-                try:
-                    if kwargs is None:
-                        kwargs = {}
-                    return math_callable(dict_of_dataframes, **kwargs)
-                except ZeroDivisionError:
-                    return 'failed'
-
-            risk_free_return_rate = self.preferences['settings']["risk_free_return_rate"]
-            trading_period = interval_value
-            metrics_indicators['Max Drawdown (%)'] = attempt(metrics.max_drawdown, dataframes)
-            metrics_indicators['Variance (%)'] = attempt(metrics.variance, dataframes)
-            metrics_indicators['Sortino Ratio'] = attempt(metrics.sortino, dataframes,
-                                                          {'risk_free_rate': risk_free_return_rate,
-                                                           'trading_period': trading_period})
-            metrics_indicators['Sharpe Ratio'] = attempt(metrics.sharpe, dataframes,
-                                                         {'risk_free_rate': risk_free_return_rate,
-                                                          'trading_period': trading_period})
-            metrics_indicators['Calmar Ratio'] = attempt(metrics.calmar, dataframes, {'trading_period': trading_period})
-            metrics_indicators['Volatility'] = attempt(metrics.volatility, dataframes)
-            metrics_indicators['Value-at-Risk'] = attempt(metrics.var, dataframes)
-            metrics_indicators['Conditional Value-at-Risk'] = attempt(metrics.cvar, dataframes)
-            # Add risk-free-return rate to dictionary
-            metrics_indicators['Risk Free Return Rate'] = risk_free_return_rate
-            # metrics_indicators['beta'] = attempt(metrics.beta, dataframes)
-            # Add the interval value to dictionary
-            metrics_indicators['Resampled Time'] = interval_value
-            # -----=====*****=====-----
-
+                if kwargs is None:
+                    kwargs = {}
+                result = math_callable(dict_of_dataframes, **kwargs)
+                if result == np.NAN:
+                    result = None
+                return result
+            except (ZeroDivisionError, Exception) as e_:
+                return f'failed: {e_}'
+
+        risk_free_return_rate = self.preferences['settings']["risk_free_return_rate"]
+        metrics_indicators['Max Drawdown (%)'] = attempt(metrics.max_drawdown, history_and_returns)
+        metrics_indicators['Variance (%)'] = attempt(metrics.variance, history_and_returns,
+                                                     {'trading_period': interval_value})
+        metrics_indicators['Sortino Ratio'] = attempt(metrics.sortino, history_and_returns,
+                                                      {'risk_free_rate': risk_free_return_rate,
+                                                       'trading_period': interval_value})
+        metrics_indicators['Sharpe Ratio'] = attempt(metrics.sharpe, history_and_returns,
+                                                     {'risk_free_rate': risk_free_return_rate,
+                                                      'trading_period': interval_value})
+        metrics_indicators['Calmar Ratio'] = attempt(metrics.calmar, history_and_returns, 
+                                                     {'trading_period': interval_value})
+        metrics_indicators['Volatility'] = attempt(metrics.volatility, history_and_returns,
+                                                   {'trading_period': interval_value})
+        metrics_indicators['Value-at-Risk'] = attempt(metrics.var, history_and_returns)
+        metrics_indicators['Conditional Value-at-Risk'] = attempt(metrics.cvar, history_and_returns)
+        
+        # Add risk-free-return rate to dictionary
+        metrics_indicators['Risk Free Return Rate'] = risk_free_return_rate
+        # metrics_indicators['beta'] = attempt(metrics.beta, dataframes)
+        # Add the interval value to dictionary
+        metrics_indicators['Resampled Time'] = interval_value
+        # -----=====*****=====-----
+
+        # If a benchmark was requested, add it to the pd_prices frame
+        if benchmark_symbol is not None:
+            # Resample the benchmark results
+            resampled_benchmark_value = result_object.resample_account(benchmark_symbol,
+                                                                       interval_value,
+                                                                       use_asset_history=True,
+                                                                       use_price=use_price)
+            
+            # Push data into the dictionary for use by the metrics
+            history_and_returns['benchmark_value'] = resampled_benchmark_value
+            history_and_returns['benchmark_returns'] = resampled_benchmark_value.copy(deep=True)
+            history_and_returns['benchmark_returns']['value'] = \
+                history_and_returns['benchmark_returns']['value'].pct_change()
+            
+            # Calculate beta
+            metrics_indicators['Beta'] = attempt(metrics.beta, history_and_returns, 
+                                                 {"trading_period": interval_value})
+
+        # This trys to reference vars created in the resample portion, so it has to also be in the specified
+        #  resample if
+        # --
         # Run this last so that the user can override what they want
         for callback in self.callbacks:
-            user_callbacks[callback.__name__] = callback(dataframes)
-
-        result_object = BacktestResult(dataframes, metrics_indicators, user_callbacks)
+            user_callbacks[callback.__name__] = callback(history_and_returns, metrics_indicators)
+
+        # Remove NaN values here
+        history_and_returns['resampled_account_value'] = history_and_returns['resampled_account_value'].\
+            where(history_and_returns['resampled_account_value'].notnull(), None)
+
+        # Remove NaN values on this one too
+        history_and_returns['returns'] = history_and_returns['returns'].where(history_and_returns['returns'].notnull(),
+                                                                              None)
+        # Lastly remove Nan values in the metrics
+        for i in metrics_indicators:
+            if not isinstance(metrics_indicators[i], str) and np.isnan(metrics_indicators[i]):
+                metrics_indicators[i] = None
+
+        # Assign all these new values back to the result object
+        result_object.history_and_returns = history_and_returns
+        result_object.metrics = metrics_indicators
+        result_object.user_callbacks = user_callbacks
 
         self.interface.set_backtesting(False)
         return result_object
diff --git a/blankly/exchanges/interfaces/paper_trade/backtest_result.py b/blankly/exchanges/interfaces/paper_trade/backtest_result.py
index 88df21a9..9f5d3637 100644
--- a/blankly/exchanges/interfaces/paper_trade/backtest_result.py
+++ b/blankly/exchanges/interfaces/paper_trade/backtest_result.py
@@ -17,22 +17,33 @@
 """
 
 from pandas import DataFrame
+from blankly.utils.utils import time_interval_to_seconds as _time_interval_to_seconds
 
 
 class BacktestResult:
-    def __init__(self, dataframes: dict, metrics: dict, user_callbacks: dict):
-        self.dataframes = dataframes
-        self.metrics = metrics
-        self.user_callbacks = user_callbacks
+    def __init__(self, history_and_returns: dict, trades: dict, history: dict,
+                 start_time: float, stop_time: float, quote_currency: str, price_events: list):
+        # This can use a ton of memory if these attributes are not cleared
+        self.history_and_returns = history_and_returns
+        self.metrics = None  # Assigned after construction
+        self.user_callbacks = None  # Assigned after construction
+        self.trades = trades
+        self.history = history
+
+        self.quote_currency = quote_currency
+        self.price_events = price_events
+
+        self.start_time = start_time
+        self.stop_time = stop_time
 
     def get_account_history(self) -> DataFrame:
-        return self.dataframes['history']
+        return self.history_and_returns['history']
 
     def get_returns(self) -> DataFrame:
-        return self.dataframes['returns']
+        return self.history_and_returns['returns']
 
     def get_resampled_account(self) -> DataFrame:
-        return self.dataframes['resampled_account_value']
+        return self.history_and_returns['resampled_account_value']
 
     def get_user_callback_results(self) -> dict:
         return self.user_callbacks
@@ -40,20 +51,89 @@ def get_user_callback_results(self) -> dict:
     def get_metrics(self) -> dict:
         return self.metrics
 
+    def resample_account(self, symbol, interval: [str, float],
+                         use_asset_history: bool = False,
+                         use_price=None) -> DataFrame:
+        """
+        Resample the raw account value metrics to any resolution
+
+        Args:
+            symbol: The column to resample at the interval resolution. This can include the account value column
+            interval: A string such as '1h' or '1m' or a number in seconds such as 3600 or 60 which the values
+            will be resampled at
+            use_asset_history: Use the history from the assets rather than the account history
+            use_price: Specify a price to use when querying comparison columns
+        """
+        search_index = 0
+
+        def search_price(values, times, epoch):
+            # In this case because each asset is called individually
+            def search(arr, size, x):
+                # Use a global search index to accelerate search
+                nonlocal search_index
+
+                while True:
+                    if search_index == size:
+                        # Must be the last one in the list
+                        return search_index - 1
+
+                    if arr[search_index] <= x <= arr[search_index + 1]:
+                        # Found it in this range
+                        return search_index
+                    else:
+                        search_index += 1
+            try:
+                # Iterate and find the correct quote price
+                index_ = search(times, len(times), epoch)
+                return values[index_]
+            except KeyError:
+                # Not a currency that we have data for at all
+                return 0
+
+        resampled_array = []
+        interval = _time_interval_to_seconds(interval)
+
+        if use_asset_history:
+            # Find the necessary values to assemble the resamples
+            time_array = self.history[symbol]['time'].tolist()
+            price_array = self.history[symbol][use_price].tolist()
+        else:
+            # Find the necessary values to assemble the resamples
+            time_array = self.history_and_returns['history']['time'].tolist()
+            price_array = self.history_and_returns['history'][symbol].tolist()
+
+        # Add the epoch
+        epoch_start = time_array[0]
+        epoch_stop = time_array[-1]
+
+        while epoch_start <= epoch_stop:
+            # Append this dict to the array
+            resampled_array.append({
+                'time': epoch_start,
+                'value': search_price(price_array, time_array, epoch_start)
+            })
+
+            # Increase the epoch value
+            epoch_start += interval
+
+        # Turn that resample into a dataframe
+        df_conversion = DataFrame(columns=['time', 'value'])
+        return df_conversion.append(resampled_array, ignore_index=True)
+
     def __str__(self):
         return_string = "\n"
         return_string += "Historical Dataframes: \n"
 
         return_string += "Account History: \n"
-        return_string += self.dataframes['history'].__str__()
+        return_string += self.history_and_returns['history'].__str__()
         return_string += "\n"
 
         return_string += "Account Returns: \n"
-        return_string += self.dataframes['returns'].__str__()
+        return_string += self.history_and_returns['returns'].__str__()
         return_string += "\n"
 
         return_string += "Resampled Account Value: \n"
-        return_string += self.dataframes['resampled_account_value'].__str__()
+        return_string += self.history_and_returns['resampled_account_value'].__str__()
         return_string += "\n"
 
         return_string += "Blankly Metrics: \n"
diff --git a/examples/backtest.json b/examples/backtest.json
index 464eb0fe..5141d051 100644
--- a/examples/backtest.json
+++ b/examples/backtest.json
@@ -14,6 +14,7 @@
     "resample_account_value_for_metrics": "1d",
     "quote_account_value_in": "USD",
     "ignore_user_exceptions": true,
-    "risk_free_return_rate": 0.0
+    "risk_free_return_rate": 0.0,
+    "benchmark_symbol" : null
   }
 }
\ No newline at end of file
diff --git a/tests/config/keys_demo.json b/tests/config/keys_demo.json
index 70054afe..2b354530 100644
--- a/tests/config/keys_demo.json
+++ b/tests/config/keys_demo.json
@@ -6,6 +6,13 @@
             "API_PASS": "**********"
         }
     },
+    "kucoin": {
+        "kucoin portfolio": {
+            "API_KEY": "************************",
+            "API_SECRET": "************************************",
+            "API_PASS": "*********************"
+        }
+    },
     "binance": {
         "binance test portfolio": {
             "API_KEY": "**************************************************************",
diff --git a/tests/config/settings.json b/tests/config/settings.json
index 64a3115d..895fcc95 100644
--- a/tests/config/settings.json
+++ b/tests/config/settings.json
@@ -1,6 +1,5 @@
 {
   "settings": {
-    "account_update_time": 5000,
     "use_sandbox": true,
     "use_sandbox_websockets": false,
     "websocket_buffer_size": 10000,
@@ -9,6 +8,9 @@
     "coinbase_pro": {
       "cash": "USD"
     },
+    "kucoin": {
+      "cash": "USDT"
+    },
     "binance": {
       "cash": "USDT",
       "binance_tld": "us"
diff --git a/tests/config/settings_live_enabled.json b/tests/config/settings_live_enabled.json
index 31c59ae7..69be40e9 100644
--- a/tests/config/settings_live_enabled.json
+++ b/tests/config/settings_live_enabled.json
@@ -1,6 +1,5 @@
 {
   "settings": {
-    "account_update_time": 5000,
     "use_sandbox": false,
     "use_sandbox_websockets": false,
     "websocket_buffer_size": 10000,
@@ -9,6 +8,9 @@
     "coinbase_pro": {
       "cash": "USD"
     },
+    "kucoin": {
+      "cash": "USDT"
+    },
     "binance": {
       "cash": "USDT",
       "binance_tld": "us"
diff --git a/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py b/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py
index 1e234731..0bb22219 100644
--- a/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py
+++ b/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py
@@ -21,8 +21,7 @@
 import pytest
 from pytest_mock import MockerFixture
 
-from blankly.exchanges.interfaces.alpaca.alpaca_auth import AlpacaAuth
-from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory
+import blankly
 
 
 class mock_alpaca_direct_calls:
@@ -199,15 +198,18 @@ def get_account(self):
 
 
 @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()
+def alpaca_mock_interface(mocker: MockerFixture):
+    keys_file_path = Path("tests/config/keys.json").resolve().__str__()
+    settings_file_path = Path("tests/config/settings.json").resolve().__str__()
 
-    auth_obj = AlpacaAuth(keys_file_path, "alpaca test portfolio")
     mocker.patch("alpaca_trade_api.REST", new=mock_alpaca_direct_calls)
-    _, alpaca_interface = DirectCallsFactory.create("alpaca", auth_obj, settings_file_path)
+    alpaca = blankly.Alpaca(keys_path=keys_file_path,
+                            settings_path=settings_file_path,
+                            portfolio_name="alpaca test portfolio")
+    # auth_obj = AlpacaAuth(keys_file_path, "alpaca test portfolio")
+    # _, alpaca_interface = DirectCallsFactory.create("alpaca", auth_obj, settings_file_path)
 
-    return alpaca_interface
+    return alpaca.interface
 
 
 # TODO: Need to create a functional testing package
diff --git a/tests/exchanges/interfaces/alpaca/test_alpaca_interface_functional.py b/tests/exchanges/interfaces/alpaca/test_alpaca_interface_functional.py
index 7d9c621e..bb832c8e 100644
--- a/tests/exchanges/interfaces/alpaca/test_alpaca_interface_functional.py
+++ b/tests/exchanges/interfaces/alpaca/test_alpaca_interface_functional.py
@@ -27,9 +27,8 @@
 import pytest
 import pytz
 
-from blankly.exchanges.interfaces.alpaca.alpaca_auth import AlpacaAuth
+import blankly
 from blankly.exchanges.interfaces.alpaca.alpaca_interface import AlpacaInterface
-from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory
 
 timeZ_Ny = pytz.timezone('America/New_York')
 MARKET_OPEN = datetime.time(hour=9, minute=0, second=0, tzinfo=timeZ_Ny)
@@ -41,9 +40,13 @@ def alpaca_interface():
     keys_file_path = Path("tests/config/keys.json").resolve()
     settings_file_path = Path("tests/config/settings.json").resolve()
 
-    auth_obj = AlpacaAuth(str(keys_file_path), "alpaca test portfolio")
-    _, alpaca_interface = DirectCallsFactory.create("alpaca", auth_obj, str(settings_file_path))
-    return alpaca_interface
+    alpaca = blankly.Alpaca(keys_path=keys_file_path,
+                            settings_path=settings_file_path,
+                            portfolio_name="alpaca test portfolio")
+
+    # auth_obj = AlpacaAuth(str(keys_file_path), "alpaca test portfolio")
+    # _, alpaca_interface = DirectCallsFactory.create("alpaca", auth_obj, str(settings_file_path))
+    return alpaca.interface
 
 
 def test_get_exchange(alpaca_interface: AlpacaInterface) -> None:
diff --git a/tests/exchanges/interfaces/binance/test_binance_interface.py b/tests/exchanges/interfaces/binance/test_binance_interface.py
index 14927a7e..b81b1fa6 100644
--- a/tests/exchanges/interfaces/binance/test_binance_interface.py
+++ b/tests/exchanges/interfaces/binance/test_binance_interface.py
@@ -22,9 +22,8 @@
 import pytest
 
 import blankly
-from blankly.exchanges.interfaces.binance.binance_auth import BinanceAuth
+
 from blankly.exchanges.interfaces.binance.binance_interface import BinanceInterface
-from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory
 from blankly.utils.utils import AttributeDict
 
 
@@ -46,9 +45,10 @@ def binance_interface():
     keys_file_path = Path("tests/config/keys.json").resolve()
     settings_file_path = Path("tests/config/settings.json").resolve()
 
-    auth_obj = BinanceAuth(str(keys_file_path), "Spot Test Key")
-    _, binance_interface = DirectCallsFactory.create("binance", auth_obj, str(settings_file_path))
-    return binance_interface
+    binance = blankly.Binance(keys_path=keys_file_path,
+                              settings_path=settings_file_path, portfolio_name='Spot Test Key')
+
+    return binance.interface
 
 
 def test_get_exchange(binance_interface: BinanceInterface) -> None:
diff --git a/tests/exchanges/interfaces/coinbase_pro/test_coinbase_pro_interface.py b/tests/exchanges/interfaces/coinbase_pro/test_coinbase_pro_interface.py
index 87343a19..c265ed40 100644
--- a/tests/exchanges/interfaces/coinbase_pro/test_coinbase_pro_interface.py
+++ b/tests/exchanges/interfaces/coinbase_pro/test_coinbase_pro_interface.py
@@ -15,17 +15,15 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
+
+
 import unittest
-from datetime import datetime as dt
 from pathlib import Path
 
-import dateparser
 import pytest
 
 import blankly
-from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_auth import CoinbaseProAuth
 from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_interface import CoinbaseProInterface
-from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory
 
 
 class CoinbaseInterface2(unittest.TestCase):
@@ -46,9 +44,13 @@ def coinbase_interface():
     keys_file_path = Path("tests/config/keys.json").resolve()
     settings_file_path = Path("tests/config/settings.json").resolve()
 
-    auth_obj = CoinbaseProAuth(str(keys_file_path), "Sandbox Portfolio")
-    _, coinbase_interface = DirectCallsFactory.create("coinbase_pro", auth_obj, str(settings_file_path))
-    return coinbase_interface
+    coinbase_pro = blankly.CoinbasePro(keys_path=keys_file_path,
+                                       settings_path=settings_file_path,
+                                       portfolio_name='Sandbox Portfolio')
+
+    # auth_obj = CoinbaseProAuth(str(keys_file_path), "Sandbox Portfolio")
+    # _, coinbase_interface = DirectCallsFactory.create("coinbase_pro", auth_obj, str(settings_file_path))
+    return coinbase_pro.interface
 
 
 def test_get_exchange(coinbase_interface: CoinbaseProInterface) -> None:
diff --git a/tests/exchanges/interfaces/ftx/test_ftx_interface.py b/tests/exchanges/interfaces/ftx/test_ftx_interface.py
new file mode 100644
index 00000000..495400f5
--- /dev/null
+++ b/tests/exchanges/interfaces/ftx/test_ftx_interface.py
@@ -0,0 +1,210 @@
+"""
+
+ATTENTION:
+FTX has NO sandbox mode, these are methods specifically designed
+to test interface methods which interact with LIVE orders, and 
+move REAL funds. It is not recommended that you run these tests.
+They must be executed individually and with great caution. Blankly
+is not responsible for losses incurred by improper use of this test. 
+
+How do we test methods which affect live funds?
+
+There are 5 main methods that need to be tested which involve
+using live funds
+
+1. get_open_orders()
+2. market_order()
+3. limit_order()
+4. cancel_order
+
+We test these by creating a series of assertations that can only
+pass if everything works properly. 
+
+"""
+
+import time
+import sys
+
+import blankly
+from pathlib import Path
+from typing import List
+
+from blankly.exchanges.interfaces.ftx.ftx_interface import FTXInterface
+import test_ftx_interface_utils as test_utils
+
+
+def ftx_interface() -> FTXInterface:
+    keys_file_path = Path("../../../config/keys.json").resolve()
+    settings_file_path = Path("../../../config/settings.json").resolve()
+
+    ftx = blankly.FTX(keys_path=keys_file_path,
+                               settings_path=settings_file_path,
+                               portfolio_name='Main Account')
+
+    # auth_obj = FTXAuth(str(keys_file_path), "Main Account")
+    # _, ftx_interface = DirectCallsFactory.create("ftx", auth_obj, str(settings_file_path))
+    return ftx.interface
+
+
+"""
+cancels ALL open orders and distributes funds for testing purposes
+
+pass command line argument reset to activate
+
+use with extreme caution
+
+"""
+
+
+def reset(ftx_interface: FTXInterface):
+    validation = input(
+        "Continuing with this test will cancel ALL existing open orders and redistribute funds for testing purposes. "
+        "Continue (Y/n)? ")
+    if validation.lower() != "y":
+        print("aborting test")
+        quit()
+
+    for order in ftx_interface.get_open_orders():
+        ftx_interface.cancel_order("BTC/USD", order["id"])
+
+
+def main():
+    interface: FTXInterface = ftx_interface()
+
+    validation = input(
+        "To complete this test properly, you should have around $10 in USD in your FTX account.\nThis test will "
+        "involve the conversion of no more than $10 to BTC.\nDo not have any trades going on while completing this "
+        "test.\nContinue (Y/n)? ")
+    if validation.lower() != "y":
+        quit()
+
+    if len(sys.argv) == 2:
+        if sys.argv[1] == "reset":
+            reset(interface)
+        else:
+            print("Invalid command line argument. Aborting")
+
+    # start testing ###
+
+    initial_num_open_orders = len(interface.get_open_orders())
+
+    # creates a buy limit order unlikely to be fulfilled (buying a bitcoin for $5)
+    limit_order = interface.limit_order("BTC/USD", "buy", 1, 5)
+    assert len(
+        interface.get_open_orders()) == 1 + initial_num_open_orders, f"expected {1 + initial_num_open_orders} " \
+                                                                     f"but got {len(interface.get_open_orders())}"
+    assert limit_order.get_status()['status'] == "new" or limit_order.get_status()[
+        'status'] == "open", f"expected open but got {limit_order.get_status()['status']} of " \
+                             f"type {type(limit_order.get_status()['status'])}"
+
+    open_order_ids: List[int] = test_utils.get_open_order_ids(interface.get_open_orders())
+    limit_order_id = limit_order.get_id()
+    assert limit_order_id in open_order_ids, f"id {limit_order_id} (type: {type(limit_order_id)}) not in " \
+                                             f"list {open_order_ids} (type: {type(open_order_ids)})"
+
+    # cancels the open limit order
+    order_id = int(interface.cancel_order("BTC/USD", limit_order.get_id()))
+
+    # wait for order cancellation to go through
+    print("waiting for order cancellation to be accepted")
+    while interface.get_order("BTC/USD", order_id)["status"] != "closed":
+        print(
+            f"waiting for order to be cancelled. Current status: " + interface.get_order("BTC/USD", order_id)["status"],
+            end='\r')
+        time.sleep(1)
+    time.sleep(5)
+    assert (interface.get_order("BTC/USD", order_id)["status"] == "closed")
+    print("order successfully cancelled")
+
+    curr_num_open_orders = len(interface.get_open_orders())
+    assert curr_num_open_orders == initial_num_open_orders, f"expected {initial_num_open_orders} " \
+                                                            f"open orders, but got {curr_num_open_orders}\nopen " \
+                                                            f"order list:\n{interface.get_open_orders()}"
+    assert order_id not in test_utils.get_open_order_ids(interface.get_open_orders())
+
+    # creates a buy market order for $5 worth of bitcoin
+    market_order = interface.market_order("BTC/USD", "buy", 5 / interface.get_price("BTC/USD"))
+
+    curr_order_status = ""
+    # waits for status of market order to update
+    while curr_order_status != "closed":
+        curr_order_status = interface.get_order("BTC/USD", market_order.get_id())["status"]
+        print("waiting for test market order to get accepted. Current status: " + curr_order_status, end='\r')
+        time.sleep(1)
+
+    time.sleep(5)
+
+    print("Test market order accepted... waiting for status to update")
+
+    market_order_info = interface.get_order("BTC/USD", market_order.get_id())
+
+    assert market_order.get_id() not in test_utils.get_open_order_ids(interface.get_open_orders())
+    assert market_order_info["id"] == market_order.get_id()
+    market_order_status = market_order_info["status"]
+    assert market_order_status == "closed", f"expected closed, but got {market_order_status} (type: " \
+                                            f"{type(market_order_status)}) \nopen orders: {interface.get_open_orders()}"
+
+    # creates a sell limit order unlikely to get fulfilled (selling ~$5 worth of bitcoin for $50,000)
+    price = 50000
+    size = 5 / interface.get_price("BTC/USD")
+    print(f"Attempting to create a limit order of {size} BTC for {price} USD")
+    limit_order = interface.limit_order("BTC/USD", "sell", price, size)
+
+    curr_open_orders = test_utils.get_open_order_ids(interface.get_open_orders())
+    assert limit_order.get_id() in curr_open_orders, \
+        f"expected order id {limit_order.get_id()} to be in list of open orders: \n{curr_open_orders}"
+    limit_order_info = interface.get_order("BTC/USD", limit_order.get_id())
+    assert limit_order_info["id"] == limit_order.get_id()
+
+    print("Waiting for limit order status to update. Please sit tight...")
+
+    curr_order_status = limit_order_info["status"]
+
+    while curr_order_status == "new":
+        curr_order_status = interface.get_order("BTC/USD", limit_order.get_id())["status"]
+        print(f"Waiting on order status to get updated. Current order status: {curr_order_status}", end="\r")
+        time.sleep(1)
+    time.sleep(5)
+    print("Limit order successfully closed")
+
+    assert curr_order_status == "open", f"expected open but got {curr_order_status} (type: {type(curr_order_status)})"
+    assert len(interface.get_open_orders()) == initial_num_open_orders + 1
+
+    # cancels sell limit order
+    order_id = interface.cancel_order("BTC/USD", limit_order.get_id())
+    assert order_id not in test_utils.get_open_order_ids(interface.get_open_orders())
+    assert len(interface.get_open_orders()) == initial_num_open_orders
+
+    # creates sell market order
+    market_order = interface.market_order("BTC/USD", "sell", 5 / interface.get_price("BTC/USD"))
+
+    while market_order.get_status() == "open":
+        print("Waiting for test market order to get accepted...", end='\r')
+        time.sleep(1)
+
+    time.sleep(5)
+
+    print("Test market order accepted... continuing test")
+
+    market_order_info = interface.get_order("BTC/USD", market_order.get_id())
+
+    assert market_order.get_id() not in test_utils.get_open_order_ids(interface.get_open_orders())
+    assert market_order_info["id"] == market_order.get_id()
+    assert market_order_info["status"] == "closed", f"expected closed but got" + market_order_info["status"]
+
+    # end testing ###
+
+    print("\n --- All tests passed --- \n")
+
+
+if __name__ == "__main__":
+
+    """    
+    Feel free to run the test as many times in a row as you would like for redundancy purposes
+    
+    net change in distribution of funds as a result of testing, between before and after the test,
+    is appoximately zero
+    """
+
+    for i in range(1):
+        main()
diff --git a/tests/exchanges/interfaces/ftx/test_ftx_interface_utils.py b/tests/exchanges/interfaces/ftx/test_ftx_interface_utils.py
new file mode 100644
index 00000000..cafcaad0
--- /dev/null
+++ b/tests/exchanges/interfaces/ftx/test_ftx_interface_utils.py
@@ -0,0 +1,11 @@
+from typing import List
+
+
+def get_open_order_ids(open_orders: List[dict]) -> List[str]:
+
+    ids: List[str] = []
+
+    for order in open_orders:
+        ids.append(str(order["id"]))
+    
+    return ids
diff --git a/tests/exchanges/interfaces/oanda/test_oanda_interface.py b/tests/exchanges/interfaces/oanda/test_oanda_interface.py
index e768f354..fd3927d0 100644
--- a/tests/exchanges/interfaces/oanda/test_oanda_interface.py
+++ b/tests/exchanges/interfaces/oanda/test_oanda_interface.py
@@ -21,9 +21,8 @@
 
 import pytest
 
-from blankly.exchanges.interfaces.oanda.oanda_auth import OandaAuth
+import blankly
 from blankly.exchanges.interfaces.oanda.oanda_interface import OandaInterface
-from blankly.exchanges.interfaces.direct_calls_factory import DirectCallsFactory
 from tests.helpers.comparisons import validate_response
 
 
@@ -32,9 +31,13 @@ def oanda_interface():
     keys_file_path = Path("tests/config/keys.json").resolve()
     settings_file_path = Path("tests/config/settings.json").resolve()
 
-    auth_obj = OandaAuth(str(keys_file_path), "oanda test portfolio")
-    _, oanda_interface = DirectCallsFactory.create("oanda", auth_obj, str(settings_file_path))
-    return oanda_interface
+    oanda = blankly.Oanda(keys_path=keys_file_path,
+                          settings_path=settings_file_path,
+                          portfolio_name='oanda test portfolio')
+
+    # auth_obj = OandaAuth(str(keys_file_path), "oanda test portfolio")
+    # _, oanda_interface = DirectCallsFactory.create("oanda", auth_obj, str(settings_file_path))
+    return oanda.interface
 
 
 def test_get_exchange(oanda_interface: OandaInterface) -> None:
diff --git a/tests/exchanges/test_interface_homogeneity.py b/tests/exchanges/test_interface_homogeneity.py
index 58a0a727..505d81c5 100644
--- a/tests/exchanges/test_interface_homogeneity.py
+++ b/tests/exchanges/test_interface_homogeneity.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 time
 import unittest
 from datetime import datetime as dt
@@ -58,6 +57,19 @@ def setUpClass(cls) -> None:
         cls.interfaces.append(cls.Coinbase_Pro_Interface)
         cls.data_interfaces.append(cls.Coinbase_Pro_Interface)
 
+        # Kucoin definition and appending
+        cls.Kucoin = blankly.Kucoin(portfolio_name="KC Sandbox Portfolio",
+                                    keys_path='./tests/config/keys.json',
+                                    settings_path="./tests/config/settings.json")
+        cls.Kucoin_Interface = cls.Kucoin.get_interface()
+        cls.interfaces.append(cls.Kucoin_Interface)
+
+        cls.Kucoin_data = blankly.Kucoin(portfolio_name="KC Data Keys",
+                                         keys_path='./tests/config/keys.json',
+                                         settings_path="./tests/config/settings_live_enabled.json")
+        cls.Kucoin_Interface_data = cls.Kucoin_data.get_interface()
+        cls.data_interfaces.append(cls.Kucoin_Interface_data)
+
         # Binance definition and appending
         cls.Binance = blankly.Binance(portfolio_name="Spot Test Key",
                                       keys_path='./tests/config/keys.json',
@@ -87,6 +99,13 @@ def setUpClass(cls) -> None:
         cls.Oanda_Interface = cls.Oanda.get_interface()
         cls.interfaces.append(cls.Oanda_Interface)
         cls.data_interfaces.append(cls.Oanda_Interface)
+        
+        cls.FTX = blankly.FTX(portfolio_name="Main Account",
+                              keys_path='./tests/config/keys.json',
+                              settings_path="./tests/config/settings.json")
+        cls.FTX_Interface = cls.FTX.get_interface()
+        cls.interfaces.append(cls.FTX_Interface)
+        cls.data_interfaces.append(cls.FTX_Interface)
 
         # Paper trade wraps binance
         cls.paper_trade_binance = blankly.PaperTrade(cls.Binance)
@@ -115,7 +134,6 @@ def test_get_account(self):
         responses = []
 
         availability_results = []
-
         for i in range(len(self.interfaces)):
             if self.interfaces[i].get_exchange_type() == "alpaca":
                 responses.append(self.interfaces[i].get_account()['AAPL'])
@@ -168,8 +186,10 @@ def test_market_order(self):
         # These are the immediate exchange responses that we aggregate & check
         exchange_responses = []
         for i in self.interfaces:
-            type_ = i.get_exchange_type()
 
+            type_ = i.get_exchange_type()
+            if type_ == "ftx":
+                continue
             if not (type_ == 'alpaca' or type_ == 'oanda'):
                 size = .01
             else:
@@ -239,6 +259,12 @@ def test_limit_order(self):
         coinbase_sell = self.Coinbase_Pro_Interface.limit_order('BTC-USD', 'sell', 100000, 1)
         self.check_limit_order(coinbase_sell, 'sell', 1, 'BTC-USD')
 
+        kucoin_buy = self.Kucoin_Interface.limit_order('ETH-USDT', 'buy', .01, 1)
+        self.check_limit_order(kucoin_buy, 'buy', 1, 'ETH-USDT')
+
+        kucoin_sell = self.Kucoin_Interface.limit_order('ETH-USDT', 'sell', 100000, 1)
+        self.check_limit_order(kucoin_sell, 'sell', 1, 'ETH-USDT')
+
         alpaca_buy = self.Alpaca_Interface.limit_order('AAPL', 'buy', 10, 1)
         self.check_limit_order(alpaca_buy, 'buy', 1, 'AAPL')
 
@@ -251,8 +277,8 @@ def test_limit_order(self):
         oanda_sell = self.Oanda_Interface.limit_order('EUR-USD', 'sell', 100000, 1)
         self.check_limit_order(oanda_sell, 'sell', 1, 'EUR-USD')
 
-        limits = [binance_buy, binance_sell, coinbase_buy, coinbase_sell, alpaca_sell, alpaca_buy,
-                  oanda_buy, oanda_sell]
+        limits = [binance_buy, binance_sell, coinbase_buy, coinbase_sell, kucoin_buy, kucoin_sell,
+                  alpaca_sell, alpaca_buy, oanda_buy, oanda_sell]
         responses = []
         status = []
 
@@ -261,6 +287,7 @@ def test_limit_order(self):
         open_orders = {
             'coinbase_pro': self.Coinbase_Pro_Interface.get_open_orders('BTC-USD'),
             'binance': self.Binance_Interface.get_open_orders('BTC-USDT'),
+            'kucoin': self.Kucoin_Interface.get_open_orders('ETH-USDT'),
             'alpaca': self.Alpaca_Interface.get_open_orders('AAPL'),
             'oanda': self.Oanda_Interface.get_open_orders('EUR-USD')
         }
@@ -272,6 +299,7 @@ def test_limit_order(self):
         # Just scan through both simultaneously to reduce code copying
         all_orders = open_orders['coinbase_pro']
         all_orders = all_orders + open_orders['binance']
+        all_orders = all_orders + open_orders['kucoin']
         all_orders = all_orders + open_orders['alpaca']
         all_orders = all_orders + open_orders['oanda']
 
@@ -301,6 +329,9 @@ def test_limit_order(self):
         cancels.append(self.Binance_Interface.cancel_order('BTC-USDT', binance_buy.get_id()))
         cancels.append(self.Binance_Interface.cancel_order('BTC-USDT', binance_sell.get_id()))
 
+        cancels.append(self.Kucoin_Interface.cancel_order('ETH-USDT', kucoin_buy.get_id()))
+        cancels.append(self.Kucoin_Interface.cancel_order('ETH-USDT', kucoin_sell.get_id()))
+
         cancels.append(self.Coinbase_Pro_Interface.cancel_order('BTC-USD', coinbase_sell.get_id()))
         cancels.append(self.Coinbase_Pro_Interface.cancel_order('BTC-USD', coinbase_buy.get_id()))
 
@@ -452,7 +483,9 @@ def test_get_product_history(self):
             end_time = i['time'].iloc[-1]
 
             # Make sure that the final time we have is within the resolution window. Notice this is shifted backwards
-            self.assertTrue(current_time-(build_day()) < end_time < current_time)
+            self.assertTrue(current_time-(build_day()) < end_time < current_time,
+                            f"\ncurrent_time-(build_day()): {current_time-(build_day())}\nend_time: "
+                            f"{end_time}\ncurrent_time: {current_time}\n")
 
             # Do the same, the start time has to be within a resolution interval of the actual time
             # This is shifted forward
@@ -462,9 +495,10 @@ def test_get_product_history(self):
 
     def test_get_order_filter(self):
         responses = []
-
+        types = []
         for i in self.interfaces:
             type_ = i.get_exchange_type()
+            types.append(type_)
             responses.append(i.get_order_filter(get_valid_symbol(type_)))
 
         self.assertTrue(compare_responses(responses))
diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py
index 289a94d0..44dff7c7 100644
--- a/tests/strategy/test_strategy.py
+++ b/tests/strategy/test_strategy.py
@@ -29,7 +29,8 @@ class StrategyTest(unittest.TestCase):
 
     @classmethod
     def setUpClass(cls) -> None:
-        pass
+        cls.ran_price_event = False
+        cls.ran_bar_event = False
 
     def price_event(self, price, symbol, state: StrategyState):
         self.assertTrue(isinstance(price, float))
@@ -38,34 +39,62 @@ def price_event(self, price, symbol, state: StrategyState):
 
         min_size = filter_['market_order']['base_min_size']
 
+        # Ensure you check for ints
+        if min_size == 1.0:
+            min_size = 1
+
         state.interface.market_order(symbol, 'buy', min_size)
 
         state.interface.market_order(symbol, 'buy', min_size)
 
+        self.ran_price_event = True
+
     def bar_event(self, price, symbol, state: StrategyState):
         self.assertTrue(isinstance(price, dict))
         # Check ohlcv on bar
         self.assertTrue(set(price.keys()) == {'open', 'high', 'low', 'close', 'volume'})
+        self.ran_bar_event = True
 
     def test_backtest(self):
         kwargs = {'keys_path': './tests/config/keys.json',
-                  'settings_path': "./tests/config/settings.json"
-                  }
+                  'settings_path': "./tests/config/settings.json"}
+
         self.exchanges = [
+            blankly.Kucoin(**kwargs),
+            blankly.FTX(**kwargs),
             blankly.CoinbasePro(**kwargs),
             blankly.Binance(**kwargs),
             blankly.Oanda(**kwargs),
             blankly.Alpaca(**kwargs)
         ]
 
-        self.strats = []
         for i in range(len(self.exchanges)):
             strategy = blankly.Strategy(self.exchanges[i])
             type_ = self.exchanges[i].interface.get_exchange_type()
             strategy.add_price_event(self.price_event, get_valid_symbol(type_), '1d')
-            self.strats.append(strategy)
-
-        for i in range(len(self.exchanges)):
-            self.strats[i].backtest(to='1y', settings_path='./tests/config/backtest.json',
-                                    quote_account_value_in=get_quote_asset(get_valid_symbol(
-                                        self.exchanges[i].interface.get_exchange_type())))
+            strategy.add_bar_event(self.bar_event, get_valid_symbol(type_), '1d')
+            print(f'Associated {get_valid_symbol(type_)} with {type_}.')
+
+            print(f"Testing: {self.exchanges[i].interface.get_exchange_type()} without benchmark")
+            quote_asset = get_quote_asset(get_valid_symbol(self.exchanges[i].interface.get_exchange_type()))
+            strategy.backtest(to='1y', settings_path='./tests/config/backtest.json',
+                              quote_account_value_in=quote_asset,
+                              initial_values={quote_asset: 1000000000},
+                              benchmark_symbol=None,
+                              continuous_caching=False)
+            # Of course, it'll pass if the function never runs, make sure it does
+            assert self.ran_price_event
+            self.ran_price_event = False
+            assert self.ran_bar_event
+            self.ran_bar_event = False
+            print(f"Testing: {self.exchanges[i].interface.get_exchange_type()} with benchmark")
+            strategy.backtest(to='1y', settings_path='./tests/config/backtest.json',
+                              quote_account_value_in=get_quote_asset(get_valid_symbol(
+                                        self.exchanges[i].interface.get_exchange_type())),
+                              benchmark_symbol=get_valid_symbol(self.exchanges[i].interface.get_exchange_type()),
+                              initial_values={quote_asset: 1000000000},
+                              continuous_caching=False)
+            assert self.ran_price_event
+            self.ran_price_event = False
+            assert self.ran_bar_event
+            self.ran_bar_event = False
diff --git a/tests/testing_utils.py b/tests/testing_utils.py
index bb98698e..9965140c 100644
--- a/tests/testing_utils.py
+++ b/tests/testing_utils.py
@@ -26,5 +26,9 @@ def get_valid_symbol(exchange: str):
         return 'AAPL'
     elif exchange == 'oanda':
         return 'EUR-USD'
+    elif exchange == 'ftx':
+        return 'BTC-USD'
+    elif exchange == 'kucoin':
+        return 'ETH-USDT'
     else:
         raise LookupError("Specified exchange not found.")
diff --git a/tests/websockets/test_crypto_websockets.py b/tests/websockets/test_crypto_websockets.py
new file mode 100644
index 00000000..8bdc109d
--- /dev/null
+++ b/tests/websockets/test_crypto_websockets.py
@@ -0,0 +1,157 @@
+"""
+    Unit tests for validating crypto behaviors
+    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
+
+
+def test_crypto_websocket():
+    test = CryptoWebsockets()
+
+    assert test.passed
+
+
+class CryptoWebsockets:
+
+    def __init__(self):
+        self.validated_responses = {
+            'orderbook': {
+                'coinbase_pro': False,
+                'binance': False
+            },
+            'prices': {
+                'coinbase_pro': False,
+                'binance': False,
+                'ftx':  False,
+            }
+        }
+
+        kwargs = {'keys_path': './tests/config/keys.json',
+                  'settings_path': "./tests/config/settings.json"}
+        # Create an exchange to reference the settings.json
+        blankly.CoinbasePro(**kwargs)
+
+        self.price_manager = blankly.TickerManager('coinbase_pro', 'BTC-USD')
+        self.orderbook_manager = blankly.OrderbookManager('coinbase_pro', 'BTC-USD')
+
+        self.passed = False
+
+        self.test_websockets()
+
+    def test_websockets(self):
+
+        self.price_manager.create_ticker(self.coinbase_price, override_symbol='BTC-USD',
+                                         override_exchange='coinbase_pro')
+
+        # Subscribe to a bunch with binance
+        self.price_manager.create_ticker(self.binance_price, override_symbol='BTC-USDT', override_exchange='binance')
+        self.price_manager.create_ticker(self.binance_price, override_symbol='ETH-USDT', override_exchange='binance')
+
+        self.price_manager.create_ticker(self.ftx_price, override_symbol='BTC-USD', override_exchange='ftx')
+
+        self.orderbook_manager.create_orderbook(self.coinbase_orderbook, override_symbol='BTC-USD',
+                                                override_exchange='coinbase_pro')
+
+        self.orderbook_manager.create_orderbook(self.binance_orderbook, override_symbol='BTC-USDT',
+                                                override_exchange='binance')
+
+        if self.timeout(180):
+            self.passed = True
+
+    def timeout(self, start_at: int):
+        def check_responses() -> bool:
+            for i in self.validated_responses['orderbook']:
+                if not self.validated_responses['orderbook'][i]:
+                    return False
+
+            for i in self.validated_responses['prices']:
+                if not self.validated_responses['prices'][i]:
+                    return False
+
+            return True
+
+        while start_at > 0:
+            # It can pass if it's valid within here
+            if check_responses():
+                assert True
+                return True
+            time.sleep(1)
+            start_at -= 1
+
+        # Better exit before here
+        # If this is thrown then the websockets aren't returning values
+        print(blankly.utils.pretty_print_json(self.validated_responses))
+        assert False
+
+    @staticmethod
+    def validate_price_event(event: dict):
+        try:
+            assert 'symbol' in event
+            assert 'price' in event
+            assert 'time' in event
+            assert 'trade_id' in event
+            assert 'size' in event
+            return True
+        except AssertionError:
+            return False
+
+    @staticmethod
+    def validate_orderbook_event(event: dict):
+        try:
+            # Validate the keys
+            assert (set(event.keys()) == {'bids', 'asks'})
+
+            # Validate the internal types
+            assert (isinstance(event['bids'][0], tuple))
+            assert (isinstance(event['asks'][0], tuple))
+
+            return True
+        except AssertionError:
+            return False
+
+    """
+    These can async validate each response
+    """
+    def coinbase_price(self, message):
+        if self.validate_price_event(message):
+            self.validated_responses['prices']['coinbase_pro'] = True
+            self.price_manager.close_websocket('BTC-USD', 'coinbase_pro')
+
+    def binance_price(self, message):
+        if self.validate_price_event(message):
+            self.validated_responses['prices']['binance'] = True
+            self.price_manager.close_websocket('BTC-USDT', 'binance')
+            self.price_manager.close_websocket('ETH-USDT', 'binance')
+
+    def ftx_price(self, message):
+        if self.validate_price_event(message):
+            self.validated_responses['prices']['ftx'] = True
+            self.price_manager.close_websocket('BTC-USD', 'ftx')
+
+    """
+    These can async validate each response
+    """
+    def coinbase_orderbook(self, message):
+        if self.validate_orderbook_event(message):
+            self.validated_responses['orderbook']['coinbase_pro'] = True
+            self.orderbook_manager.close_websocket('BTC-USD', 'coinbase_pro')
+
+    def binance_orderbook(self, message):
+        if self.validate_orderbook_event(message):
+            self.validated_responses['orderbook']['binance'] = True
+            self.orderbook_manager.close_websocket('BTC-USDT', 'binance')