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

Issue:
FTX & Kucoin Websockets

`cancel_order` does not move value from hold to available
## Description
When using Binance interface in backtesting, calling `cancel_order` on a sell limit order does not move the value of the asset from hold to available.

## settings.json
```json
settings.json here
```

## backtest.json (if applicable)
```json
backtest.json here
```

## Error (if applicable)
```text
Error here
```

## Platform Info
 - Python version:
 - Platform:

**Additional context**
Add any other context about the problem here.



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 57effda5..35e6ffbe 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -1,7 +1,9 @@
 name: Build
 
-# on: [push, pull_request]
-on: [push]
+on:
+  push:
+    branches: [ development ]
+
 
 jobs:
   build_wheels:
@@ -17,11 +19,11 @@ jobs:
       # Used to host cibuildwheel
       - uses: actions/setup-python@v2
 
-      - name: Install cibuildwheel
-        run: python -m pip install cibuildwheel==2.1.1
+      - name: Install wheel
+        run: python -m pip install wheel
 
       - name: Build wheels
-        run: python -m cibuildwheel --output-dir wheelhouse
+        run: pip wheel -w DEST_DIR .
         # to supply options, put them in 'env', like:
         # env:
         #   CIBW_SOME_OPTION: value
diff --git a/README.md b/README.md
index 3e011daf..0c42f795 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,6 @@ Check out our [website](https://blankly.finance) and our [docs](https://docs.bla
 [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.jpg?alt=media&token=a9dd030a-805c-447f-a970-2bc8e1815662" style="border-radius:10px"></a>
 
-
 ### Trade Stocks, Crypto, and Forex Seamlessly
 
 ```python
diff --git a/blankly/__init__.py b/blankly/__init__.py
index 33be752c..ca25f881 100644
--- a/blankly/__init__.py
+++ b/blankly/__init__.py
@@ -16,15 +16,18 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 import blankly.utils.utils
+import blankly.data as data
 from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro import CoinbasePro
 from blankly.exchanges.interfaces.binance.binance import Binance
 from blankly.exchanges.interfaces.alpaca.alpaca import Alpaca
 from blankly.exchanges.interfaces.oanda.oanda import Oanda
 from blankly.exchanges.interfaces.kucoin.kucoin import Kucoin
 from blankly.exchanges.interfaces.ftx.ftx import FTX
+from blankly.exchanges.interfaces.okx.okx import Okx
 from blankly.exchanges.interfaces.paper_trade.paper_trade import PaperTrade
 from blankly.exchanges.interfaces.keyless.keyless import KeylessExchange
 from blankly.frameworks.strategy import Strategy as Strategy
+from blankly.frameworks.model.model import Model as Model
 from blankly.frameworks.strategy import StrategyState as StrategyState
 from blankly.frameworks.screener.screener import Screener
 from blankly.frameworks.screener.screener_state import ScreenerState
@@ -40,10 +43,14 @@
 import blankly.indicators as indicators
 from blankly.utils import time_builder
 
+from blankly.enums import Side, OrderType, OrderStatus, TimeInForce
+
 from blankly.deployment.reporter_headers import Reporter as __Reporter_Headers
 is_deployed = False
 _screener_runner = None
 
+
+
 _backtesting = blankly.utils.check_backtesting()
 try:
     from blankly_external import Reporter as __Reporter
diff --git a/blankly/data/__init__.py b/blankly/data/__init__.py
new file mode 100644
index 00000000..cd0773ad
--- /dev/null
+++ b/blankly/data/__init__.py
@@ -0,0 +1,61 @@
+"""
+    Datareader types & imports
+    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/>.
+"""
+
+
+from enum import Enum as __Enum
+
+
+class DataTypes(__Enum):
+    ohlcv_csv = 0
+    tick_csv = 1
+    ohlcv_json = 2
+    event_json = 3
+
+
+from .data_reader import PriceReader, EventReader, TickReader
+
+
+"""
+Some datatype examples
+
+
+ohlcv_csv
+symbol1, time, open, high, low, close, volume, symbol2, time, open, high, low, close, volume
+
+
+ohlcv_json
+{
+    "symbol": {
+        "open": [],
+        "high": [],
+        "low": [],
+        "close": [],
+        "volume": []
+    }
+}
+
+
+event_json
+{
+    "event_type": {
+        "time": [],
+        "data": []
+    }
+
+}
+"""
diff --git a/blankly/data/data_reader.py b/blankly/data/data_reader.py
new file mode 100644
index 00000000..43c60a72
--- /dev/null
+++ b/blankly/data/data_reader.py
@@ -0,0 +1,226 @@
+"""
+    A reader for handling a variety of data formats
+    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 json
+import pandas as pd
+from enum import Enum
+
+from . import DataTypes
+
+"""
+This class seeks to solve these problems:
+
+- Read in custom price data for keyless exchange
+- Read in event-based
+"""
+
+
+class FileTypes(Enum):
+    csv = 'csv'
+    json = 'json'
+
+
+class __DataReader:
+    @staticmethod
+    def _check_length(df: pd.DataFrame, identifier: str):
+        try:
+            assert (len(df) > 2)
+        except AssertionError:
+            raise AssertionError(f"Must give data with at least 2 rows in {identifier}.")
+
+    @staticmethod
+    def __is_price_data(data_type: DataTypes) -> bool:
+        return data_type == DataTypes.ohlcv_json or data_type == DataTypes.ohlcv_csv
+
+    @property
+    def data(self):
+        return self._internal_dataset
+
+    def _write_dataset(self, contents: dict, key: str, required_columns: tuple):
+        try:
+            # Check if all the keys exist for each part of the dictionary
+            assert (all(key in contents for key in required_columns))
+        except AssertionError:
+            raise AssertionError(f"Must have at least these columns: {required_columns} for {key}")
+        self._internal_dataset[key] = pd.DataFrame.from_dict(contents)
+
+    def __init__(self, data_type: [DataTypes, str]):
+        self._internal_dataset = {}
+        self.type: DataTypes = data_type
+        self.price_data: bool = self.__is_price_data(data_type)
+
+
+class __FormatReader(__DataReader):
+    def __init__(self, data_type):
+        super().__init__(data_type)
+
+    @staticmethod
+    def _convert_to_list(file_path, symbol):
+        """
+        Primarily for CSV prices, this allows single or multi sets of symbols to be converted into matching arrays
+        """
+        # Turn it into a list
+        if isinstance(file_path, str):
+            file_paths = [file_path]
+        else:
+            file_paths = file_path
+
+        if isinstance(symbol, str):
+            symbols = [symbol]
+        else:
+            # Could be None still
+            symbols = symbol
+
+        return file_paths, symbols
+
+    def _parse_csv_prices(self, file_paths: list, symbols: list, columns: set) -> None:
+        if symbols is None:
+            raise LookupError("Must pass one or more symbols to identify the csv files")
+        if len(file_paths) != len(symbols):
+            raise LookupError(f"Mismatching symbol & file path lengths, got {len(file_paths)} and {len(symbols)} for "
+                              f"file paths and symbol lengths.")
+
+        for index in range(len(file_paths)):
+            # Load the file via pandas
+            contents = pd.read_csv(file_paths[index])
+
+            self._check_length(contents, file_paths[index])
+
+            # Check if its contained
+            assert (columns.issubset(contents.columns))
+
+            # Now push it directly into the dataset and sort by time
+            self._internal_dataset[symbols[index]] = contents.sort_values('time')
+
+    def _parse_json_prices(self, file_paths: list, keys: tuple) -> None:
+        for file in file_paths:
+            # Load the contents into json first because it has a ton of symbols
+            contents = json.loads(open(file).read())
+
+            for symbol in contents:
+                self._write_dataset(contents[symbol], symbol, keys)
+
+                self._check_length(self._internal_dataset[symbol], file)
+
+                # Ensure that the dataframe is sorted by time
+                self._internal_dataset[symbol] = self._internal_dataset[symbol].sort_values('time')
+
+
+class PriceReader(__FormatReader):
+    @staticmethod
+    def __associate(file_paths: list) -> str:
+        file_type = ''
+
+        def complain_if_different(ending_: str, type_: str):
+            nonlocal file_type
+            if file_type == '':
+                file_type = type_
+            elif file_type is not None and file_type != ending_:
+                raise LookupError("Cannot pass both csv files and json files into a single constructor.")
+
+        for file_path in file_paths:
+            if file_path[-3:] == 'csv':
+                complain_if_different('csv', FileTypes.csv.value)
+            elif file_path[-4:] == 'json':
+                # In this instance the symbols should be None
+                complain_if_different('json', FileTypes.json.value)
+            else:
+                raise LookupError(f"Unknown filetype for {file_path}")
+
+        return file_type
+
+    def _guess_resolutions(self):
+        for symbol in self._internal_dataset:
+            # Get the time diff
+            time_series: pd.Series = self._internal_dataset[symbol]['time']
+            time_dif = time_series.diff()
+
+            # Now find the most common difference and use that
+            if symbol not in self.prices_info:
+                self.prices_info[symbol] = {}
+
+            # Store the resolution start time and end time of each dataset
+            self.prices_info[symbol]['resolution'] = int(time_dif.value_counts().idxmax())
+            self.prices_info[symbol]['start_time'] = time_series.iloc[0]
+            self.prices_info[symbol]['stop_time'] = time_series.iloc[-1]
+
+    def __init__(self, file_path: [str, list], symbol: [str, list] = None):
+        """
+        Read in a new custom price dataset in either json or csv format
+
+        Args:
+            file_path (str or list): A single file path or list of filepaths pointing to a set of price data
+            symbol (str or list): Only required if using .csv files. These must match in index to the symbol that the
+             csv file path corresponds to. The CSV files also must have at least 2 rows of data in them.
+
+            symbol (str or list): Pass the symbol or symbols that the file paths correspond to. One file path and one
+             symbol can be passed as a non list but multiple can be passed as lists in both arguments. Just make sure
+             that the symbol indices match on both arguments
+        """
+        file_paths, symbols = self._convert_to_list(file_path, symbol)
+
+        # Empty dict to store the resolutions of the inputs by symbol
+        self.prices_info = {}
+
+        try:
+            assert(len(symbols) == len(set(symbols)))
+        except AssertionError:
+            raise AssertionError("Cannot use duplicate symbols for one price reader. Please use multiple price readers"
+                                 " to read in different datasets of the same symbol.")
+
+        data_type = self.__associate(file_paths)
+
+        super().__init__(data_type)
+
+        if data_type == FileTypes.json.value:
+            self._parse_json_prices(file_paths, ('open', 'high', 'low', 'close', 'volume', 'time'))
+        elif data_type == FileTypes.csv.value:
+            self._parse_csv_prices(file_paths, symbols, {'open', 'high', 'low', 'close', 'volume', 'time'})
+        else:
+            raise LookupError("No parsing written for input type.")
+
+        self._guess_resolutions()
+
+
+class EventReader(__DataReader):
+    def __parse_json_events(self, file_path):
+        contents = json.loads(open(file_path).read())
+
+        for event_type in contents:
+            self._write_dataset(contents[event_type], event_type, ('time', 'data'))
+
+    def __init__(self, file_path):
+        super().__init__(DataTypes.event_json)
+        try:
+            assert file_path[-4:] == 'json'
+        except AssertionError:
+            raise AssertionError(f"The filepath did not have a \'json\' ending - got: {file_path[-4:]}")
+
+        self.__parse_json_events(file_path)
+
+
+class TickReader(__FormatReader):
+    def __init__(self, file_path: [str, list], symbol: [str, list] = None):
+        super().__init__(DataTypes.tick_csv)
+        file_paths, symbols = self._convert_to_list(file_path, symbol)
+
+        try:
+            assert file_path[-3:] == 'csv'
+        except AssertionError:
+            raise AssertionError(f"The filepath did not have a \'csv\' ending - got: {file_path[-3:]}")
+        self._parse_csv_prices(file_paths, symbols, {'time', 'price'})
diff --git a/blankly/frameworks/strategy/recipes/__init__.py b/blankly/data/templates/__init__.py
similarity index 100%
rename from blankly/frameworks/strategy/recipes/__init__.py
rename to blankly/data/templates/__init__.py
diff --git a/examples/keyless_backtest.py b/blankly/data/templates/keyless.py
similarity index 93%
rename from examples/keyless_backtest.py
rename to blankly/data/templates/keyless.py
index 806b2543..cdbed15f 100644
--- a/examples/keyless_backtest.py
+++ b/blankly/data/templates/keyless.py
@@ -8,7 +8,7 @@ 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 = int(state.interface.cash / price)
         state.interface.market_order(symbol, side='buy', size=buy)
         state.variables['owns_position'] = True
     elif rsi[-1] > 70 and state.variables['owns_position']:
@@ -29,8 +29,8 @@ def init(symbol, state: blankly.StrategyState):
     # This downloads an example CSV
     data = requests.get('https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/price_examples.csv?'
                         'alt=media&token=3f3c37ee-a87b-46b0-b145-077c0e25254b').text
-    f = open('./price_examples.csv', 'w')
-    f.write(data)
+    with open('./price_examples.csv', 'w') as file:
+        file.write(data)
 
     # Run on the keyless exchange, starting at 100k
     exchange = blankly.KeylessExchange('./price_examples.csv', initial_account_values={'USD': 100000})
diff --git a/blankly/data/templates/none.py b/blankly/data/templates/none.py
new file mode 100644
index 00000000..92954c96
--- /dev/null
+++ b/blankly/data/templates/none.py
@@ -0,0 +1,10 @@
+import blankly
+
+if __name__ == "__main__":
+    exchange = blankly.EXCHANGE_CLASS()
+    strategy = blankly.Strategy(exchange)
+
+    if blankly.is_deployed:
+        strategy.start()
+    else:
+        strategy.backtest(to='1y', initial_values={'USD': 10000})
diff --git a/blankly/data/templates/none_screener.py b/blankly/data/templates/none_screener.py
new file mode 100644
index 00000000..d4bde883
--- /dev/null
+++ b/blankly/data/templates/none_screener.py
@@ -0,0 +1,16 @@
+from blankly import Screener, ScreenerState
+
+
+def evaluator(symbol, state: ScreenerState):
+    pass
+
+
+def formatter(results, state: ScreenerState):
+    pass
+
+
+if __name__ == '__main__':
+    exchange = EXCHANGE_CLASS()  # initialize our exchange
+    screener = Screener(exchange, evaluator, symbols=[], formatter=formatter)  # find oversold
+
+    print(screener.formatted_results)
diff --git a/blankly/data/templates/rsi_bot.py b/blankly/data/templates/rsi_bot.py
new file mode 100644
index 00000000..b51fd71b
--- /dev/null
+++ b/blankly/data/templates/rsi_bot.py
@@ -0,0 +1,47 @@
+import blankly
+
+
+def price_event(price, symbol, state: blankly.StrategyState):
+    """ This function will give an updated price every 15 seconds from our definition below """
+    state.variables.history.append(price)
+
+    rsi = blankly.indicators.rsi(state.variables.history)
+    current_position = state.interface.account[state.base_asset].available
+
+    if rsi[-1] < 30 and not current_position:
+        # Dollar cost average buy
+        buy = blankly.trunc(state.interface.cash / price, state.variables.precision)
+        state.interface.market_order(symbol, side='buy', size=buy)
+
+    elif rsi[-1] > 70 and current_position:
+        # Sell our position
+        state.interface.market_order(symbol, side='sell', size=current_position)
+
+
+def init(symbol, state: blankly.StrategyState):
+    # Download price data to give context to the algo
+    state.variables.history = state.interface.history(symbol, to=150, return_as='deque',
+                                                      resolution=state.resolution)['close']
+
+    # Get the max precision for this symbol from the API
+    increment = next(product['base_increment']
+                     for product in state.interface.get_products()
+                     if product['symbol'] == symbol)
+    state.variables.precision = blankly.utils.increment_to_precision(increment)
+
+
+if __name__ == "__main__":
+    # Authenticate EXCHANGE_NAME strategy
+    exchange = blankly.EXCHANGE_CLASS()
+
+    # Use our strategy helper on EXCHANGE_NAME
+    strategy = blankly.Strategy(exchange)
+
+    # Run the price event function every time we check for a new price - by default that is 15 seconds
+    strategy.add_price_event(price_event, symbol='SYMBOL', resolution='1d', init=init)
+
+    # Start our strategy, or run a backtest if this script is run locally.
+    if blankly.is_deployed:
+        strategy.start()
+    else:
+        strategy.backtest(to='1y', initial_values={'USD': 10000})
diff --git a/blankly/data/templates/rsi_screener.py b/blankly/data/templates/rsi_screener.py
new file mode 100644
index 00000000..ba0deefb
--- /dev/null
+++ b/blankly/data/templates/rsi_screener.py
@@ -0,0 +1,30 @@
+from blankly import Screener, Alpaca, ScreenerState
+from blankly.indicators import rsi
+
+tickers = SYMBOL_LIST  # any stocks that you may want
+
+
+# This function is our evaluator and runs per stock
+def is_stock_buy(symbol, state: ScreenerState):
+    # This runs per stock
+    prices = state.interface.history(symbol, 40, resolution='1d',
+                                     return_as='list')  # get past 40 data points
+    price = state.interface.get_price(symbol)
+    rsi_values = rsi(prices['close'], 14)
+    return {'is_oversold': bool(rsi_values[-1] < 30), 'price': price, 'symbol': symbol}
+
+
+def formatter(results, state: ScreenerState):
+    # 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__':
+    exchange = EXCHANGE_CLASS()  # initialize our exchange
+    screener = Screener(exchange, is_stock_buy, symbols=tickers, formatter=formatter)  # find oversold
+
+    print(screener.formatted_results)
diff --git a/blankly/deployment/api.py b/blankly/deployment/api.py
index 66928f6e..c9126f09 100644
--- a/blankly/deployment/api.py
+++ b/blankly/deployment/api.py
@@ -24,6 +24,9 @@
 blankly_deployment_url = 'https://deploy.blankly.finance'
 
 
+# blankly_deployment_url = 'http://localhost:8080'
+
+
 class API:
     def __init__(self, token, override_url: str = None):
         if override_url:
@@ -34,7 +37,10 @@ def __init__(self, token, override_url: str = None):
         self.token = None
         self.auth_data = self.exchange_token(token)
 
-        self.token = self.auth_data['idToken']
+        try:
+            self.token = self.auth_data['idToken']
+        except (KeyError, TypeError):
+            raise KeyError("Failed to authenticate - run \"blankly login\" again.")
         self.user_id = self.auth_data['data']['user_id']
 
     def __request(self, type_: str, route: str, json_: dict = None, params: dict = None, file=None, data: dict = None):
@@ -95,11 +101,11 @@ def exchange_token(self, token):
         """
         return self.__request('post', 'auth/token', data={'refreshToken': token})
 
-    def get_details(self, model_id: str):
+    def get_details(self, project_id: str, model_id: str):
         """
         Get the details route
         """
-        return self.__request('post', 'model/details', data={'modelId': model_id})
+        return self.__request('post', 'model/details', data={'projectId': project_id, 'modelId': model_id})
 
     def get_status(self):
         return self.__request('get', 'model/status')
@@ -118,33 +124,33 @@ 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, project_id, model_id: str, version_description: str,
+    def deploy(self, file_path: str, model_id: str, version_description: str,
                python_version: float, type_: str, plan: str, schedule: str = None):
         file_path = r'{}'.format(file_path)
         file = {'model': open(file_path, 'rb')}
         return self.__request('post', 'model/deploy', file=file, data={'pythonVersion': python_version,
                                                                        'versionDescription': version_description,
-                                                                       'projectId': project_id,
+                                                                       'projectId': self.user_id,
                                                                        'modelId': model_id,
                                                                        'type': type_,
                                                                        'plan': plan,
                                                                        'schedule': schedule})
 
-    def backtest_deployed(self, project_id: str, model_id: str, args: dict, version_id: str, backtest_description: str):
+    def backtest_deployed(self, model_id: str, args: dict, version_id: str, backtest_description: str):
         return self.__request('post', 'model/backtestUploadedModel',
-                              json_={'projectId': project_id,
+                              json_={'projectId': self.user_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,
+    def backtest(self, file_path: str, model_id: str, args: dict, plan: str,
                  type_: 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={'pythonVersion': str(python_version),
-                                    'projectId': project_id,
+                                    'projectId': self.user_id,
                                     'modelId': model_id,
                                     'type': type_,
                                     'backtestArgs': json.dumps(args),
@@ -153,11 +159,46 @@ def backtest(self, file_path: str, project_id: str, model_id: str, args: dict, p
                                     })
 
     def create_model(self, project_id: str, type_: str, name: str, description: str):
-        return self.__request('post', 'model/create-model',
-                              data={
-                                  'projectId': project_id,
-                                  'type': type_,
-                                  'name': name,
-                                  'description': description
-                              })
-
+        model = self.__request('post', 'model/create-model',
+                               data={
+                                   'projectId': project_id or self.user_id,
+                                   'type': type_,
+                                   'name': name,
+                                   'description': description
+                               })
+        model['id'] = model['modelId']
+        model['projectId'] = project_id or self.user_id
+        return model
+
+    def list_models(self, project_id: str):
+        models = self.__request('post', 'model/list',
+                                data={
+                                    'projectId': project_id or self.user_id
+                                })
+        for model in models:
+            model['modelId'] = model['id']
+            model['projectId'] = project_id or self.user_id
+        return models
+
+    def list_all_models(self):
+        models = self.list_models(self.user_id)
+        for team in self.list_teams():
+            # TODO nah
+            models += [{'team': team, **model}
+                       for model in self.list_models(team['id'])]
+        return models
+
+    def list_teams(self):
+        return self.__request('get', 'project/teams')
+
+    def generate_keys(self, project_id: str):
+        return self.__request('post', 'project/generate-project-token',
+                              data={'projectId': project_id})
+
+
+if __name__ == '__main__':
+    from blankly.deployment import new_cli as cli
+    import code
+
+    api = cli.ensure_login()
+    code.interact(local=dict(globals(), **locals()))  # drop to interactive
diff --git a/blankly/deployment/cli.py b/blankly/deployment/cli.py
index ca662bc6..a7167879 100644
--- a/blankly/deployment/cli.py
+++ b/blankly/deployment/cli.py
@@ -18,6 +18,7 @@
 
 import argparse
 import sys
+import traceback
 import warnings
 import os
 import platform
@@ -59,7 +60,7 @@ class TermColors:
 
 
 supported_exchanges = ['binance.com', 'binance.us',
-                       'coinbase_pro', 'alpaca', 'ftx', 'oanda']
+                       'coinbase_pro', 'alpaca', 'ftx', 'oanda', 'getting-started']
 
 
 def choose_option(choice: str, options: list, descriptions: list):
@@ -94,6 +95,9 @@ def print_descriptions(descriptions_: list, show_index: bool):
             if len(i) > largest_option_name:
                 largest_option_name = len(i)
 
+        oldterm = termios.tcgetattr(fd)
+        newattr = termios.tcgetattr(fd)
+
         def print_selection_index(index_):
             # We pass none when they haven't selected yet
             if index_ is None:
@@ -125,9 +129,6 @@ def print_selection_index(index_):
         # 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)
 
@@ -171,14 +172,18 @@ def print_selection_index(index_):
         index = int(input(TermColors.UNDERLINE + TermColors.OKCYAN +
                     "You have chosen:" + TermColors.ENDC + " "))
 
+        if index < 0 or index > len(options) - 1:
+            raise LookupError(f"The index you chose is out of bounds, choose an index between {0} and "
+                              f"{len(options) -1}")
+
         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, api: API):
-    if args['path'] is None:
+def get_project_model_and_name(args, api: API):
+    if 'path' not in args or args['path'] is None:
         print(TermColors.WARNING + "Warning - No filepath specified. Assuming the current directory (./)\n" +
               TermColors.ENDC)
 
@@ -187,57 +192,90 @@ def get_project_model_and_name(args, projects, api: API):
     try:
         f = open(os.path.join(args['path'], deployment_script_name))
         deployment_options = json.load(f)
-
         f.close()
+
+        # Use this variable to always write if something has changed
+        queue_write = False
+        """
+        This handles identifying the project on login
+        """
         if 'model_id' not in deployment_options or \
-                'project_id' not in deployment_options or \
                 'type' 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 = []
+            choice = choose_option('way to connect to a model', ['Create New', 'Choose from Existing'],
+                                   ['Generate a new model',
+                                    'Attach to an existing model'])
 
-            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)
+            if choice == 'Create New':
+                model_name = input(TermColors.BOLD + TermColors.WARNING +
+                                   "Enter a name for your model: " + TermColors.ENDC)
 
-            model_name = input(TermColors.BOLD + TermColors.WARNING +
-                               "Enter a name for your model: " + TermColors.ENDC)
+                # Now we know to go ahead and create a new model on the server
+                general_description = input(TermColors.BOLD + TermColors.WARNING +
+                                            "Enter a general description for this model model: " + TermColors.ENDC)
 
-            # Now we know to go ahead and create a new model on the server
-            general_description = input(TermColors.BOLD + TermColors.WARNING +
-                                        "Enter a general description for this model model: " + TermColors.ENDC)
+                type_ = choose_option('model type', ['strategy', 'screener'],
+                                      ["\t" + TermColors.BOLD + TermColors.WARNING +
+                                       'A strategy is a model that uses blankly.Strategy' + TermColors.ENDC,
+                                       "\t" + TermColors.BOLD + TermColors.WARNING +
+                                       'A screener is a model uses blankly.Screener' + TermColors.ENDC])
 
-            type_ = choose_option('model type', ['strategy', 'screener'],
-                                  ["\t" + TermColors.BOLD + TermColors.WARNING +
-                                   'A strategy is a model that uses blankly.Strategy' + TermColors.ENDC,
-                                   "\t" + TermColors.BOLD + TermColors.WARNING +
-                                   'A screener is a model uses blankly.Screener' + TermColors.ENDC])
-
-            model_id = api.create_model(project_id, type_, model_name, general_description)['modelId']
+                model_id = api.create_model(type_, model_name, general_description)['modelId']
+            else:
+                models = api.list_models()
+                ids = []
+                descriptions = []
+                for i in models:
+                    try:
+                        id_ = i['id']
+                        name_ = i['name']
+                        ids.append(id_)
+                        descriptions.append("\t" + TermColors.BOLD + TermColors.WARNING + id_ + ": " +
+                                            TermColors.ENDC + TermColors.OKCYAN + name_)
+                    except KeyError:
+                        pass
+                model_id = choose_option('model', ids, descriptions)
+
+                # Because we have the ID, we now need to get the index directly
+                index = None
+                for i in models:
+                    if i['id'] == model_id:
+                        index = i
+
+                if index is None:
+                    raise LookupError("An issue occurred please try again.")
+
+                type_ = index['type']
+                model_name = index['name']
 
             info_print(f"Created a new model in blankly.json with ID: {model_id}")
 
             deployment_options['type'] = type_
             deployment_options['model_id'] = model_id
-            deployment_options['project_id'] = project_id
             deployment_options['model_name'] = model_name
+        model_name = deployment_options['model_name']
+
+        """
+        This part generates API keys if they aren't found
+        """
+        if 'api_key' not in deployment_options or 'api_pass' not in deployment_options:
+            response = api.generate_keys()
+            deployment_options['api_key'] = response['apiKey']
+            deployment_options['api_pass'] = response['apiPass']
+            queue_write = True
 
+        if queue_write:
             # 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']
     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, deployment_options, python_version
+    return model_name, deployment_options, python_version
 
 
 temporary_zip_file = None
@@ -320,19 +358,23 @@ def create_and_write_file(filename: str, default_contents: str = None):
 login_parser = subparsers.add_parser('login', help='Log in to your blankly account.')
 login_parser.set_defaults(which='login')
 
+login_parser = subparsers.add_parser('logout', help='Log out of your blankly account.')
+login_parser.set_defaults(which='logout')
+
 deploy_parser = subparsers.add_parser('deploy', help='Command to upload & start the model.')
 deploy_parser.set_defaults(which='deploy')
 add_path_arg(deploy_parser, required=False)
 
-project_create_parser = subparsers.add_parser('create', help='Create a new project.')
-project_create_parser.set_defaults(which='create')
+# Old project tools
+# project_create_parser = subparsers.add_parser('create', help='Create a new project.')
+# project_create_parser.set_defaults(which='create')
+# list_parser = subparsers.add_parser('list', help='Show available projects & exit.')
+# list_parser.set_defaults(which='list')
 
 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')
 
 run_parser = subparsers.add_parser('run', help='Mimic the run mechanism used in blankly deployment.')
 run_parser.add_argument('--monitor',
@@ -344,52 +386,122 @@ def create_and_write_file(filename: str, default_contents: str = None):
 
 # Create a global token value for use in the double nested function below
 token = None
+tokenfile_path = None
 
 
-def login(remove_cache: bool = False):
-    # Set the token as global here
-    global token
-    from http.server import BaseHTTPRequestHandler, HTTPServer
-    import urllib.parse
-
+def __generate_tempfile():
+    """
+    Generate a temporary file with the blankly auth prefix and then get the directory / file name from it
+    """
     fd, path = tempfile.mkstemp(prefix='blankly_auth_')
     temp_folder = os.path.dirname(path)
     file_name = os.path.basename(path)
-    for i in os.listdir(temp_folder):
+
+    return fd, temp_folder, file_name
+
+
+def logout():
+    """
+    This function will log out if not logged in
+    """
+    global tokenfile_path
+    fd, temp_folder, file_name = __generate_tempfile()
+    os.close(fd)
+    os.remove(os.path.join(temp_folder, file_name))
+    for i_ in os.listdir(temp_folder):
         # Check to see if one exists at this location
-        if i[0:13] == 'blankly_auth_' and i != file_name:
-            # 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
-                #  'token' not in token_file
-                file_name = i
-                # Now just read the token from it
-                f = open(os.path.join(temp_folder, file_name))
-                try:
-                    token_file = json.load(f)
-
-                    if 'token' in token_file:
-                        # Exit cleanly here finding the old refresh token
-                        return token_file['token']
-                except json.decoder.JSONDecodeError:
-                    # If it fails then don't return anything
-                    #  just continue with re-logging in
-                    pass
-            # If we are removing cache then these files should just be deleted
-            else:
-                print(os.path.join(temp_folder, i))
-                os.remove(os.path.join(temp_folder, i))
-            # Be sure to leave the loop
-            break
+        if i_[0:13] == 'blankly_auth_' and i_ != file_name:
+            # Kill the file we created
+            os.remove(os.path.join(temp_folder, i_))
+
+
+def is_logged_in():
+    """
+    This function will return if the user is logged in
+
+    logged_in = __is_logged_in(temp_folder) is not None
+    """
+    global tokenfile_path
+    fd, temp_folder, file_name = __generate_tempfile()
+    for i_ in os.listdir(temp_folder):
+        # Check to see if one exists at this location
+        if i_[0:13] == 'blankly_auth_' and i_ != file_name:
+            # Kill the file we created
+            os.close(fd)
+            os.remove(os.path.join(temp_folder, file_name))
+            
+            try:
+                json.loads(open(temp_folder + '/' + i_).read())['token']
+            except (KeyError, json.decoder.JSONDecodeError):
+                return False
 
-    def set_token(new_value):
-        # Set the token as global here as well
+            # Cache the filepath globally
+            tokenfile_path = os.path.join(temp_folder, i_)
+            return True
+
+    # Kill the file we created
+    os.close(fd)
+    os.remove(os.path.join(temp_folder, file_name))
+    return False
+
+
+def login(remove_cache: bool = False):
+    # Set the token as global here
+    global token
+    # Also get the global path
+    global tokenfile_path
+
+    def load_token(token_path_: str) -> str:
+        """
+        If the file has the token then this function is a success
+        """
+        global tokenfile_path
         global token
-        token = new_value
+        f_ = open(token_path_)
+        token_file_ = json.load(f_)
+
+        if 'token' in token_file_:
+            # Globally cache the path
+            tokenfile_path = token_path_
+            # Globally cache the token
+            token = token_file_['token']
+
+            # Exit cleanly here finding the old refresh token
+            return token_file_['token']
+
+    # This can be skipped if is_logged_in is run which will find the temp folder and file name if necessary
+    if tokenfile_path is None:
+        fd, temp_folder, file_name = __generate_tempfile()
+        for i_ in os.listdir(temp_folder):
+            # Check to see if one exists at this location
+            if i_[0:13] == 'blankly_auth_' and i_ != file_name:
+                # 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
+                    #  'token' not in token_file
+                    file_name = i_
+                    # Now just read the token from it
+                    token_path = os.path.join(temp_folder, file_name)
+                    try:
+                        return load_token(token_path)
+                    except json.decoder.JSONDecodeError:
+                        # If it fails then don't return anything
+                        #  just continue with re-logging in
+                        pass
+                # If we are removing cache then these files should just be deleted
+                else:
+                    os.remove(os.path.join(temp_folder, i_))
+                # Be sure to leave the loop
+                break
+    elif isinstance(tokenfile_path, str):
+        return load_token(tokenfile_path)
+
+    from http.server import BaseHTTPRequestHandler, HTTPServer
+    import urllib.parse
 
     class Handler(BaseHTTPRequestHandler):
         token: str
@@ -405,6 +517,7 @@ def do_OPTIONS(self):
             self.end_headers()
 
         def do_GET(self):
+            global token
             # Parse the URL
             args = urllib.parse.parse_qs(self.path[2:])
             self.send_response(200)
@@ -416,7 +529,7 @@ def do_GET(self):
                 # Perform a GET request to pull down a successful response
                 file = requests.get('https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/'
                                     'login_success.html?alt=media&token=41d734e2-0a88-44c4-b1dd-7e081fd019e7')
-                set_token(args['token'][0])
+                token = args['token'][0]
 
                 info_print("Login success - You can close your browser.")
             else:
@@ -494,16 +607,9 @@ def main():
 
         api = API(token_)
 
-        projects = api.list_projects()
-
-        if len(projects) == 0:
-            print(TermColors.FAIL +
-                  "Please create a project with 'blankly create' first." + TermColors.ENDC)
-            return
-
         # Read and write to the deployment options if necessary
-        model_name, project_id, deployment_options, python_version = \
-            get_project_model_and_name(args, projects, api)
+        model_name, deployment_options, python_version = \
+            get_project_model_and_name(args, api)
 
         info_print("Zipping...")
 
@@ -517,7 +623,6 @@ def main():
         info_print("Uploading...")
         kwargs = {
             'file_path': model_path,
-            'project_id': project_id,
             'model_id': deployment_options['model_id'],
             'version_description': version_description,
             'python_version': python_version,
@@ -535,7 +640,9 @@ def main():
             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']}")
+            url = f"https://app.blankly.finance/{api.user_id}/{response['modelId']}/overview"
+            webbrowser.open(url)
+            info_print(f"\tView your model here: {url}")
 
     elif which == 'init':
         exchange = args['exchange']
@@ -558,12 +665,6 @@ def main():
                 f'\nYour input: {TermColors.BOLD}{user_defined_exchange}{TermColors.ENDC}')
             return
 
-        # Directly download keys.json
-        print("Downloading keys template...")
-        keys_example = requests.get('https://raw.githubusercontent.com/Blankly-Finance/'
-                                    'Blankly/main/examples/keys_example.json')
-        create_and_write_file('keys.json', keys_example.text)
-
         tld = 'com'
         if user_defined_exchange[0:7] == 'binance':
             # Find if it's .us if needed
@@ -614,40 +715,57 @@ def main():
         print(f"{TermColors.OKCYAN}{TermColors.BOLD}Found python version: "
               f"{py_version[0]}.{py_version[1]}{TermColors.ENDC}")
 
+        # Write in a blank requirements file
+        print("Writing requirements.txt defaults...")
+        create_and_write_file('requirements.txt', 'blankly')
+
         deploy = load_deployment_settings()
 
         deploy['python_version'] = py_version[0] + "." + py_version[1]
         create_and_write_file(deployment_script_name, json.dumps(deploy, indent=2))
 
-        # Write in a blank requirements file
-        print("Writing requirements.txt defaults...")
-        create_and_write_file('requirements.txt', 'blankly')
+        try:
+            if is_logged_in():
+                # We know we're logged in so make sure that we also get a project id and a model id
+                print(f'{TermColors.WARNING}Automatically logged in!{TermColors.ENDC}')
+                api = API(login())
+                print(f'{TermColors.WARNING}Attaching this to a platform model...{TermColors.ENDC}')
+                get_project_model_and_name(args, api)
+            else:
+                print(f'{TermColors.WARNING}Run \"blankly login\" and then \"blankly init\" again to get better '
+                      f'backtest '
+                      f'viewing.{TermColors.ENDC}')
+        except Exception:
+            traceback.print_exc()
+            # Wipe the blankly.json to avoid confusion
+            os.remove('./' + deployment_script_name)
 
         print(f"{TermColors.OKGREEN}{TermColors.UNDERLINE}Success!{TermColors.ENDC}")
 
     elif which == 'login':
         login(remove_cache=True)
 
-    elif which == 'list':
-        api = API(login())
-        projects = api.list_projects()
-        if len(projects) > 0:
-            print(TermColors.BOLD + TermColors.WARNING + "Projects: " + TermColors.ENDC)
-            for i in projects:
-                print("\t" + TermColors.BOLD + TermColors.WARNING + i['projectId'] + ": " +
-                      TermColors.ENDC + TermColors.OKCYAN + i['name'])
-                print(f"\t\t Description: {i['description']}")
-        else:
-            info_print("No projects found.")
+    elif which == 'logout':
+        logout()
+        info_print("Cleared all blankly credentials.")
+
+    # elif which == 'list':
+    #     api = API(login())
+    #     models = api.list_models()
+    #     if len(models) > 0:
+    #         print(TermColors.BOLD + TermColors.WARNING + "Models: " + TermColors.ENDC)
+    #         for i in models:
+    #             print("\t" + TermColors.BOLD + TermColors.WARNING + i['id'] + ": " +
+    #                   TermColors.ENDC + TermColors.OKCYAN + i['name'])
+    #             print(f"\t\t Description: {i['description']}")
+    #     else:
+    #         info_print("No projects found.")
 
     elif which == 'backtest':
         api = API(login())
 
-        projects = api.list_projects()
-
         # Read and write to the deployment options if necessary
-        model_name, project_id, deployment_options, python_version = \
-            get_project_model_and_name(args, projects, api)
+        model_name, deployment_options, python_version = get_project_model_and_name(args, api)
 
         if deployment_options['type'] == 'screener':
             raise AttributeError("Screeners are not backtestable.")
@@ -663,7 +781,6 @@ def main():
         info_print("Uploading...")
 
         response = api.backtest(file_path=model_path,
-                                project_id=project_id,
                                 model_id=deployment_options['model_id'],
                                 args=deployment_options['backtest_args'],
                                 plan=chosen_plan,
@@ -679,25 +796,23 @@ def main():
             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())
-
-        project_name = input(TermColors.BOLD + "Input a name for your project: " + TermColors.ENDC)
-
-        project_description = input(TermColors.BOLD + "Input a description for your project: " + TermColors.ENDC)
-
-        input(TermColors.BOLD + TermColors.FAIL + "Press enter to confirm project creation." + TermColors.ENDC)
-
-        response = api.create_project(project_name, description=project_description)
-
-        if 'error' in response:
-            info_print('Error: ' + response['error'])
-        elif 'status' in response and response['status'] == 'success':
-            info_print(f"Created {response['name']} at {response['createdAt']}:")
-            info_print(f"\tProject Id:\t{response['projectId']}")
-            info_print(f"\tStatus:\t{response['status']}")
+    # elif which == 'create':
+    #     api = API(login())
+    #
+    #     project_name = input(TermColors.BOLD + "Input a name for your model: " + TermColors.ENDC)
+    #
+    #     project_description = input(TermColors.BOLD + "Input a description for your model: " + TermColors.ENDC)
+    #
+    #     input(TermColors.BOLD + TermColors.FAIL + "Press enter to confirm model creation." + TermColors.ENDC)
+    #
+    #     response = api.create_model(type_='strategy', name=project_name, description=project_description)
+    #
+    #     if 'error' in response:
+    #         info_print('Error: ' + response['error'])
+    #     elif 'status' in response and response['status'] == 'success':
+    #         info_print(f"Created {response['name']} at {response['createdAt']}:")
+    #         info_print(f"\tModel Id:\t{response['id']}")
 
     elif which == 'run':
         if args['path'] is None:
diff --git a/blankly/deployment/deploy.py b/blankly/deployment/deploy.py
new file mode 100644
index 00000000..5f117067
--- /dev/null
+++ b/blankly/deployment/deploy.py
@@ -0,0 +1,56 @@
+import os
+import tempfile
+import sys
+import zipfile
+from typing import List
+
+
+def get_python_version():
+    return '.'.join(str(i) for i in sys.version_info[:2])
+
+
+def zip_dir(path, ignore_files):
+    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(path)
+
+    model_path = os.path.join(dist_directory, 'model.zip')
+    zip_ = zipfile.ZipFile(model_path, 'w', zipfile.ZIP_DEFLATED)
+    zipdir(source, zip_, ignore_files)
+    zip_.close()
+
+    return model_path
+
+
+def zipdir(path, ziph, ignore_files: list):
+    # From https://stackoverflow.com/a/1855118/8087739
+    # Notice we're using this instead of shutil because it allows customization such as passwords and skipping
+    # directories
+    # ziph is zipfile handle
+
+    filtered_ignore_files = []
+    # Set all the ignored files to be absolute
+    for i in range(len(ignore_files)):
+        # 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 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/
+                relpath = os.path.relpath(filepath,
+                                          os.path.join(path, '..'))
+                relpath = os.path.normpath(relpath).split(os.sep)
+                relpath[0] = os.sep + 'model'
+                relpath = os.path.join(*relpath)
+
+                ziph.write(filepath, relpath)
diff --git a/blankly/deployment/exchange_data.py b/blankly/deployment/exchange_data.py
new file mode 100644
index 00000000..17f52ddf
--- /dev/null
+++ b/blankly/deployment/exchange_data.py
@@ -0,0 +1,99 @@
+"""
+    Exchange metadata for use in the CLI
+    Copyright (C) 2022 Matias Kotlik
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+
+from typing import Dict, List
+import alpaca_trade_api
+from binance.client import Client as BinanceClient
+from questionary import Choice
+
+from blankly.deployment.ui import print_failure
+from blankly.exchanges.interfaces.alpaca import alpaca_api
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_api import API as CoinbaseProAPI
+from blankly.exchanges.interfaces.ftx.ftx_api import FTXAPI
+from blankly.exchanges.interfaces.oanda.oanda_api import OandaAPI
+
+
+class Exchange:
+    name: str
+    symbols: List[str]
+    python_class: str
+    tlds: List[str]
+    display_name: str
+    key_info: Dict[str, str]
+
+    def __init__(self, name: str, symbols: List[str], test_func, key_info: List[str] = None, python_class: str = None,
+                 tlds: List[str] = None, display_name: str = None, currency: str = 'USD'):
+        self.name = name
+        self.symbols = symbols
+        self.test_func = test_func
+        self.key_info = {k.replace('_', ' ').title().replace('Api', 'API'): k  # autogen key instructions
+                         for k in key_info or ['API_KEY', 'API_SECRET']}  # default to just key/secret
+        self.python_class = python_class or name.replace('_', ' ').title().replace(' ', '')  # snake case to pascalcase
+        self.tlds = tlds or []
+        self.display_name = display_name or name.replace('_', ' ').title()  # prettify
+        self.currency = currency
+
+
+def kucoin_test_func(auth, tld):
+    try:
+        from kucoin import client as KucoinAPI
+    except ImportError:
+        print()  # we are running loading bar at this point, go next line to avoid making a mess
+        print_failure('kucoin-python must be installed to check Kucoin API Keys')
+        print_failure('Skipping check')
+    else:
+        return KucoinAPI.User(auth['API_KEY'], auth['API_SECRET'], auth['API_PASS'], auth['sandbox']).get_base_fee
+
+
+EXCHANGES = [
+    Exchange('alpaca', ['MSFT', 'GME', 'AAPL'],
+             lambda auth, tld: alpaca_trade_api.REST(key_id=auth['API_KEY'],
+                                                     secret_key=auth['API_SECRET'],
+                                                     base_url=(alpaca_api.live_url,
+                                                               alpaca_api.paper_url)[auth['sandbox']]
+                                                     ).get_account()),
+    Exchange('binance', ['BTC-USDT', 'ETH-USDT', 'SOL-USDT'],
+             lambda auth, tld: BinanceClient(api_key=auth['API_KEY'], api_secret=auth['API_SECRET'],
+                                             tld=tld, testnet=auth['sandbox']).get_account(),
+             tlds=['com', 'us'], currency='USDT'),
+    Exchange('coinbase_pro', ['BTC-USD', 'ETH-USD', 'SOL-USD'],
+             lambda auth, tld: CoinbaseProAPI(api_key=auth['API_KEY'], api_secret=auth['API_SECRET'],
+                                              api_pass=auth['API_PASS'],
+                                              API_URL=('https://api.pro.coinbase.com/',
+                                                       'https://api-public.sandbox.pro.coinbase.com/')[auth['sandbox']]
+                                              ).get_accounts(),
+             key_info=['API_KEY', 'API_SECRET', 'API_PASS']),
+    Exchange('ftx', ['BTC-USD', 'ETH-USD', 'SOL-USD'],
+             lambda auth, tld: FTXAPI(auth['API_KEY'], auth['API_SECRET'], tld).get_account_info(),
+             tlds=['com', 'us'], python_class='FTX', display_name='FTX'),
+    Exchange('oanda', ['BTC-USD', 'ETH-USD', 'SOL-USD'],
+             lambda auth, tld: OandaAPI(personal_access_token=auth['PERSONAL_ACCESS_TOKEN'],
+                                        account_id=auth['ACCOUNT_ID'], sandbox=auth['sandbox']).get_account(),
+             key_info=['ACCOUNT_ID', 'PERSONAL_ACCESS_TOKEN']),
+    Exchange('kucoin', ['BTC-USDT', 'ETH-USDT', 'SOL-USDT'], kucoin_test_func,
+             key_info=['API_KEY', 'API_SECRET', 'API_PASS'], currency='USDT'),
+]
+
+EXCHANGE_CHOICES_NO_KEYLESS = [Choice(exchange.display_name, exchange)
+                               for exchange in EXCHANGES]
+EXCHANGE_CHOICES = EXCHANGE_CHOICES_NO_KEYLESS[:]
+EXCHANGE_CHOICES.append(Choice('Keyless/No Exchange', False))
+
+
+def exc_display_name(name):
+    return next((exchange.display_name for exchange in EXCHANGES if name == exchange.name), name)
diff --git a/blankly/deployment/hello.cpp b/blankly/deployment/hello.cpp
deleted file mode 100644
index be6edc7b..00000000
--- a/blankly/deployment/hello.cpp
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
-Currently disabled potential extension module
- */
-
-//#include <iostream>
-//#include <Python/Python.h>
-//
-//#if PY_MAJOR_VERSION >= 3
-//#define PY3K
-//#endif
-//
-//// module functions
-//static PyObject* message(PyObject *self, PyObject *args)
-//{
-//    char *fromPython, result[1024];
-//    if(!PyArg_ParseTuple(args, "s", &fromPython))
-//    {
-//        return NULL;
-//    }
-//    else
-//    {
-//        strcpy(result, "Hello, ");
-//        strcat(result, fromPython);
-//        return Py_BuildValue("s", result);
-//    }
-//}
-//
-//// registration table
-//static PyMethodDef hello_methods[]={
-//        {"message", message, METH_VARARGS, "func doc"},
-//        {NULL, NULL, 0, NULL}
-//};
-//
-//
-//// module definition structure for python3
-//static struct PyModuleDef hellomodule = {
-//    PyModuleDef_HEAD_INIT,
-//    "hello",
-//    "mod doc",
-//    -1,
-//    hello_methods
-//};
-//// module initializer for python3
-//PyMODINIT_FUNC PyInit_hello()
-//{
-//    return PyModule_Create(&hellomodule);
-//}
diff --git a/blankly/deployment/keys.py b/blankly/deployment/keys.py
new file mode 100644
index 00000000..be6eb788
--- /dev/null
+++ b/blankly/deployment/keys.py
@@ -0,0 +1,78 @@
+"""
+    Functions for managing Exchange API Keys
+    Copyright (C) 2022 Matias Kotlik
+
+    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 alpaca_trade_api
+from binance.client import Client as BinanceClient
+
+from blankly.deployment.exchange_data import Exchange
+from blankly.deployment.ui import print_failure, show_spinner
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_api import API as CoinbaseProAPI
+from blankly.exchanges.interfaces.ftx.ftx_api import FTXAPI
+from blankly.exchanges.interfaces.oanda.oanda_api import OandaAPI
+
+from blankly.deployment.ui import confirm
+
+
+def load_keys():
+    try:
+        with open('keys.json', 'r') as file:
+            return json.load(file)
+    except FileNotFoundError:
+        return {}
+
+
+def write_keys(data):
+    with open('keys.json', 'w') as file:
+        json.dump(data, file, indent=4)
+
+
+def add_key(exchange: Exchange, tld: str, key_name: str, data: dict):
+    # load old keys file
+    saved_data = load_keys()
+
+    keys = saved_data.setdefault(exchange.name, {})
+
+    # change name until it's not already in our file
+    if not key_name:
+        key_name = 'example-portfolio'
+    if key_name in keys:
+        key_name = key_name + '-1'
+    while key_name in keys:
+        base, num = key_name.rsplit('-', 1)
+        key_name = f'{base}-{int(num) + 1}'
+
+    keys[key_name] = data  # this writes to saved_data
+
+    key_is_valid = check_key(exchange, tld, data)
+    if key_is_valid or confirm('Would you like to save this key anyway?', default=False).unsafe_ask():
+        write_keys(saved_data)
+        return True
+    return False
+
+
+def check_key(exchange: Exchange, tld: str, auth):
+    with show_spinner(f'Checking {exchange.display_name} API Key') as spinner:
+        try:
+            exchange.test_func(auth, tld)
+        except Exception:
+            spinner.fail(f'Failed to connect to {exchange.display_name}. Check that your keys are valid.')
+            return False
+        spinner.ok(f'Checked {exchange.display_name} API Key')
+        return True
diff --git a/blankly/deployment/login.py b/blankly/deployment/login.py
new file mode 100644
index 00000000..7ca1031c
--- /dev/null
+++ b/blankly/deployment/login.py
@@ -0,0 +1,108 @@
+"""
+    Functions for blankly login/logout commands
+    Copyright (C) 2022 Matias Kotlik
+
+    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 pathlib
+import sys
+import urllib.parse
+from http.server import BaseHTTPRequestHandler, HTTPServer
+from typing import Optional
+
+from blankly.deployment.api import API
+from blankly.deployment.ui import print_work, print_failure, print_success, show_spinner
+
+SUCCESS_URL = 'https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/login_success.html?alt=media&token=41d734e2-0a88-44c4-b1dd-7e081fd019e7'
+FAILURE_URL = 'https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/login_failure.html?alt=media&token=b2d9f6dc-aa54-4da2-aa6f-1f75a4025634'
+
+
+
+def write_token(token):
+    token_file = get_token_file()
+    token_file.parent.mkdir(parents=True, exist_ok=True)
+    with open(token_file, 'w') as file:
+        json.dump({'token': token}, file)
+
+
+def poll_login() -> Optional[API]:
+    # mutate a list to avoid needing global variable shit and piss
+    token = []
+
+    class TokenListener(BaseHTTPRequestHandler):
+        def do_GET(self):
+            try:
+                token.append(urllib.parse.parse_qs(self.path[2:])['token'][0])
+                self.send_response_only(302)
+                self.send_header('Location', SUCCESS_URL)
+                self.end_headers()
+            except Exception:
+                token.append(False)
+                self.send_response(302)
+                self.send_header('Location', FAILURE_URL)  # send them back lmao
+                self.end_headers()
+
+    server = HTTPServer(('', 9082), TokenListener)
+
+    while not token:
+        server.handle_request()
+    token = token[0]
+
+    if token:
+        api = API(token)
+        write_token(token)
+        return api
+
+
+
+
+def get_token() -> Optional[str]:
+    token_file = get_token_file()
+    if token_file.exists():
+        # file exists, parse it or error out
+        with open(token_file, 'r') as file:
+            data = json.load(file)
+        return data['token']
+    return None
+
+
+def get_token_file() -> pathlib.Path:
+    return get_datadir() / 'blankly' / 'auth.json'
+
+
+# https://stackoverflow.com/a/61901696
+def get_datadir() -> pathlib.Path:
+    """
+    Returns a parent directory path where persistent application data can be stored.
+
+    # linux: ~/.local/share
+    # macOS: ~/Library/Application Support
+    # windows: C:/Users/<USER>/AppData/Roaming
+    """
+
+    home = pathlib.Path.home()
+
+    if sys.platform == "win32":
+        return home / "AppData/Roaming"
+    elif sys.platform == "darwin":
+        return home / "Library/Application Support"
+    elif sys.platform.startswith("linux"):
+        return home / ".local/share"
+    return home
+
+
+def logout():
+    get_token_file().unlink(missing_ok=True)
diff --git a/blankly/deployment/new_cli.py b/blankly/deployment/new_cli.py
new file mode 100644
index 00000000..47ccdd3d
--- /dev/null
+++ b/blankly/deployment/new_cli.py
@@ -0,0 +1,464 @@
+"""
+    Updated CLI for interacting with and uploading blankly models.
+    Copyright (C) 2022 Matias Kotlik
+
+    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 argparse
+import json
+import os.path
+import sys
+import traceback
+import webbrowser
+from pathlib import Path
+from typing import Optional
+import pkgutil
+
+from questionary import Choice
+
+from blankly.deployment.api import API, blankly_deployment_url
+from blankly.deployment.deploy import zip_dir, get_python_version
+from blankly.deployment.keys import add_key, load_keys, write_keys
+from blankly.deployment.login import logout, poll_login, get_token
+from blankly.deployment.ui import text, confirm, print_work, print_failure, print_success, select, show_spinner
+from blankly.deployment.exchange_data import EXCHANGES, Exchange, EXCHANGE_CHOICES, exc_display_name, \
+    EXCHANGE_CHOICES_NO_KEYLESS
+
+TEMPLATES = {'strategy': {'none': 'none.py',
+                          'rsi_bot': 'rsi_bot.py'},
+             'screener': {'none': 'none_screener.py',
+                          'rsi_screener': 'rsi_screener.py'}}
+
+AUTH_URL = 'https://app.blankly.finance/auth/signin?redirectUrl=/deploy'
+
+
+def validate_non_empty(text):
+    if not text.strip():
+        return 'Please enter a value'
+    return True
+
+
+def create_model(api, name, description, model_type, project_id=None):
+    with show_spinner('Creating model') as spinner:
+        try:
+            model = api.create_model(project_id or api.user_id, model_type, name, description)
+        except Exception:
+            spinner.fail('Failed to create model')
+            raise
+        spinner.ok('Created model')
+    return model
+
+
+def ensure_login() -> API:
+    # TODO print selected team ?
+    api = is_logged_in()
+    if api:
+        return api
+    return launch_login_flow()
+
+
+def is_logged_in() -> Optional[API]:
+    token = get_token()
+    if not token:
+        return
+
+    # log into deployment api
+    try:
+        return API(token)
+    except Exception:  # TODO
+        return
+
+
+def launch_login_flow() -> API:
+    try:
+        webbrowser.open_new(AUTH_URL)
+        print_work(f'Your browser was opened to {AUTH_URL}. Open the window and login.')
+    except Exception:
+        print_work(f'Could not find a browser to open. Navigate to {AUTH_URL} and login')
+
+    api = None
+    with show_spinner(f'Waiting for login') as spinner:
+        try:
+            api = poll_login()
+        except Exception:
+            pass  # we just check for api being valid, poll_login can return None
+
+        if not api:
+            spinner.fail('Failed to login')
+            sys.exit(1)
+
+        spinner.ok('Logged in')
+    return api
+
+
+def add_key_interactive(exchange: Exchange):
+    tld = None
+    if exchange.tlds:
+        tld = select('What TLD are you using?',
+                     [Choice(f'{exchange.name}.{tld}', tld) for tld in exchange.tlds]).unsafe_ask()
+
+    name = text('Give this key a name:', instruction='(Optional)').unsafe_ask()
+
+    saved_data = {}
+    for instruction, key in exchange.key_info.items():
+        saved_data[key] = text(f'{instruction}:', validate=validate_non_empty).unsafe_ask().strip()
+    saved_data['sandbox'] = confirm('Is this testnet/sandbox key?', default=False).unsafe_ask()
+
+    if add_key(exchange, tld, name, saved_data):
+        print_success(f'Your API key for {exchange.display_name} was added to this model')
+        return tld
+    print_failure(f'Your API Key for {exchange.display_name} was not added to the model')
+    return False
+
+
+def blankly_init(args):
+    # warn nonempty dir
+    dir_is_empty = len([f for f in os.listdir() if not f.startswith('.')]) == 0
+    if not dir_is_empty \
+            and not confirm('This directory is not empty. Would you like to continue anyways?').unsafe_ask():
+        return
+
+    exchange = select('What exchange would you like to connect to?', EXCHANGE_CHOICES).unsafe_ask()
+
+    model_type = 'strategy'
+    tld = None
+    template = None
+    if exchange:
+        model_type = select('What type of model do you want to create?',
+                            [Choice(mt.title(), mt) for mt in TEMPLATES]).unsafe_ask()
+        templates = TEMPLATES[model_type]
+
+        template_name = select('What template would you like to use for your new model?', templates).unsafe_ask()
+        template = templates[template_name]
+
+        if confirm('Would you like to add keys for this exchange?\n'
+                   'You can do this later at any time by running `blankly key add`').unsafe_ask():
+            tld = add_key_interactive(exchange)
+
+    model = None
+    if args.prompt_login and confirm('Would you like to connect this model to the Blankly Platform?').unsafe_ask():
+        api = ensure_login()
+        model = get_model_interactive(api, model_type)
+
+    with show_spinner('Generating files') as spinner:
+        files = [
+            ('bot.py', generate_bot_py(exchange, template), False),
+            ('backtest.json', generate_backtest_json(exchange), False),
+            ('requirements.txt', 'blankly\n', False),
+            ('keys.json', generate_keys_json(), True),
+            ('blankly.json', generate_blankly_json(model, model_type), False),
+            ('settings.json', generate_settings_json(tld or 'com'), False)
+        ]
+        spinner.ok('Generated files')
+
+    for path, data, force_overwrite in files:
+        exists = Path(path).exists()
+        overwrite_prompt = confirm(f'{path} already exists, would you like to overwrite it?', default=False)
+        if force_overwrite or not exists or overwrite_prompt.unsafe_ask():
+            with open(path, 'w') as file:
+                file.write(data)
+
+    # TODO open on platform WITHOUT STARTING
+
+    print_success('Done! Your model was created. Run `python bot.py` to run a backtest and get started.')
+
+
+def get_model_interactive(api, model_type):
+    create = select('Would you like to create a new model or attach to an existing one?',
+                    [Choice('Create new model', True), Choice('Attach to existing model', False)]).unsafe_ask()
+    if create:
+        default_name = Path.cwd().name  # default name is working dir name
+        name = text('Model name?', default=default_name, validate=validate_non_empty).unsafe_ask()
+        description = text('Model description?', instruction='(Optional)').unsafe_ask()
+        teams = api.list_teams()
+        team_id = None
+        if teams:
+            team_choices = [Choice('Create on my personal account', False)] \
+                           + [Choice(team.get('name', team['id']), team['id']) for team in teams]
+            team_id = select('What team would you like to create this model under?', team_choices).unsafe_ask()
+            return create_model(api, name, description, model_type, team_id or None)
+
+    with show_spinner('Loading models...') as spinner:
+        models = api.list_all_models()
+        spinner.ok('Loaded')
+
+    model = select('Select an existing model to attach to:',
+                   [Choice(get_model_repr(model), model) for model in models]).unsafe_ask()
+    if model.get('type', None) != model_type:
+        pass  # TODO
+    return model
+
+
+def get_model_repr(model: dict) -> str:
+    name = model.get('name', model['id'])
+    team = model.get('team', {}).get('name', None)
+    if team:
+        name = team + ' - ' + name
+    return name
+
+
+def generate_settings_json(tld: str):
+    data = {"settings": {"use_sandbox_websockets": False,
+                         "websocket_buffer_size": 10000,
+                         "test_connectivity_on_auth": True,
+                         "coinbase_pro": {"cash": "USD"},
+                         "binance": {"cash": "USDT",
+                                     "binance_tld": tld, },
+                         "alpaca": {"websocket_stream": "iex",
+                                    "cash": "USD",
+                                    "enable_shorting": True,
+                                    "use_yfinance": False},
+                         "oanda": {"cash": "USD"},
+                         "keyless": {"cash": "USD"},
+                         "ftx": {"cash": "USD",
+                                 "ftx_tld": tld, },
+                         "ftx_futures": {"cash": "USD",
+                                         "ftx_tld": tld},
+                         "kucoin": {"cash": "USDT"}}}
+    return json.dumps(data, indent=4)
+
+
+def generate_keys_json():
+    try:
+        with open('keys.json') as file:
+            keys = json.load(file)
+    except FileNotFoundError:
+        keys = {}
+
+    for exchange in EXCHANGES:
+        keys[exchange.name] = keys.get(exchange.name, {
+            'example-portfolio': {v: '*' * 20 for v in exchange.key_info.values()}
+        })
+        for portfolio in keys[exchange.name].values():
+            portfolio['sandbox'] = portfolio.get('sandbox', False)
+    return json.dumps(keys, indent=4)
+
+
+def generate_blankly_json(model: Optional[dict], model_type):
+    data = {'main_script': './bot.py',
+            'python_version': get_python_version(),
+            'requirements': './requirements.txt',
+            'working_directory': '.',
+            'ignore_files': ['price_caches', '.git', '.idea', '.vscode'],
+            'backtest_args': {'to': '1y'},
+            'type': model_type,
+            'screener': {'schedule': '30 14 * * 1-5'}}
+    if model:
+        data['model_id'] = model['id']
+        data['project_id'] = model['projectId']
+    return json.dumps(data, indent=4)
+
+
+def generate_backtest_json(exchange: Optional[Exchange]) -> str:
+    currency = exchange.currency if exchange else 'USD'
+    data = {'price_data': {'assets': []},
+            'settings': {'use_price': 'close',
+                         'smooth_prices': False,
+                         'GUI_output': True,
+                         'show_tickers_with_zero_delta': False,
+                         'save_initial_account_value': True,
+                         'show_progress_during_backtest': True,
+                         'cache_location': './price_caches',
+                         'continuous_caching': True,
+                         'resample_account_value_for_metrics': '1d',
+                         'quote_account_value_in': currency,
+                         'ignore_user_exceptions': True,
+                         'risk_free_return_rate': 0.0,
+                         'benchmark_symbol': None}}
+    return json.dumps(data, indent=4)
+
+
+def generate_bot_py(exchange: Optional[Exchange], template: str) -> str:
+    if not exchange:
+        # template should be None
+        return pkgutil.get_data('blankly', 'data/templates/keyless.py').decode('utf-8')
+    bot_py = pkgutil.get_data('blankly', f'data/templates/{template}').decode('utf-8')
+    for pattern, replacement in [
+        ('EXCHANGE_NAME', exchange.display_name,),
+        ('EXCHANGE_CLASS', exchange.python_class,),
+        ('SYMBOL_LIST', "['" + "', '".join(exchange.symbols) + "']",),
+        ('SYMBOL', exchange.symbols[0],)
+    ]:
+        bot_py = bot_py.replace(pattern, replacement)
+    return bot_py
+
+
+def blankly_login(args):
+    if is_logged_in():
+        print_success('You are already logged in')
+        return
+
+    launch_login_flow()
+
+
+def blankly_logout(args):
+    with show_spinner('Logging out of the Blankly Platform') as spinner:
+        try:
+            logout()
+        except Exception:
+            spinner.fail('Failed to logout')
+            raise
+        spinner.ok('Logged out')
+
+
+def ensure_model(api: API):
+    # create model if it doesn't exist
+    try:
+        with open('blankly.json', 'r') as file:
+            data = json.load(file)
+    except FileNotFoundError:
+        print_failure('There was no model detected in this directory. Try running `blankly init` to create one')
+        sys.exit(1)
+
+    if 'plan' not in data:
+        data['plan'] = select('Select a plan:', [Choice(f'{name} - CPU: {info["cpu"]} RAM: {info["ram"]}', name)
+                                                 for name, info in api.get_plans('live').items()]).unsafe_ask()
+
+    if 'model_id' not in data or 'project_id' not in data:
+        model = get_model_interactive(api, data['type'])
+        data['model_id'] = model['modelId']
+        data['project_id'] = model['projectId']
+
+    # save model_id and plan back into blankly.json
+    with open('blankly.json', 'w') as file:
+        json.dump(data, file, indent=4)
+
+    return data
+
+
+def missing_deployment_files() -> list:
+    paths = ['bot.py', 'blankly.json', 'keys.json', 'backtest.json', 'requirements.txt', 'settings.json']
+    return [path for path in paths if not Path(path).is_file()]
+
+
+def blankly_deploy(args):
+    api = ensure_login()
+
+    data = ensure_model(api)
+    for path in missing_deployment_files():
+        if not confirm(f'{path} is missing. Are you sure you want to continue?',
+                       default=False).unsafe_ask():
+            print_failure('Deployment cancelled')
+            print_failure(f'You can try `blankly init` to regenerate the {path} file.')
+            return
+
+    description = text('Enter a description for this version of the model:').unsafe_ask()
+
+    with show_spinner('Uploading model') as spinner:
+        model_path = zip_dir('.', data['ignore_files'])
+
+        params = {
+            'file_path': model_path,
+            'project_id': data['project_id'],  # set by ensure_model
+            'model_id': data['model_id'],  # set by ensure_model
+            'version_description': description,
+            'python_version': get_python_version(),
+            'type_': data.get('type', 'strategy'),
+            'plan': data['plan']  # set by ensure_model
+        }
+        if data['type'] == 'screener':
+            params['schedule'] = data['screener']['schedule']
+
+        response = api.deploy(**params)
+        if response.get('status', None) == 'success':
+            spinner.ok('Model uploaded')
+        else:
+            spinner.fail('Error: ' + response['error'])
+
+
+def blankly_add_key(args):
+    exchange = select('What exchange would you like to add a key for?', EXCHANGE_CHOICES_NO_KEYLESS).unsafe_ask()
+    add_key_interactive(exchange)
+
+
+def blankly_list_key(args):
+    data = load_keys()
+    for exchange_name, keys in data.items():
+        for name, key_data in keys.items():
+            # filter 'empty'
+            if any((isinstance(d, str) and '*' in d) for d in key_data.values()):
+                continue
+            exchange_display_name = exc_display_name(exchange_name)
+            print_work(f'{exchange_display_name}: {name}')
+            for k, v in key_data.items():
+                print_work(f'    {k}: {v}')
+
+
+def blankly_remove_key(args):
+    data = load_keys()
+
+    key_choices = [Choice(f'{exc_display_name(exchange)}: {name}', (exchange, name))
+                   for exchange, keys in data.items() for name in keys]
+    if not key_choices:
+        return
+    exchange, key = select('Which key would you like to delete?', key_choices).unsafe_ask()
+
+    del data[exchange][key]
+    write_keys(data)
+
+
+def blankly_key(args):
+    func = select('What would you like to do?', [
+        Choice('Add a key', blankly_add_key),
+        Choice('Show all keys', blankly_list_key),
+        Choice('Remove a key', blankly_remove_key)
+    ]).unsafe_ask()
+    func(args)
+
+
+def main():
+    parser = argparse.ArgumentParser(description='Blankly CLI & deployment tool')
+    subparsers = parser.add_subparsers(required=True)
+
+    init_parser = subparsers.add_parser('init', help='Initialize a new model in the current directory')
+    init_parser.add_argument('-n', '--no-login', action='store_false', dest='prompt_login',
+                             help='don\'t prompt to connect to Blankly Platform')
+    init_parser.set_defaults(func=blankly_init)
+
+    login_parser = subparsers.add_parser('login', help='Login to the Blankly Platform')
+    login_parser.set_defaults(func=blankly_login)
+
+    logout_parser = subparsers.add_parser('logout', help='Logout of the Blankly Platform')
+    logout_parser.set_defaults(func=blankly_logout)
+
+    deploy_parser = subparsers.add_parser('deploy', help='Upload this model to the Blankly Platform')
+    deploy_parser.set_defaults(func=blankly_deploy)
+
+    key_parser = subparsers.add_parser('key', help='Manage model Exchange API keys')
+    key_parser.set_defaults(func=blankly_key)
+    key_subparsers = key_parser.add_subparsers()
+
+    key_add_parser = key_subparsers.add_parser('remove', help='Delete API Keys from this model')
+    key_add_parser.set_defaults(func=blankly_remove_key)
+
+    key_add_parser = key_subparsers.add_parser('list', help='List API Keys that this model is using')
+    key_add_parser.set_defaults(func=blankly_list_key)
+
+    key_add_parser = key_subparsers.add_parser('add', help='Add an API Key to this model')
+    key_add_parser.set_defaults(func=blankly_add_key)
+
+    # run the selected command
+    args = parser.parse_args()
+    try:
+        args.func(args)
+    except KeyboardInterrupt:
+        print_failure('Cancelled by user')
+    except Exception:
+        print_failure('An error occurred. Traceback:')
+        traceback.print_exc()
+
+
+if __name__ == '__main__':
+    main()
diff --git a/blankly/deployment/reporter_headers.py b/blankly/deployment/reporter_headers.py
index d7792b94..29e0627f 100644
--- a/blankly/deployment/reporter_headers.py
+++ b/blankly/deployment/reporter_headers.py
@@ -73,7 +73,7 @@ def export_screener(self, screener: Screener):
         if self.__screener is None:
             self.__screener = screener
         else:
-            raise RuntimeError("Currently only a single screener can be exported per model.")
+            raise RuntimeError("Currently only a single screener can be created per model.")
 
     def export_screener_result(self, screener: Screener):
         """
@@ -84,12 +84,12 @@ def export_screener_result(self, screener: Screener):
         """
         pass
 
-    def export_backtest_result(self, backtest_result: BacktestResult):
+    def export_backtest_result(self, platform_result: dict):
         """
         Re-export for the finished screener result
 
         Args:
-            backtest_result: A blankly backtest result object
+            platform_result: A blankly backtest result object
         """
         pass
 
diff --git a/blankly/deployment/ui.py b/blankly/deployment/ui.py
new file mode 100644
index 00000000..bc6b4937
--- /dev/null
+++ b/blankly/deployment/ui.py
@@ -0,0 +1,93 @@
+import prompt_toolkit
+from functools import lru_cache
+from prompt_toolkit import print_formatted_text, formatted_text
+from prompt_toolkit.formatted_text import to_formatted_text
+
+import questionary.constants
+from yaspin import yaspin, Spinner as YaspinSpinner
+from yaspin.core import Yaspin
+from yaspin.spinners import Spinners
+
+BOLD = '\033[1m'
+
+STYLE = questionary.Style(
+    [
+        ('qmark', 'fg:#5f819d'),  # token in front of the question
+        ('work', 'fg:#5f819d'),  # token in front of work message
+        ('failure', 'fg:#ff726f'),  # token in front of failure message
+        ('success', 'fg:#4BCA81'),  # token in front of success message
+        ('question', 'bold'),  # question text
+        ('answer', 'fg:#FF9D00 bold'),  # submitted answer text behind the question
+        ('pointer', ''),  # pointer used in select and checkbox prompts
+        ('selected', ''),  # style for a selected item of a checkbox
+        ('separator', ''),  # separator in lists
+        ('instruction', ''),  # user instructions for select, rawselect, checkbox
+        ('text', ''),  # any other text
+        ('instruction', ''),  # user instructions for select, rawselect, checkbox
+    ]
+)
+questionary.constants.DEFAULT_STYLE = STYLE
+
+from questionary import confirm, text, select
+
+
+def fprint(text, **kwargs):
+    if isinstance(text, str):
+        text = [('class:text', text)]
+    print_formatted_text(to_formatted_text(text), style=STYLE, **kwargs)
+
+
+def print_work(text):
+    fprint([('class:work', '*'), ('class:question', ' ' + text)])
+
+
+def print_failure(text):
+    fprint([('class:failure', '✘ '), ('class:question', text)])
+
+
+def print_success(text):
+    fprint([('class:success', '✔ '), ('class:question', text)])
+
+
+@lru_cache(None)
+def text_spinner(
+        text='BLANKLY',
+        left='[',
+        right=']',
+        interval=80
+):
+    fullstr = text + ' ' * (len(text)//2) + text[:-1]
+    win_size = len(text)
+    return YaspinSpinner([
+        left + fullstr[i:i + win_size] + right
+        for i in range(len(fullstr) - win_size, 0, -1)
+    ], interval)
+
+
+def show_spinner(text):
+    return Spinner(text_spinner(), text=BOLD + text)
+
+
+class Spinner(Yaspin):
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.outcome = None
+        self.outcome_text = ''
+
+    def fail(self, text):
+        self.outcome = False
+        self.outcome_text = text
+
+    def ok(self, text):
+        self.outcome = True
+        self.outcome_text = text
+
+    def stop(self):
+        super().stop()
+        if self.outcome is None:
+            return
+
+        if self.outcome:
+            print_success(self.outcome_text)
+        else:
+            print_failure(self.outcome_text)
diff --git a/blankly/enums.py b/blankly/enums.py
new file mode 100644
index 00000000..4c0524b2
--- /dev/null
+++ b/blankly/enums.py
@@ -0,0 +1,50 @@
+import enum
+
+
+class MarginType(str, enum.Enum):
+    CROSSED = 'crossed'
+    ISOLATED = 'isolated'
+
+
+class Side(str, enum.Enum):
+    BUY = 'buy'
+    SELL = 'sell'
+
+
+class PositionMode(str, enum.Enum):
+    BOTH = 'both'
+    LONG = 'long'
+    SHORT = 'short'
+
+
+class TimeInForce(str, enum.Enum):
+    GTC = 'GTC'  # Good Till Cancelled
+    FOK = 'FOK'  # Fill or Kill
+    IOC = 'IOC'  # Immediate or Cancel
+
+
+class HedgeMode(str, enum.Enum):
+    HEDGE = 'hedge'
+    ONEWAY = 'oneway'
+
+
+class OrderStatus(str, enum.Enum):
+    OPEN = 'open'
+    PARTIALLY_FILLED = 'partially_filled'
+    FILLED = 'filled'
+    CANCELED = 'canceled'
+    EXPIRED = 'expired'
+
+
+class OrderType(str, enum.Enum):
+    MARKET = 'market'
+    LIMIT = 'limit'
+    STOP = 'stop'
+    TAKE_PROFIT = 'take_profit'
+    LIQUIDATION = 'liquidation'
+
+
+class ContractType(str, enum.Enum):
+    PERPETUAL = 'perpetual'
+    EXPIRING = 'expiring'
+    MOVE = 'move'
diff --git a/blankly/exchanges/abc_base_exchange.py b/blankly/exchanges/abc_base_exchange.py
new file mode 100644
index 00000000..52147cea
--- /dev/null
+++ b/blankly/exchanges/abc_base_exchange.py
@@ -0,0 +1,33 @@
+"""
+    Base Exchange object.
+    Copyright (C) 2022 Matias Kotlik
+
+    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
+
+
+class ABCBaseExchange(abc.ABC):
+
+    @abc.abstractmethod
+    def get_interface(self):
+        pass
+
+    @abc.abstractmethod
+    def get_name(self) -> str:
+        pass
+
+    @abc.abstractmethod
+    def get_type(self) -> str:
+        pass
diff --git a/blankly/exchanges/abc_exchange.py b/blankly/exchanges/abc_exchange.py
index a6fb78d6..111f6fb1 100644
--- a/blankly/exchanges/abc_exchange.py
+++ b/blankly/exchanges/abc_exchange.py
@@ -18,27 +18,9 @@
 
 import abc
 
+from blankly.exchanges.abc_base_exchange import ABCBaseExchange
+from blankly.exchanges.interfaces.abc_base_exchange_interface import ABCBaseExchangeInterface
 
-class ABCExchange(abc.ABC):
-    """
-    Functions required for easy interaction with the GUI/main
-    """
 
-    @abc.abstractmethod
-    def append_model(self, model, coin, args):
-        pass
-
-    @abc.abstractmethod
-    def get_full_state(self, symbol):
-        pass
-
-    @abc.abstractmethod
-    def get_model_state(self, symbol):
-        pass
-
-    @abc.abstractmethod
-    def get_asset_state(self, symbol):
-        pass
-
-    def get_exchange_state(self):
-        pass
+class ABCExchange(ABCBaseExchange, abc.ABC):
+    pass
diff --git a/blankly/exchanges/exchange.py b/blankly/exchanges/exchange.py
index ce589fa2..328dddf3 100644
--- a/blankly/exchanges/exchange.py
+++ b/blankly/exchanges/exchange.py
@@ -28,6 +28,8 @@
 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
+from blankly.exchanges.interfaces.okx.okx_interface import OkxInterface
+
 
 
 class Exchange(ABCExchange, abc.ABC):
@@ -79,6 +81,8 @@ def construct_interface_and_cache(self, calls):
             self.interface = OandaInterface(self.__type, calls)
         elif self.__type == "kucoin":
             self.interface = KucoinInterface(self.__type, calls)
+        elif self.__type == "okx":
+            self.interface = OkxInterface(self.__type, calls)
 
         blankly.reporter.export_used_exchange(self.__type)
 
diff --git a/blankly/exchanges/futures/__init__.py b/blankly/exchanges/futures/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/blankly/exchanges/futures/futures_exchange.py b/blankly/exchanges/futures/futures_exchange.py
new file mode 100644
index 00000000..3bafa55d
--- /dev/null
+++ b/blankly/exchanges/futures/futures_exchange.py
@@ -0,0 +1,65 @@
+"""
+    Base FuturesExchange class.
+    Copyright (C) 2022 Matias Kotlik
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+import abc
+import time
+
+import blankly
+from blankly.exchanges.abc_base_exchange import ABCBaseExchange
+from blankly.exchanges.auth.utils import write_auth_cache
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+
+
+class FuturesExchange(ABCBaseExchange, abc.ABC):
+    exchange_type: str
+    portfolio_name: str
+
+    def __init__(self, exchange_type, portfolio_name, preferences_path):
+        self.exchange_type = exchange_type  # binance, ftx
+        self.portfolio_name = portfolio_name  # my_cool_portfolio
+        self.preferences = blankly.utils.load_user_preferences(
+            preferences_path)
+        self.models = {}
+
+    @property
+    @abc.abstractmethod
+    def calls(self):
+        """Authenticated direct calls object"""
+        pass
+
+    @property
+    @abc.abstractmethod
+    def interface(self) -> FuturesExchangeInterface:
+        """This FuturesExchange's Interface"""
+        pass
+
+    def initialize(self):
+        """
+        Initialize the exchange. This should usually be called at the end of subclass constructors.
+        This will report the exchange type to blankly.reporter and initialize the cache for this exchange.
+        """
+        blankly.reporter.export_used_exchange(self.exchange_type)
+        write_auth_cache(self.exchange_type, self.portfolio_name, self.calls)
+
+    def get_name(self) -> str:
+        return self.portfolio_name
+
+    def get_type(self) -> str:
+        return self.exchange_type
+
+    def get_interface(self):
+        return self.interface
diff --git a/blankly/exchanges/futures/futures_strategy_logger.py b/blankly/exchanges/futures/futures_strategy_logger.py
new file mode 100644
index 00000000..d0c07ec3
--- /dev/null
+++ b/blankly/exchanges/futures/futures_strategy_logger.py
@@ -0,0 +1,47 @@
+"""
+    Logging system to allow users to view & understand actions done in the strategy
+    Copyright (C) 2022 Matias Kotlik
+
+    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 abc import ABC
+
+from blankly.enums import Side
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+
+# TODO
+from blankly.exchanges.orders.futures.futures_order import FuturesOrder
+
+
+class FuturesStrategyLogger:
+    interface: FuturesExchangeInterface
+
+    def __init__(self, interface=None, strategy=None):
+        self.interface = interface
+        self.strategy = strategy
+
+    def __getattribute__(self, item):
+        # run the overridden function in this class if it exists, or default to the method in self.interface
+        try:
+            # this is NOT recursive, it will get the *actual* attribute
+            return object.__getattribute__(self, item)
+        except AttributeError:
+            return self.interface.__getattribute__(item)
+
+    # TODO log order (and other order types)
+    # example:
+    def market_order(self, symbol: str, side: Side, size: float, *args,
+                     **kwargs) -> FuturesOrder:
+        # log here
+        return self.interface.market_order(symbol, side, size, *args, **kwargs)
diff --git a/blankly/exchanges/interfaces/abc_base_exchange_interface.py b/blankly/exchanges/interfaces/abc_base_exchange_interface.py
new file mode 100644
index 00000000..b678e137
--- /dev/null
+++ b/blankly/exchanges/interfaces/abc_base_exchange_interface.py
@@ -0,0 +1,167 @@
+"""
+    Base ExchangeInterface object.
+    Copyright (C) 2022 Matias Kotlik
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+
+import abc
+import time
+import warnings
+from collections import deque
+from datetime import datetime as dt
+from typing import Union
+
+import numpy
+import pandas
+import pandas as pd
+from dateutil import parser
+
+from blankly import utils
+from blankly.utils import time_interval_to_seconds
+
+
+# A lot of this class is just glue between ExchangeInterface and the new Futures classes.
+# At some point it should probably all be refactored away but for now let's get futures working!
+class ABCBaseExchangeInterface(abc.ABC):
+
+    @abc.abstractmethod
+    def get_exchange_type(self):
+        pass
+
+    @abc.abstractmethod
+    def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
+        pass
+
+    def history(self,
+                symbol: str,
+                to: Union[str, int] = 200,
+                resolution: Union[str, float] = '1d',
+                start_date: Union[str, dt, float] = None,
+                end_date: Union[str, dt, float] = None,
+                return_as: str = 'df'):
+
+        start, stop, res_seconds, to, present = self.calculate_epochs(start_date, end_date, resolution, to)
+
+        response = self.overridden_history(symbol, start, stop, res_seconds, to=to,)
+
+        # Add a check to make sure that coinbase pro has updated
+        # I tried to delete this code but the entire function broke :(
+        if present and self.get_exchange_type() == "coinbase_pro":
+            data_append = None
+            tries = 0
+            while True:
+                if data_append is None:
+                    # We can continue if this is valid
+                    if response['time'].iloc[-1] == stop:
+                        break
+                else:
+                    if data_append[0]['time'] == stop:
+                        break
+                time.sleep(.5)
+                tries += 1
+                if tries > 5:
+                    # Admit failure and return
+                    warnings.warn("Exchange failed to provide updated data within the timeout.")
+                    return self.cast_type(response, return_as)
+                try:
+                    data_append = [self.get_product_history(symbol,
+                                                            stop - res_seconds,
+                                                            stop,
+                                                            res_seconds).iloc[-1].to_dict()]
+                    data_append[0]['time'] = int(data_append[0]['time'])
+                except IndexError:
+                    # If there is no recent data on the exchange this will be an empty dataframe.
+                    # This happens for low volume
+                    utils.info_print("Most recent bar at this resolution does not yet exist - skipping.")
+                    break
+
+            response = response.append(data_append, ignore_index=True)
+
+        # Determine the deque length - we really should use this generally
+        if isinstance(to, int):
+            point_count = to
+        else:
+            point_count = (stop-start)/res_seconds + 1
+        # response.index = pd.to_datetime(response['time'], unit='s')
+        return self.cast_type(response, return_as, point_count)
+
+    def calculate_epochs(self, start_date, end_date, resolution, to):
+        is_backtesting = self.is_backtesting()
+        if is_backtesting is not None and end_date is None:
+            # is_backtesting can only return a non None value if a function overrides the is_backtesting function
+            #  and says its backtesting by returning a valid time
+            end_date = is_backtesting
+        if start_date is not None and end_date is not None:
+            to = None
+        to_present = False
+        if end_date is None:
+            to_present = True
+        # convert resolution into epoch seconds
+        resolution_seconds = int(time_interval_to_seconds(resolution))
+        if end_date is None:
+            # Figure out the next point and then subtract to the last stamp
+            most_recent_valid_resolution = utils.ceil_date(dt.now(),
+                                                           seconds=resolution_seconds).timestamp() - resolution_seconds
+            # Binance is nice enough to update OHLCV data, so we have to exclude that by subtracting a resolution
+            epoch_stop = most_recent_valid_resolution - resolution_seconds
+            count_from = most_recent_valid_resolution
+        else:
+            if isinstance(end_date, str):
+                parsed_date = parser.parse(end_date)
+            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
+            valid_time_in_past = utils.ceil_date(parsed_date,
+                                                 seconds=resolution_seconds).timestamp() - resolution_seconds
+            epoch_stop = valid_time_in_past - resolution_seconds
+            count_from = valid_time_in_past
+        if start_date is None and end_date is None:
+            if isinstance(to, int):
+                # use number of points to calculate the start epoch
+                epoch_start = count_from - (to * resolution_seconds)
+            else:
+                epoch_start = count_from - time_interval_to_seconds(to)
+        elif start_date is None and end_date is not None:
+            if isinstance(to, int):
+                epoch_start = count_from - (to * resolution_seconds)
+            else:
+                epoch_start = count_from - time_interval_to_seconds(to)
+        else:
+            epoch_start = utils.convert_input_to_epoch(start_date)
+        return epoch_start, epoch_stop, resolution_seconds, to, to_present
+
+    def overridden_history(self, symbol, epoch_start, epoch_stop, resolution, **kwargs) -> pd.DataFrame:
+        return self.get_product_history(symbol, epoch_start, epoch_stop, resolution)
+
+    def is_backtesting(self):
+        return None
+
+    @staticmethod
+    def cast_type(response: pd.DataFrame, return_as: str, point_count=None):
+        if return_as != 'df' and return_as != 'deque':
+            return response.to_dict(return_as)
+        elif return_as == 'deque':
+            # Create a deque object that has the same length
+            response = response.to_dict('list')
+            for i in response.keys():
+                response[i] = deque(response[i], point_count)
+            return response
+        elif return_as == 'df':
+            return response
+        else:
+            utils.info_print(f"Return type {return_as} is not supported.")
+        return response
diff --git a/blankly/exchanges/interfaces/abc_exchange_interface.py b/blankly/exchanges/interfaces/abc_exchange_interface.py
index a49b94eb..5b52363c 100644
--- a/blankly/exchanges/interfaces/abc_exchange_interface.py
+++ b/blankly/exchanges/interfaces/abc_exchange_interface.py
@@ -22,12 +22,13 @@
 
 import pandas
 
+from blankly.exchanges.interfaces.abc_base_exchange_interface import ABCBaseExchangeInterface
 from blankly.utils.utils import AttributeDict
 from blankly.exchanges.orders.limit_order import LimitOrder
 from blankly.exchanges.orders.market_order import MarketOrder
 
 
-class ABCExchangeInterface(abc.ABC):
+class ABCExchangeInterface(ABCBaseExchangeInterface, abc.ABC):
     @abc.abstractmethod
     def __init__(self, exchange_name, authenticated_API):
         """
@@ -153,65 +154,13 @@ def get_order(self,
         pass
 
     @abc.abstractmethod
-    def get_fees(self) -> dict:
+    def get_fees(self, symbol: str) -> dict:
         """
         Get market fees
         TODO add return example
         """
         pass
 
-    """
-    Coinbase Pro: get_account_history
-    binance: 
-        get_deposit_history
-        get_withdraw_history
-
-    """
-
-    @abc.abstractmethod
-    def history(self,
-                symbol: str,
-                to: Union[str, int] = 200,
-                resolution: Union[str, int] = '1d',
-                start_date: Union[str, dt, float] = None,
-                end_date: Union[str, dt, float] = None,
-                return_as: str = 'df') -> pandas.DataFrame:
-        """
-        Wrapper for .get_product_history() which allows users to more easily get product history from right now.
-        Args:
-            symbol: Blankly product ID format (BTC-USD)
-            to (str or int): The number of data points back in time either expressed as a string
-                ("1y" meaning 1 year back") or int of points (300 points at specified resolution)
-            resolution: Resolution as a string (i.e. "1d", "4h", "1y")
-            start_date (str or datetime or float): Start Date for data gathering (in either string, datetime or epoch
-                timestamp)
-            end_date (str or datetime or float): End Date for data gathering (in either string, datetime or epoch
-                timestamp)
-            return_as (str): Return Type (Either list or df (dataframe))
-        Returns:
-            Dataframe with *at least* 'time (epoch)', 'low', 'high', 'open', 'close', 'volume' as columns.
-            TODO add return example
-        """
-        pass
-
-    @abc.abstractmethod
-    def get_product_history(self,
-                            symbol: str,
-                            epoch_start: float,
-                            epoch_stop: float,
-                            resolution: Union[str, int]) -> pandas.DataFrame:
-        """
-        Returns the product history from an exchange
-        Args:
-            symbol: The asset such as (BTC-USD, or MSFT)
-            epoch_start: Time to begin download
-            epoch_stop: Time to stop download
-            resolution: Resolution in seconds between tick (ex: 60 = 1 per minute)
-        Returns:
-            Dataframe with *at least* 'time (epoch)', 'low', 'high', 'open', 'close', 'volume' as columns.
-            TODO add return example
-        """
-
     @abc.abstractmethod
     def get_order_filter(self,
                          symbol: str) -> dict:
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca.py b/blankly/exchanges/interfaces/alpaca/alpaca.py
index 2ac31e77..c3d2cef7 100644
--- a/blankly/exchanges/interfaces/alpaca/alpaca.py
+++ b/blankly/exchanges/interfaces/alpaca/alpaca.py
@@ -38,7 +38,7 @@ def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=Non
         super().construct_interface_and_cache(calls)
 
     def get_exchange_state(self):
-        return self.interface.get_fees()
+        return self.interface.get_products()
 
     def get_asset_state(self, symbol):
         return self.interface.get_account(symbol)
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_interface.py b/blankly/exchanges/interfaces/alpaca/alpaca_interface.py
index 8849ccc2..d0d01167 100644
--- a/blankly/exchanges/interfaces/alpaca/alpaca_interface.py
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_interface.py
@@ -33,8 +33,6 @@
 from blankly.utils.exceptions import APIException
 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, exchange_name, authenticated_api):
@@ -111,6 +109,7 @@ def cash(self):
         account_dict = self.calls.get_account()
         return float(account_dict['buying_power'])
 
+    @utils.enforce_base_asset
     def get_account(self, symbol=None):
         assert isinstance(self.calls, alpaca_trade_api.REST)
 
@@ -331,7 +330,7 @@ def homogenize_order(self, order):
         order = utils.isolate_specific(needed, order)
         return order
 
-    def get_fees(self):
+    def get_fees(self, symbol):
         assert isinstance(self.calls, alpaca_trade_api.REST)
         return {
             'maker_fee_rate': 0.0,
@@ -372,8 +371,8 @@ def get_product_history(self, symbol: str, epoch_start: float, epoch_stop: float
                     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."
+                    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:
@@ -400,68 +399,6 @@ def get_product_history(self, symbol: str, epoch_start: float, epoch_stop: float
             # This runs yfinance on the symbol
             return self.parse_yfinance(symbol, epoch_start, epoch_stop, resolution)
 
-    # def history(self,
-    #             symbol: str,
-    #             to: Union[str, int] = 200,
-    #             resolution: Union[str, int] = '1d',
-    #             start_date: Union[str, dt, float] = None,
-    #             end_date: Union[str, dt, float] = None,
-    #             return_as: str = 'df'):
-    #
-    #     assert isinstance(self.calls, alpaca_trade_api.REST)
-    #
-    #     if not end_date:
-    #         end_date = dt.now(tz=timezone.utc)
-    #
-    #     end_date = self.__convert_times(end_date)
-    #
-    #     if start_date:
-    #         start_date = self.__convert_times(start_date)
-    #
-    #     alpaca_v1_resolutions = [60 * 1, 60 * 5, 60 * 15, 60 * 60 * 24]
-    #
-    #     # convert resolution into epoch seconds
-    #     resolution_seconds = time_interval_to_seconds(resolution)
-    #
-    #     found_multiple, row_divisor = self.__evaluate_multiples(alpaca_v1_resolutions, resolution_seconds)
-    #
-    #     if found_multiple == 60:
-    #         time_interval = '1Min'
-    #     elif found_multiple == 60 * 5:
-    #         time_interval = '5Min'
-    #     elif found_multiple == 60 * 15:
-    #         time_interval = '15Min'
-    #     elif found_multiple == 60 * 60 * 24:
-    #         time_interval = '1D'
-    #     else:
-    #         time_interval = 'failed'
-    #
-    #     if to:
-    #         aggregated_limit = to * row_divisor
-    #         bars = self.calls.get_barset(symbol, time_interval, limit=int(aggregated_limit),
-    #         end=end_date.isoformat())[
-    #             symbol]
-    #         return_df = pd.DataFrame(bars)
-    #         return_df.rename(columns={"t": "time", "o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"},
-    #                          inplace=True)
-    #
-    #         history = utils.get_ohlcv(return_df, row_divisor, from_zero=True)
-    #
-    #     else:
-    #         # bars = self.calls.get_barset(symbol, time_interval, start=start_date.isoformat(),
-    #         # end=end_date.isoformat())[symbol]
-    #         history = self.get_product_history(symbol,
-    #                                            start_date.timestamp(),
-    #                                            end_date.timestamp(),
-    #                                            int(resolution_seconds))
-    #
-    #     return super().cast_type(history, return_as)
-    #     # return_df = pd.DataFrame(bars)
-    #     # return_df.rename(columns={"t": "time", "o": "open", "h": "high", "l": "low", "c": "close", "v": "volume"},
-    #     # inplace=True)
-    #     #
-    #     # return utils.get_ohlcv_2(return_df, row_divisor)
-
     def overridden_history(self, symbol, epoch_start, epoch_stop, resolution_seconds, **kwargs) -> pd.DataFrame:
         to = kwargs['to']
         # If it's a string alpaca is an edge case where epoch can be used
@@ -476,32 +413,52 @@ def overridden_history(self, symbol, epoch_start, epoch_stop, resolution_seconds
                     60: '1Min',
                     300: '5Min',
                     900: '15Min',
-                    86400: '1D'
+                    86400: '1Day'
                 }
-                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
+                time_interval = resolution_lookup[resolution_seconds]
 
-                    frames.append(self.calls.get_barset(symbol, time_interval, limit=1000,
-                                                        end=utils.iso8601_from_epoch(epoch_stop))[symbol])
-                    to -= 1000
+                def find_last_n_points(epoch_start_, epoch_stop_):
+                    frames = []
+
+                    # This just overestimates and then chops off the remainder
+                    while epoch_start_ <= epoch_stop_:
+                        # Create an end time by moving after the start time by 1000 datapoints
+                        frames.append(self.calls.get_bars(symbol, time_interval, limit=10000,
+                                                          start=utils.iso8601_from_epoch(epoch_start_)).df)
+                        epoch_start_ += resolution_seconds * 10000
+
+                    for i in range(len(frames)):
+                        series = []
+                        for j in frames[i].index:
+                            series.append(j.timestamp())
+                        frames[i]['time'] = pd.Series(series).values
+                        frames[i] = frames[i].reset_index(drop=True)
+
+                    if len(frames) != 0:
+                        response_ = pd.concat(frames, ignore_index=True)
+                    else:
+                        response_ = pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
 
-                frames.append(self.calls.get_barset(symbol, time_interval, limit=to,
-                                                    end=utils.iso8601_from_epoch(epoch_stop))[symbol])
+                    return response_
 
-                for i in range(len(frames)):
-                    frames[i] = pd.DataFrame(frames[i])
+                # Create an empty dataframe here
+                response = pd.DataFrame(columns=['time', 'open', 'high', 'low', 'close', 'volume'])
+                batched_ranges = [epoch_start, epoch_stop]
+                # Batch it until we have enough points
+                tries = 2
+                while len(response) < to:
+                    response = pd.concat([response, find_last_n_points(batched_ranges[0], batched_ranges[1])])
+                    batched_ranges[0] -= resolution_seconds * tries
+                    batched_ranges[1] -= resolution_seconds * tries
+                    tries = tries * 2
 
-                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[['time', 'open', 'high', 'low', 'close', 'volume']]
 
                 response = response.sort_values(by=['time'], ignore_index=True)
 
+                response = response.tail(to)
+
                 response = response.astype({
                     'time': int,
                     'open': float,
@@ -632,8 +589,8 @@ def get_order_filter(self, symbol: str):
 
     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'])
+        response = self.calls.get_latest_trade(symbol=symbol)
+        return float(response['p'])
 
     @staticmethod
     def parse_yfinance(symbol: str, epoch_start: [int, float], epoch_stop: [int, float], resolution: int):
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py b/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py
index c0c590d4..887805c6 100644
--- a/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py
@@ -91,7 +91,7 @@ def create_ticker_connection(symbol, url, channel):
 class Tickers(ABCExchangeWebsocket):
     def __init__(self, symbol, stream, log=None,
                  pre_event_callback=None, initially_stopped=False,
-                 WEBSOCKET_URL="wss://stream.data.alpaca.markets/v2/iex/"):
+                 websocket_url="wss://stream.data.alpaca.markets/v2/iex/", **kwargs):
         """
         Create and initialize the ticker
         Args:
@@ -99,11 +99,12 @@ def __init__(self, symbol, stream, log=None,
             stream: Valid exchange stream to attach to, these are:
                 trades, quotes, bars, dailyBars, statuses, lulds
             log: Fill this with a path to a log file that should be created
-            WEBSOCKET_URL: Default websocket URL feed.
+            websocket_url: Default websocket URL feed.
         """
         self.__symbol = symbol
         self.__stream = stream
         self.__logging_callback, self.__interface_callback, log_message = switch_type(stream)
+        self.__kwargs = kwargs
 
         # Initialize log file
         if log is not None:
@@ -117,7 +118,7 @@ def __init__(self, symbol, stream, log=None,
         else:
             self.__log = False
 
-        self.URL = WEBSOCKET_URL
+        self.URL = websocket_url
         self.ws = None
         self.__response = None
         self.__subscription_response = None
@@ -190,7 +191,7 @@ def read_websocket(self):
 
                 try:
                     for i in self.__callbacks:
-                        i(interface_message)
+                        i(interface_message, **self.__kwargs)
                 except Exception:
                     traceback.print_exc()
 
diff --git a/blankly/exchanges/interfaces/binance/binance.py b/blankly/exchanges/interfaces/binance/binance.py
index dd56534e..b00dbe81 100644
--- a/blankly/exchanges/interfaces/binance/binance.py
+++ b/blankly/exchanges/interfaces/binance/binance.py
@@ -61,7 +61,7 @@ def get_exchange_state(self):
         Exchange state is the external properties for the exchange block
         """
         # TODO Populate this with useful information
-        return self.interface.get_fees()
+        return self.interface.get_products()
 
     def get_direct_calls(self) -> Client:
         return self.calls
diff --git a/blankly/exchanges/interfaces/binance/binance_interface.py b/blankly/exchanges/interfaces/binance/binance_interface.py
index 703f7413..066196ff 100644
--- a/blankly/exchanges/interfaces/binance/binance_interface.py
+++ b/blankly/exchanges/interfaces/binance/binance_interface.py
@@ -18,7 +18,6 @@
 import time
 
 import binance.exceptions
-import numpy
 import pandas as pd
 
 import blankly.utils.exceptions as exceptions
@@ -151,6 +150,7 @@ def get_products(self):
             products[i] = utils.isolate_specific(needed, products[i])
         return products
 
+    @utils.enforce_base_asset
     def get_account(self, symbol=None) -> utils.AttributeDict:
         """
         Get all currencies in an account, or sort by asset/account_id
@@ -532,7 +532,7 @@ def get_order(self, symbol, order_id) -> dict:
     binance: get_trade_fee
     """
 
-    def get_fees(self) -> dict:
+    def get_fees(self, symbol) -> dict:
         needed = self.needed['get_fees']
         """
         {
@@ -771,7 +771,11 @@ def get_order_filter(self, symbol):
         max_market_notational = 92233720368.547752  # For some reason equal to the first *11 digits* of 2^63 then
         # it gets weird past the decimal
 
-        max_orders = int(filters[6]["maxNumOrders"])
+        # Must test both nowadays
+        try:
+            max_orders = int(filters[7]["maxNumOrders"])
+        except KeyError:
+            max_orders = int(filters[6]["maxNumOrders"])
 
         if percent_min_price < hard_min_price:
             min_price = hard_min_price
diff --git a/blankly/exchanges/interfaces/binance/binance_websocket.py b/blankly/exchanges/interfaces/binance/binance_websocket.py
index ff375ae8..badd990d 100644
--- a/blankly/exchanges/interfaces/binance/binance_websocket.py
+++ b/blankly/exchanges/interfaces/binance/binance_websocket.py
@@ -27,22 +27,22 @@
 
 class Tickers(Websocket):
     def __init__(self, symbol, stream, log=None, initially_stopped=False,
-                 WEBSOCKET_URL="wss://stream.binance.{}:9443/ws"):
+                 websocket_url="wss://stream.binance.{}:9443/ws", **kwargs):
         """
         Create and initialize the ticker
         Args:
             symbol: Currency to initialize on such as "btcusdt"
             stream: Stream to use, such as "depth" or "trade"
             log: Fill this with a path to a log file that should be created
-            WEBSOCKET_URL: Default websocket URL feed.
+            websocket_url: Default websocket URL feed.
         """
         # Reload preferences
         self.__preferences = blankly.utils.load_user_preferences()
 
         self.__logging_callback, self.__interface_callback, log_message = websocket_utils.switch_type(stream)
-        url = WEBSOCKET_URL.format(self.__preferences['settings']['binance']['binance_tld'])
+        url = websocket_url.format(self.__preferences['settings']['binance']['binance_tld'])
 
-        super().__init__(symbol, stream, log, log_message, url, None)
+        super().__init__(symbol, stream, log, log_message, url, None, kwargs)
 
         # Start the websocket
         if not initially_stopped:
@@ -75,7 +75,7 @@ def on_message(self, ws, message):
             self.ticker_feed.append(interface_message)
             self.most_recent_tick = interface_message
             for i in self.callbacks:
-                i(interface_message)
+                i(interface_message, **self.kwargs)
         except KeyError:
             # If the try below figures this out then we don't have to traceback
             error_found = False
diff --git a/blankly/exchanges/interfaces/binance_futures/__init__.py b/blankly/exchanges/interfaces/binance_futures/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/blankly/exchanges/interfaces/binance_futures/binance_futures.py b/blankly/exchanges/interfaces/binance_futures/binance_futures.py
new file mode 100644
index 00000000..54c21485
--- /dev/null
+++ b/blankly/exchanges/interfaces/binance_futures/binance_futures.py
@@ -0,0 +1,53 @@
+"""
+    Binance Futures Exchange object.
+    Copyright (C) 2021 Matias Kotlik
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+from binance.client import Client
+
+from blankly.exchanges.auth.auth_constructor import AuthConstructor
+from blankly.exchanges.futures.futures_exchange import FuturesExchange
+from blankly.exchanges.interfaces.binance_futures.binance_futures_interface import BinanceFuturesInterface
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+
+
+class BinanceFutures(FuturesExchange):
+
+    def __init__(self,
+                 portfolio_name=None,
+                 keys_path="keys.json",
+                 preferences_path=None):
+        super().__init__("binance_futures", portfolio_name, preferences_path)
+
+        # Load auth from keys file
+        auth = AuthConstructor(keys_path, portfolio_name, 'binance',
+                               ['API_KEY', 'API_SECRET', 'sandbox'])
+
+        self.__calls = Client(api_key=auth.keys['API_KEY'],
+                              api_secret=auth.keys['API_SECRET'],
+                              testnet=auth.keys['sandbox'])
+
+        self.__interface = BinanceFuturesInterface(self.exchange_type,
+                                                   self.calls)
+
+        self.initialize()
+
+    @property
+    def calls(self):
+        return self.__calls
+
+    @property
+    def interface(self) -> FuturesExchangeInterface:
+        return self.__interface
diff --git a/blankly/exchanges/interfaces/binance_futures/binance_futures_interface.py b/blankly/exchanges/interfaces/binance_futures/binance_futures_interface.py
new file mode 100644
index 00000000..0dfd6178
--- /dev/null
+++ b/blankly/exchanges/interfaces/binance_futures/binance_futures_interface.py
@@ -0,0 +1,529 @@
+"""
+    Binance Futures ExchangeInterface object.
+    Copyright (C) 2022 Matias Kotlik
+
+    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
+from typing import Optional
+
+import binance.exceptions
+
+try:
+    from functools import cached_property
+except ImportError:  # emerson is "too cool" for py3.8
+    from functools import lru_cache
+
+    def cached_property(func):
+        return property(lru_cache(maxsize=None)(func))
+
+
+from datetime import datetime as dt
+import pandas as pd
+from binance.client import Client
+from binance.exceptions import BinanceAPIException
+
+import blankly
+from blankly.utils import utils, time_builder
+from blankly.enums import MarginType, PositionMode, Side, TimeInForce, HedgeMode, OrderType, ContractType, OrderStatus
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+from blankly.exchanges.orders.futures.futures_order import FuturesOrder
+
+BINANCE_FUTURES_FEES = [
+    (0.020, 0.040),
+    (0.016, 0.040),
+    (0.014, 0.035),
+    (0.012, 0.032),
+    (0.010, 0.030),
+    (0.008, 0.027),
+    (0.006, 0.025),
+    (0.004, 0.022),
+    (0.002, 0.020),
+    (0.000, 0.017),
+]
+
+
+class BinanceFuturesInterface(FuturesExchangeInterface):
+    calls: Client
+
+    @staticmethod
+    def to_exchange_symbol(symbol: str):
+        return symbol.replace('-', '')
+
+    # TODO binance SPOT should prob be refactored to do this at some point
+    def to_blankly_symbol(self, symbol: str):
+        return self.symbol_map[symbol]
+
+    @cached_property
+    def symbol_map(self):
+        symbols = self.calls.futures_exchange_info()['symbols']
+        return {
+            symbol['symbol']: symbol['baseAsset'] + '-' + symbol['quoteAsset']
+            for symbol in symbols
+        }
+
+    def __init__(self, exchange_name, authenticated_api):
+        super().__init__(exchange_name, authenticated_api)
+
+    def init_exchange(self):
+        try:
+            self.calls.futures_account()
+        except BinanceAPIException as e:
+            raise Exception(
+                f"{e.error_message}. 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.")
+        try:
+            # force oneway mode
+            self.calls.futures_change_position_mode(dualSidePosition=False)
+        except binance.error.ClientError as e:
+            if e.error_code != -4059:  # re raise anything other than "already set"
+                raise e
+
+    def set_hedge_mode(self, hedge_mode: HedgeMode):
+        is_hedge = True if hedge_mode == HedgeMode.HEDGE else False
+
+        if self.calls.futures_get_position_mode(
+        )['dualSidePosition'] != is_hedge:
+            self.calls.futures_change_position_mode(dualSidePosition=is_hedge)
+
+    def get_hedge_mode(self):
+        is_hedge = self.calls.futures_get_position_mode()['dualSidePosition']
+        return HedgeMode.HEDGE if is_hedge else HedgeMode.ONEWAY
+
+    def get_leverage(self, symbol: str = None) -> float:
+        if not symbol:
+            raise Exception(
+                'Binance Futures does not support account wide leverage. Use interface.get_leverage(leverage, '
+                'symbol) to get leverage for a symbol. ')
+        symbol = self.to_exchange_symbol(symbol)
+        return float(
+            self.calls.futures_position_information(
+                symbol=symbol)[0]['leverage'])
+
+    @utils.order_protection
+    def set_leverage(self, leverage: int, symbol: str = None):
+        if not symbol:
+            raise Exception(
+                'Binance Futures does not support account wide leverage. Use interface.set_leverage(leverage, '
+                'symbol) to set leverage for each symbol you wish to trade. ')
+        symbol = self.to_exchange_symbol(symbol)
+        return self.calls.futures_change_leverage(
+            symbol=symbol, leverage=leverage)['maxNotionalValue']
+
+    @utils.order_protection
+    def set_margin_type(self, symbol: str, type: MarginType):
+        """
+        Set margin type for a symbol.
+        """
+        symbol = self.to_exchange_symbol(symbol)
+        try:
+            self.calls.futures_change_margin_type(symbol=symbol,
+                                                  marginType=type.upper())
+        except BinanceAPIException as e:
+            if e.code != -4046:  # -4046 NO_NEED_TO_CHANGE_MARGIN_TYPE (margin type is already set)
+                raise e
+
+    def get_margin_type(self, symbol: str):
+        # binance doesn't have an API for this
+        # an attempt to create a workaround is preserved below
+        # proceed at your own risk
+        raise NotImplementedError
+        # symbol = self.to_exchange_symbol(symbol)
+        #
+        # # first check for open position
+        # position = self.get_positions(symbol)
+        # if position:
+        #     return position.margin_type
+        #
+        # # to get the current margin type, try setting CROSSED and see if an exception is raised.
+        # try:
+        #     self.calls.futures_change_margin_type(
+        #         symbol=symbol, marginType=MarginType.CROSSED.upper())
+        # except BinanceAPIException as e:
+        #     if e.code != -4046:  # -4046 NO_NEED_TO_CHANGE_MARGIN_TYPE (margin type is already set)
+        #         return MarginType.CROSSED
+        #
+        # # if it actually changed, change it back to ISOLATED
+        # self.calls.futures_change_margin_type(
+        #     symbol=symbol, marginType=MarginType.ISOLATED.upper())
+        # return MarginType.ISOLATED
+
+    def get_products(self, filter: str = None) -> dict:
+        # https://binance-docs.github.io/apidocs/futures/en/#exchange-information
+        res = self.calls.futures_exchange_info()["symbols"]
+        products = {}
+        for prod in res:
+            if '_' in prod['symbol']:
+                continue  # don't support expiring contracts
+            symbol = prod['baseAsset'] + '-' + prod['quoteAsset']
+            products[symbol] = utils.AttributeDict({
+                'symbol': symbol,
+                'base_asset': prod['baseAsset'],
+                'quote_asset': prod['quoteAsset'],
+                'contract_type': ContractType.PERPETUAL,
+                'price_precision': int(prod['pricePrecision']),
+                'size_precision': int(prod['quantityPrecision']),
+                'exchange_specific': prod
+            })
+
+        if filter:
+            return products[filter]
+        return products
+
+    def get_account(self, filter=None) -> utils.AttributeDict:
+        res = self.calls.futures_account()
+
+        accounts = utils.AttributeDict()
+        for asset in res['assets']:
+            symbol = asset['asset']
+            accounts[symbol] = utils.AttributeDict({
+                'available': float(asset['availableBalance']),
+                'exchange_specific': asset,
+            })
+
+        if filter:
+            return accounts[filter]
+        return accounts
+
+    def get_positions(self, filter: str = None) -> Optional[dict]:
+        account = self.calls.futures_account()
+
+        positions = {}
+
+        # write in data from binance
+        for position in account['positions']:
+            symbol = position['symbol']
+            size = float(position['positionAmt'])
+            if size == 0:
+                continue  # don't show empty positions
+            symbol = self.to_blankly_symbol(symbol)
+            margin = MarginType.ISOLATED \
+                if position['isolated'] else MarginType.CROSSED
+            positions[symbol] = utils.AttributeDict({
+                'symbol': symbol,
+                'base_asset': utils.get_base_asset(symbol),
+                'quote_asset': utils.get_quote_asset(symbol),
+                'size': size,
+                'position': PositionMode(position['positionSide'].lower()),
+                'entry_price': float(position['entryPrice']),
+                'contract_type': ContractType.PERPETUAL,
+                'leverage': float(position['leverage']),
+                'margin_type': margin,
+                'unrealized_pnl': float(position['unrealizedProfit']),
+                'exchange_specific': position
+            })
+
+        if filter:
+            return positions.get(filter, None)
+        return positions
+
+    @staticmethod
+    def to_order_status(status: str):
+        if status == 'NEW':
+            return OrderStatus.OPEN
+        elif status == 'PARTIALLY_FILLED':
+            return OrderStatus.PARTIALLY_FILLED
+        elif status == 'FILLED':
+            return OrderStatus.FILLED
+        elif status in ('CANCELED', 'REJECTED'):
+            return OrderStatus.CANCELED
+        elif status in 'EXPIRED':
+            return OrderStatus.EXPIRED
+        raise ValueError(f'invalid order status: {status}')
+
+    def parse_order_response(self, response: dict) -> FuturesOrder:
+        return FuturesOrder(symbol=self.to_blankly_symbol(response['symbol']),
+                            id=int(response['orderId']),
+                            status=self.to_order_status(response['status']),
+                            size=float(response['origQty']),
+                            type=OrderType(response['type'].lower()),
+                            contract_type=ContractType.PERPETUAL,
+                            side=Side(response['side'].lower()),
+                            position=PositionMode(
+                                response['positionSide'].lower()),
+                            time_in_force=TimeInForce(response['timeInForce']),
+                            price=float(response['cumQuote']),
+                            limit_price=float(response['price']),
+                            response=response,
+                            interface=self)
+
+    @utils.order_protection
+    def market_order(self,
+                     symbol: str,
+                     side: Side,
+                     size: float,
+                     position: PositionMode = PositionMode.BOTH,
+                     reduce_only: bool = False) -> FuturesOrder:
+        symbol = self.to_exchange_symbol(symbol)
+        params = {
+            'type': 'MARKET',
+            'symbol': symbol,
+            'side': side.upper(),
+            'quantity': size,
+            'positionSide': position.upper(),
+            'reduceOnly': reduce_only
+        }
+        response = self.calls.futures_create_order(**params)
+
+        return self.parse_order_response(response)
+
+    @utils.order_protection
+    def limit_order(
+            self,
+            symbol: str,
+            side: Side,
+            price: float,
+            size: float,
+            position: PositionMode = PositionMode.BOTH,
+            reduce_only: bool = False,
+            time_in_force: TimeInForce = TimeInForce.GTC) -> FuturesOrder:
+        symbol = self.to_exchange_symbol(symbol)
+        params = {
+            'type': 'LIMIT',
+            'symbol': symbol,
+            'side': side.upper(),
+            'price': price,
+            'quantity': size,
+            'positionSide': position.upper(),
+            'reduceOnly': reduce_only,
+            'timeInForce': time_in_force.value
+        }
+        response = self.calls.futures_create_order(**params)
+
+        return self.parse_order_response(response)
+
+    def take_profit(
+            self,
+            symbol: str,
+            side: Side,
+            price: float,
+            size: float,
+            position: PositionMode = PositionMode.BOTH,
+            time_in_force: TimeInForce = TimeInForce.GTC) -> FuturesOrder:
+        symbol = self.to_exchange_symbol(symbol)
+        params = {
+            'type': 'TAKE_PROFIT',
+            'symbol': symbol,
+            'side': side.upper(),
+            'price': price,
+            'stopPrice': price,  # TODO allow set stop price
+            'quantity': size,
+            'positionSide': position.upper(),
+            'timeInForce': time_in_force.value
+        }
+        response = self.calls.futures_create_order(**params)
+
+        return self.parse_order_response(response)
+
+    @utils.order_protection
+    def stop_loss(
+            self,
+            symbol: str,
+            side: Side,
+            price: float,
+            size: float,
+            position: PositionMode = PositionMode.BOTH,
+            time_in_force: TimeInForce = TimeInForce.GTC) -> FuturesOrder:
+        symbol = self.to_exchange_symbol(symbol)
+        params = {
+            'type': 'STOP',
+            'symbol': symbol,
+            'side': side.upper(),
+            'price': price,
+            'stopPrice': price,  # TODO allow set stop price
+            'quantity': size,
+            'positionSide': position.upper(),
+            'timeInForce': time_in_force.value
+        }
+        response = self.calls.futures_create_order(**params)
+
+        return self.parse_order_response(response)
+
+    @utils.order_protection
+    def cancel_order(self, symbol: str, order_id: int) -> FuturesOrder:
+        symbol = self.to_exchange_symbol(symbol)
+        # this library method is broken for some reason 2021-02-25
+        res = self.calls.futures_cancel_order(symbol=symbol, orderId=order_id)
+        return self.parse_order_response(res)
+
+    def get_open_orders(self, symbol: str = None) -> list:
+        if symbol:
+            symbol = self.to_exchange_symbol(symbol)
+            orders = self.calls.futures_get_open_orders(symbol=symbol)
+        else:
+            orders = self.calls.futures_get_open_orders()
+        return [self.parse_order_response(order) for order in orders]
+
+    def get_order(self, symbol: str, order_id: int) -> FuturesOrder:
+        symbol = self.to_exchange_symbol(symbol)
+        res = self.calls.futures_get_order(symbol=symbol, orderId=order_id)
+        return self.parse_order_response(res)
+
+    def get_price(self, symbol: str) -> float:
+        symbol = self.to_exchange_symbol(symbol)
+        return float(self.calls.futures_mark_price(symbol=symbol)['markPrice'])
+
+    def get_fees(self) -> utils.AttributeDict:
+        # https://www.binance.com/en/blog/futures/trade-crypto-futures-how-much-does-it-cost-421499824684902239
+        tier = int(self.calls.futures_account()['feeTier'])
+        maker, taker = BINANCE_FUTURES_FEES[tier]
+        return utils.AttributeDict({'maker': maker, 'taker': taker})
+
+    @property
+    def account(self) -> utils.AttributeDict:
+        return self.get_account()
+
+    @property
+    def orders(self) -> list:
+        return self.get_open_orders()
+
+    def get_product_history(self, symbol, epoch_start, epoch_stop,
+                            resolution) -> pd.DataFrame:
+        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))
+
+        # TODO dedup this from (non-futures) binance interface
+        granularities = {
+            60: "1m",
+            180: "3m",
+            300: "5m",
+            900: "15m",
+            1800: "30m",
+            3600: "1h",
+            7200: "2h",
+            14400: "4h",
+            21600: "6h",
+            28800: "8h",
+            43200: "12h",
+            86400: "1d",
+            259200: "3d",
+            604800: "1w",
+            2592000: "1M"
+        }
+
+        if resolution not in granularities:
+            utils.info_print(
+                "Granularity is not an accepted granularity...rounding to nearest valid value."
+            )
+            resolution = min(granularities,
+                             key=lambda gran: abs(resolution - gran))
+        gran_string = granularities[resolution]
+
+        # Figure out how many points are needed
+        need = int((epoch_stop - epoch_start) / resolution)
+        initial_need = need
+        window_open = epoch_start
+        history = []
+
+        # Convert coin id to binance coin
+        symbol = self.to_exchange_symbol(symbol)
+        while need > 1000:
+            # Close is always 300 points ahead
+            window_close = int(window_open + 1000 * resolution)
+            history = history + self.calls.futures_klines(
+                symbol=symbol,
+                startTime=window_open * 1000,
+                endTime=window_close * 1000,
+                interval=gran_string,
+                limit=1000)
+
+            window_open = window_close
+            need -= 1000
+            time.sleep(.2)
+            utils.update_progress((initial_need - need) / initial_need)
+
+        # Fill the remainder
+        history_block = history + self.calls.futures_klines(
+            symbol=symbol,
+            startTime=window_open * 1000,
+            endTime=epoch_stop * 1000,
+            interval=gran_string,
+            limit=1000)
+
+        data_frame = pd.DataFrame(history_block,
+                                  columns=[
+                                      'time', 'open', 'high', 'low', 'close',
+                                      'volume', 'close time',
+                                      'quote asset volume', 'number of trades',
+                                      'taker buy base asset volume',
+                                      'taker buy quote asset volume', 'ignore'
+                                  ],
+                                  dtype=None)
+        # Clear the ignore column, why is that there binance?
+        del data_frame['ignore']
+
+        # Want them in this order: ['time (epoch)', 'low', 'high', 'open', 'close', 'volume']
+
+        # Time is so big it has to be cast separately for windows
+        data_frame['time'] = data_frame['time'].div(1000).astype(int)
+
+        # Cast dataframe
+        data_frame = data_frame.astype({
+            'open': float,
+            'high': float,
+            'low': float,
+            'close': float,
+            'volume': float,
+            'close time': int,
+            'quote asset volume': float,
+            'number of trades': int,
+            'taker buy base asset volume': float,
+            'taker buy quote asset volume': float
+        })
+
+        # Convert time to seconds
+        return data_frame.reindex(
+            columns=['time', 'low', 'high', 'open', 'close', 'volume'])
+
+    def get_funding_rate_history(self, symbol: str, epoch_start: int,
+                                 epoch_stop: int) -> list:
+        symbol = self.to_exchange_symbol(symbol)
+        limit = 1000
+        history = []
+        window_start = epoch_start
+        window_end = epoch_stop
+
+        # UNCOMMENT FOR WALRUS
+        # while response := self.calls.futures_funding_rate(
+        #         symbol=symbol,
+        #         startTime=window_start * 1000,
+        #         endTime=window_end * 1000,
+        #         limit=limit):
+
+        # WARNING! non-walrus code ahead:
+        response = True
+        while response:
+            response = self.calls.futures_funding_rate(
+                symbol=symbol,
+                startTime=window_start * 1000,
+                endTime=window_end * 1000,
+                limit=limit)
+            # very stinky ^^
+
+            history.extend({
+                'rate': float(e['fundingRate']),
+                'time': e['fundingTime'] // 1000
+            } for e in response)
+
+            if history:
+                window_start = history[-1]['time'] + 1
+                window_end = min(dt.now().timestamp(), epoch_stop)
+
+        return history
+
+    def get_funding_rate_resolution(self) -> int:
+        return time_builder.build_hour() * 8  # 8 hours
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
index c5b754ef..015488cf 100644
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
@@ -63,7 +63,7 @@ def get_exchange_state(self):
         Exchange state is the external properties for the exchange block
         """
         # TODO Populate this with useful information
-        return self.interface.get_fees()
+        return self.interface.get_products()
 
     def get_direct_calls(self) -> CoinbaseProAPI:
         return self.calls
@@ -73,7 +73,7 @@ def get_direct_calls(self) -> CoinbaseProAPI:
     """
 
     def get_indicators(self):
-        return self.calls.get_fees()
+        return self.calls.get_products()
 
     def filter_relevant(self, dict):
         """
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
index a32c6daa..f3098ebd 100644
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
@@ -35,7 +35,8 @@ def __init__(self, exchange_name, authenticated_api):
 
     def init_exchange(self):
         # This is purely an authentication check which can be disabled in settings
-        fees = self.calls.get_fees()
+        # Coinbase is symbol agnostic so passing a blank here is okay
+        fees = self.calls.get_fees(None)
         try:
             if fees['message'] == "Invalid API Key":
                 raise LookupError("Invalid API Key - are you trying to use your normal exchange keys "
@@ -79,6 +80,7 @@ def get_products(self):
             products[i] = utils.isolate_specific(needed, products[i])
         return products
 
+    @utils.enforce_base_asset
     def get_account(self, symbol=None) -> utils.AttributeDict:
         """
         Get all currencies in an account, or sort by symbol/account_id
@@ -403,7 +405,7 @@ def get_order(self, symbol, order_id) -> dict:
     binance: get_trade_fee
     """
 
-    def get_fees(self) -> dict:
+    def get_fees(self, symbol) -> dict:
         needed = self.needed['get_fees']
         """
         {
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py
index b725e789..3b328a82 100644
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket.py
@@ -52,17 +52,18 @@ def create_ticker_connection(id, url, channel):
 
 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"):
+                 pre_event_callback=None, initially_stopped=False, websocket_url="wss://ws-feed.pro.coinbase.com",
+                 **kwargs):
         """
         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.
+            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)
+        super().__init__(symbol, stream, log, log_message, websocket_url, pre_event_callback, kwargs)
 
         self.__pre_event_callback_filled = False
 
@@ -177,7 +178,7 @@ def on_message(self, ws, message):
 
         try:
             for i in self.callbacks:
-                i(interface_message)
+                i(interface_message, **self.kwargs)
         except Exception:
             traceback.print_exc()
 
diff --git a/blankly/exchanges/interfaces/exchange_interface.py b/blankly/exchanges/interfaces/exchange_interface.py
index bd0bb9d3..6761924e 100644
--- a/blankly/exchanges/interfaces/exchange_interface.py
+++ b/blankly/exchanges/interfaces/exchange_interface.py
@@ -17,19 +17,9 @@
 """
 
 import abc
-import time
-import warnings
-from datetime import datetime as dt
-from typing import Union
-from collections import deque
-
-import numpy
-import pandas as pd
-from dateutil import parser
 
 import blankly.utils.utils as utils
 from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface
-from blankly.utils.time_builder import time_interval_to_seconds
 
 
 # TODO: need to add a cancel all orders function
@@ -171,109 +161,6 @@ def cash(self):
         using_setting = self.user_preferences['settings'][self.exchange_name]['cash']
         return self.get_account(using_setting)['available']
 
-    def history(self,
-                symbol: str,
-                to: Union[str, int] = 200,
-                resolution: Union[str, float] = '1d',
-                start_date: Union[str, dt, float] = None,
-                end_date: Union[str, dt, float] = None,
-                return_as: str = 'df'):
-
-        is_backtesting = self.is_backtesting()
-        if is_backtesting is not None and end_date is None:
-            # is_backtesting can only return a non None value if a function overrides the is_backtesting function
-            #  and says its backtesting by returning a valid time
-            end_date = is_backtesting
-
-        if start_date is not None and end_date is not None:
-            to = None
-
-        to_present = False
-        if end_date is None:
-            to_present = True
-
-        # convert resolution into epoch seconds
-        resolution_seconds = int(time_interval_to_seconds(resolution))
-
-        if end_date is None:
-            # Figure out the next point and then subtract to the last stamp
-            most_recent_valid_resolution = utils.ceil_date(dt.now(),
-                                                           seconds=resolution_seconds).timestamp() - resolution_seconds
-            # Binance is nice enough to update OHLCV data, so we have to exclude that by subtracting a resolution
-            epoch_stop = most_recent_valid_resolution - resolution_seconds
-            count_from = most_recent_valid_resolution
-        else:
-            if isinstance(end_date, str):
-                parsed_date = parser.parse(end_date)
-            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
-            valid_time_in_past = utils.ceil_date(parsed_date,
-                                                 seconds=resolution_seconds).timestamp() - resolution_seconds
-            epoch_stop = valid_time_in_past - resolution_seconds
-            count_from = valid_time_in_past
-
-        if start_date is None and end_date is None:
-            if isinstance(to, int):
-                # use number of points to calculate the start epoch
-                epoch_start = count_from - (to * resolution_seconds)
-            else:
-                epoch_start = count_from - time_interval_to_seconds(to)
-        elif start_date is None and end_date is not None:
-            if isinstance(to, int):
-                epoch_start = count_from - (to * resolution_seconds)
-            else:
-                epoch_start = count_from - time_interval_to_seconds(to)
-        else:
-            epoch_start = utils.convert_input_to_epoch(start_date)
-
-        response = self.overridden_history(symbol, epoch_start, epoch_stop, resolution_seconds, to=to,)
-
-        # Add a check to make sure that coinbase pro has updated
-        if to_present and self.get_exchange_type() == "coinbase_pro":
-            data_append = None
-            tries = 0
-            while True:
-                if data_append is None:
-                    # We can continue if this is valid
-                    if response['time'].iloc[-1] == epoch_stop:
-                        break
-                else:
-                    if data_append[0]['time'] == epoch_stop:
-                        break
-                time.sleep(.5)
-                tries += 1
-                if tries > 5:
-                    # Admit failure and return
-                    warnings.warn("Exchange failed to provide updated data within the timeout.")
-                    return self.cast_type(response, return_as)
-                try:
-                    data_append = [self.get_product_history(symbol,
-                                                            epoch_stop - resolution_seconds,
-                                                            epoch_stop,
-                                                            resolution_seconds).iloc[-1].to_dict()]
-                    data_append[0]['time'] = int(data_append[0]['time'])
-                except IndexError:
-                    # If there is no recent data on the exchange this will be an empty dataframe.
-                    # This happens for low volume
-                    utils.info_print("Most recent bar at this resolution does not yet exist - skipping.")
-                    break
-
-            response = response.append(data_append, ignore_index=True)
-
-        # Determine the deque length - we really should use this generally
-        if isinstance(to, int):
-            point_count = to
-        else:
-            point_count = (epoch_stop-epoch_start)/resolution_seconds + 1
-        # response.index = pd.to_datetime(response['time'], unit='s')
-        return self.cast_type(response, return_as, point_count)
-
-    def overridden_history(self, symbol, epoch_start, epoch_stop, resolution, **kwargs) -> pd.DataFrame:
-        return self.get_product_history(symbol, epoch_start, epoch_stop, resolution)
-
     def is_backtesting(self):
         """
         Overridden by interfaces which have a valid backtesting boolean value
@@ -300,22 +187,6 @@ def evaluate_multiples(valid_resolutions: list, resolution_seconds: float):
 
         return found_multiple, row_divisor
 
-    @staticmethod
-    def cast_type(response: pd.DataFrame, return_as: str, point_count=None):
-        if return_as != 'df' and return_as != 'deque':
-            return response.to_dict(return_as)
-        elif return_as == 'deque':
-            # Create a deque object that has the same length
-            response = response.to_dict('list')
-            for i in response.keys():
-                response[i] = deque(response[i], point_count)
-            return response
-        elif return_as == 'df':
-            return response
-        else:
-            utils.info_print(f"Return type {return_as} is not supported.")
-        return response
-
     def get_account(self, symbol=None):
         """
         This method is a helper that allows the get_account functions to assume the base asset is being passed in
@@ -331,10 +202,6 @@ def get_account(self, symbol=None):
 
         return symbol
 
-    @abc.abstractmethod
-    def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
-        pass
-
     def choose_order_specificity(self, order_type):
         # This lower should not be necessary if everything is truly homogeneous
         order_type = order_type.lower()
diff --git a/blankly/exchanges/interfaces/ftx/ftx.py b/blankly/exchanges/interfaces/ftx/ftx.py
index 89361aa7..df7fa152 100644
--- a/blankly/exchanges/interfaces/ftx/ftx.py
+++ b/blankly/exchanges/interfaces/ftx/ftx.py
@@ -35,7 +35,8 @@ def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=Non
         keys = auth.keys
         sandbox = super().evaluate_sandbox(auth)
 
-        calls = FTXAPI(keys['API_KEY'], keys['API_SECRET'])
+        calls = FTXAPI(keys['API_KEY'], keys['API_SECRET'], tld=self.preferences["settings"]["ftx"]["ftx_tld"],
+                       _subaccount_name=keys.get('SUBACCOUNT', None))
 
         # Always finish the method with this function
         super().construct_interface_and_cache(calls)
@@ -63,7 +64,7 @@ def get_exchange_state(self):
         Exchange state is the external properties for the exchange block
         """
         # TODO Populate this with useful information
-        return self.interface.get_fees()
+        return self.interface.get_products()
 
     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
index 1c0e3075..dfd655d2 100644
--- a/blankly/exchanges/interfaces/ftx/ftx_api.py
+++ b/blankly/exchanges/interfaces/ftx/ftx_api.py
@@ -26,18 +26,23 @@
 
 
 class FTXAPI:
-    _API_URL = "https://ftx.us/api/"
+    API_URL = "https://ftx.{}/api/"
 
     # no option to instantiate with sandbox mode, unlike every other exchange
-    def __init__(self, api_key, api_secret, _subaccount_name=None):
+    def __init__(self, api_key, api_secret, tld: str = 'us', _subaccount_name=None):
 
         self._ftx_session = requests.Session()
+        self._api_url = self.API_URL.format(tld)
         self._api_key = api_key
         self._api_secret = api_secret
         self._subaccount_name = _subaccount_name
 
+        self._header_prefix = 'FTX'
+        if tld == 'us':
+            self._header_prefix += 'US'
+
     def _signed_request(self, method: str, path: str, **kwargs):
-        request = requests.Request(method, self._API_URL + path, **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)
@@ -52,12 +57,12 @@ def _get_signature(self, request: requests.Request):
 
         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)
+        request.headers[f'{self._header_prefix}-KEY'] = self._api_key
+        request.headers[f'{self._header_prefix}-SIGN'] = signature
+        request.headers[f'{self._header_prefix}-TS'] = str(ts)
 
         if self._subaccount_name:
-            request.headers['FTXUS-SUBACCOUNT'] = urllib.parse.quote(self._subaccount_name)
+            request.headers[f'{self._header_prefix}-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)
@@ -87,6 +92,9 @@ def list_markets(self) -> List[dict]:
 
         return self._signed_get('markets')
 
+    def change_account_leverage(self, leverage: int) -> dict:
+        return self._signed_post('account/leverage', {'leverage': leverage})
+
     def get_market(self, symbol: str) -> dict:
 
         # symbol format must be BTC/USD instead of BTC-USD
@@ -123,12 +131,25 @@ def get_fills(self) -> List[dict]:
     def get_balances(self) -> List[dict]:
         return self._signed_get('wallet/balances')
 
+    def get_coins(self) -> List[dict]:
+        return self._signed_get('wallet/coins')
+
+    def get_future(self, future) -> dict:
+        return self._signed_get(f'futures/{future}')
+
     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_funding_rates(self, start_time: int, end_time: int, symbol: str):
+        return self._signed_get('funding_rates', {
+            'start_time': start_time,
+            'end_time': end_time,
+            'future': symbol
+        })
+
     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)
diff --git a/blankly/exchanges/interfaces/ftx/ftx_interface.py b/blankly/exchanges/interfaces/ftx/ftx_interface.py
index 6efa273c..621e7227 100644
--- a/blankly/exchanges/interfaces/ftx/ftx_interface.py
+++ b/blankly/exchanges/interfaces/ftx/ftx_interface.py
@@ -117,6 +117,7 @@ def get_products(self) -> list:
 
         return end_products
 
+    @utils.enforce_base_asset
     def get_account(self, symbol: str = None) -> utils.AttributeDict:
         """
         Get all assets in an account, or sort by symbol/account_id
@@ -404,7 +405,7 @@ def get_order(self, symbol: str, order_id: str) -> dict:
         response = utils.isolate_specific(needed, response)
         return response
 
-    def get_fees(self) -> dict:
+    def get_fees(self, symbol) -> dict:
         """
         Get market fees
 
diff --git a/blankly/exchanges/interfaces/ftx/ftx_websocket.py b/blankly/exchanges/interfaces/ftx/ftx_websocket.py
index 41d9dbfe..22c3b61f 100644
--- a/blankly/exchanges/interfaces/ftx/ftx_websocket.py
+++ b/blankly/exchanges/interfaces/ftx/ftx_websocket.py
@@ -18,47 +18,28 @@
 
 import json
 import traceback
-
+import blankly
+from blankly.utils.utils import epoch_from_iso8601, to_blankly_symbol
 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/"):
+                 pre_event_callback=None, initially_stopped=False, websocket_url="wss://ftx.com/ws/", **kwargs):
         """
         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.
+            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)
+        super().__init__(symbol, stream, log, log_message, websocket_url, pre_event_callback, kwargs)
+
+        self.__pre_event_callback_filled = False
 
         # Start the websocket
         if not initially_stopped:
@@ -85,27 +66,51 @@ def on_message(self, ws, message):
             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)
+        if (self.pre_event_callback is not None) and (not self.__pre_event_callback_filled) and \
+                (self.stream == "orderbook"):
+            if received_dict['type'] == 'partial':
+                try:
+                    self.pre_event_callback(received_dict)
+                except Exception:
+                    traceback.print_exc()
 
-            self.log_response(self.__logging_callback, received)
+            self.__pre_event_callback_filled = True
+            return
 
-            # 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
+        if self.stream == "orderbook":
+            interface_response = self.__interface_callback(received_dict['data'])
+            self.most_recent_time = interface_response["time"]
+            self.time_feed.append(self.most_recent_time)
+            self.most_recent_tick = interface_response
 
             try:
+                interface_response['symbol'] = received_dict['market']
                 for i in self.callbacks:
-                    i(interface_message)
+                    i(interface_response, **self.kwargs)
             except Exception as e:
                 info_print(e)
                 traceback.print_exc()
-
-            self.message_count += 1
+        elif self.stream == "trades":
+            for received in received_dict['data']:
+                interface_response = self.__interface_callback(received)
+                # This could be passed into the received var above which could be cleaner
+                interface_response['symbol'] = to_blankly_symbol(received_dict['market'], 'ftx')
+                self.ticker_feed.append(interface_response)
+
+                self.most_recent_time = epoch_from_iso8601(received["time"])
+                self.time_feed.append(self.most_recent_time)
+                self.most_recent_tick = received
+
+                self.log_response(self.__logging_callback, received)
+
+                try:
+                    for i in self.callbacks:
+                        i(interface_response)
+                except Exception as e:
+                    info_print(e)
+                    traceback.print_exc()
+
+        self.message_count += 1
 
     def on_error(self, ws, error):
         info_print(error)
diff --git a/blankly/exchanges/interfaces/ftx/ftx_websocket_utils.py b/blankly/exchanges/interfaces/ftx/ftx_websocket_utils.py
index b5fa5368..c6908297 100644
--- a/blankly/exchanges/interfaces/ftx/ftx_websocket_utils.py
+++ b/blankly/exchanges/interfaces/ftx/ftx_websocket_utils.py
@@ -18,16 +18,16 @@
 
 import time
 import blankly.utils.utils as utils
-from typing import List
+from typing import List, Dict, Union, Any
 
 
 def switch_type(stream):
-    if stream == "ticker":
+    if stream == "trades":
         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":
+    elif stream == "orderbook":
         return no_callback, \
                no_callback, \
                ""
@@ -44,30 +44,16 @@ def no_callback(message):
 """
 
 
-def process_trades(response: dict) -> List[dict]:
-    list_trades: List = []
-    num_trades: int = len(response['data'])
-    trades_data: List[dict] = response['data']
+def process_trades(response: dict) -> Dict[str, Union[float, Any]]:
+    output = {
+              'trade_id': response.pop('id'),
+              'time': utils.epoch_from_iso8601(response['time']),
+              'size': response['size'],
+              'price': response['price']
+              }
 
-    needed = [
-        ["symbol", str],
-        ["price", float],
-        ["time", float],
-        ["trade_id", int],
-        ["size", float]
-    ]
+    return output
 
-    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):
diff --git a/blankly/exchanges/interfaces/ftx_futures/__init__.py b/blankly/exchanges/interfaces/ftx_futures/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/blankly/exchanges/interfaces/ftx_futures/ftx_futures.py b/blankly/exchanges/interfaces/ftx_futures/ftx_futures.py
new file mode 100644
index 00000000..82e1f771
--- /dev/null
+++ b/blankly/exchanges/interfaces/ftx_futures/ftx_futures.py
@@ -0,0 +1,43 @@
+from blankly.exchanges.auth.auth_constructor import AuthConstructor
+from blankly.exchanges.futures.futures_exchange import FuturesExchange
+from blankly.exchanges.interfaces.ftx.ftx_api import FTXAPI
+from blankly.exchanges.interfaces.ftx_futures.ftx_futures_interface import FTXFuturesInterface
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+
+
+class FTXFutures(FuturesExchange):
+
+    def __init__(self,
+                 portfolio_name=None,
+                 keys_path="keys.json",
+                 preferences_path=None):
+        super().__init__("ftx_futures", portfolio_name, preferences_path)
+
+        # Load auth from keys file
+        auth = AuthConstructor(keys_path, portfolio_name, 'ftx',
+                               ['API_KEY', 'API_SECRET', 'sandbox'])
+
+        if auth.keys['sandbox']:
+            raise Exception(
+                'FTX Futures exchange does not support sandbox mode')
+
+        tld = self.preferences["settings"]["ftx_futures"]["ftx_tld"]
+        if tld != 'com':
+            raise Exception(
+                f'FTX Futures exchange does not support .{tld} tld.')
+
+        self.__calls = FTXAPI(auth.keys['API_KEY'],
+                              auth.keys['API_SECRET'],
+                              tld=tld)
+
+        self.__interface = FTXFuturesInterface(self.exchange_type, self.calls)
+
+        self.initialize()
+
+    @property
+    def calls(self):
+        return self.__calls
+
+    @property
+    def interface(self) -> FuturesExchangeInterface:
+        return self.__interface
diff --git a/blankly/exchanges/interfaces/ftx_futures/ftx_futures_interface.py b/blankly/exchanges/interfaces/ftx_futures/ftx_futures_interface.py
new file mode 100644
index 00000000..2192a276
--- /dev/null
+++ b/blankly/exchanges/interfaces/ftx_futures/ftx_futures_interface.py
@@ -0,0 +1,335 @@
+"""
+    FTX Futures impl of FuturesExchangeInterface.
+    Copyright (C) 2022 Matias Kotlik
+
+    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 operator
+from typing import Optional
+
+from blankly.enums import MarginType, HedgeMode, Side, PositionMode, TimeInForce, ContractType, OrderStatus, OrderType
+from blankly.exchanges.interfaces.ftx.ftx_api import FTXAPI
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+from blankly.exchanges.orders.futures.futures_order import FuturesOrder
+from blankly.utils import utils, time_builder
+import datetime
+import math
+
+
+class FTXFuturesInterface(FuturesExchangeInterface):
+    calls: FTXAPI
+
+    @staticmethod
+    def to_exchange_symbol(symbol: str):
+        base_asset, quote_asset = symbol.split('-')
+        if quote_asset != 'USD':
+            raise ValueError('invalid symbol')
+        return base_asset + '-PERP'  # only perpetual contracts right now
+
+    @staticmethod
+    def to_blankly_symbol(symbol: str):
+        base_asset, contract_type = symbol.split('-', 1)
+        if contract_type != 'PERP':
+            raise ValueError(
+                'invalid symbol -- blankly only supports perpetual contracts')
+        return base_asset + '-USD'
+
+    @staticmethod
+    def parse_timestamp(time: str) -> int:
+        return int(datetime.datetime.fromisoformat(time).timestamp())
+
+    @staticmethod
+    def to_order_status(status: str, cancel: bool = False) -> OrderStatus:
+        if status in ('new', 'open'):
+            return OrderStatus.OPEN
+        elif status == 'closed':
+            if cancel:
+                return OrderStatus.CANCELED
+            else:
+                return OrderStatus.FILLED
+        raise ValueError(f'invalid order status: {status}')
+
+    @staticmethod
+    def to_order_type(type: str) -> OrderType:
+        if type == 'market':
+            return OrderType.MARKET
+        elif type == 'limit':
+            return OrderType.LIMIT
+        elif type == 'takeProfit':
+            return OrderType.TAKE_PROFIT
+        elif type == 'stop':
+            return OrderType.STOP
+        raise ValueError(f'invalid order type: {type}')
+
+    def parse_order_response(self,
+                             response: dict,
+                             cancel: bool = False) -> FuturesOrder:
+        return FuturesOrder(
+            symbol=response['future'],
+            id=int(response['id']),
+            status=self.to_order_status(response['status'], cancel),
+            size=float(response['size']),
+            type=self.to_order_type(response['type']),
+            contract_type=ContractType.PERPETUAL,
+            side=Side(response['side']),
+            position=PositionMode.BOTH,
+            price=float(response['avgFillPrice']),
+            limit_price=float(response['price']),
+            time_in_force=TimeInForce.IOC
+            if response['ioc'] else TimeInForce.GTC,
+            response=response,
+            interface=self)
+
+    def init_exchange(self):
+        # will throw exception if our api key is stinky
+        self.calls.get_account_info()
+
+    def get_products(self, filter: str = None) -> dict:
+        res = self.calls.list_futures()
+        products = {}
+        for prod in res:
+            if '-PERP' not in prod['name']:
+                continue  # only perpetual contracts for now
+            symbol = prod['underlying'] + '-USD'
+            products[symbol] = utils.AttributeDict({
+                'symbol': symbol,
+                'base_asset': prod['underlying'],
+                'quote_asset': 'USD',
+                'contract_type': ContractType.PERPETUAL,
+                'price_precision': self.increment_to_precision(
+                    prod['priceIncrement']),
+                'size_precision': self.increment_to_precision(
+                    prod['sizeIncrement']),
+                'exchange_specific': prod
+            })
+        if filter:
+            return products[filter]
+        return products
+
+    def get_account(self, filter: str = None) -> utils.AttributeDict:
+        balances = self.calls.get_balances()
+        coins = self.calls.get_coins()
+        accounts = utils.AttributeDict({
+            coin['id']: utils.AttributeDict({
+                'available': 0.0,
+                'exchange_specific': coin
+            })
+            for coin in coins
+        })
+
+        for bal in balances:
+            coin = bal['coin']
+            accounts[coin].available = float(bal['free'])
+            # merge into exchange_specific
+            accounts[coin].exchange_specific = {
+                **accounts[coin].exchange_specific,
+                **bal
+            }
+
+        if filter:
+            return accounts[filter]
+        return accounts
+
+    def get_positions(self, filter: str = None) -> Optional[dict]:
+        leverage = self.get_leverage()
+
+        res = self.calls.get_positions()
+        positions = {}
+        for position in res:
+            symbol = self.to_exchange_symbol(position['future'])
+            size = float(position['netSize'])
+            if size == 0:
+                continue
+            positions[symbol] = utils.AttributeDict({
+                'size': size,
+                'position': PositionMode.BOTH,
+                'entry_price': float(position['entryPrice']),
+                'contract_type': ContractType.PERPETUAL,
+                'leverage': leverage,
+                'margin_type': MarginType.CROSSED,
+                'unrealized_pnl': float(
+                    position['unrealizedPnl']),  # TODO not sure on this one
+                'exchange_specific': position
+            })
+
+        if filter:
+            return positions.get(filter, None)
+        return positions
+
+    def market_order(self,
+                     symbol: str,
+                     side: Side,
+                     size: float,
+                     position: PositionMode = PositionMode.BOTH,
+                     reduce_only: bool = False) -> FuturesOrder:
+        symbol = self.to_exchange_symbol(symbol)
+        if position != PositionMode.BOTH:
+            raise ValueError(
+                f'position mode {position} not supported on FTX Futures')
+        res = self.calls.place_order(symbol, side, None, size, 'market',
+                                     reduce_only)
+        return self.parse_order_response(res)
+
+    def limit_order(
+            self,
+            symbol: str,
+            side: Side,
+            price: float,
+            size: float,
+            position: PositionMode = PositionMode.BOTH,
+            reduce_only: bool = False,
+            time_in_force: TimeInForce = TimeInForce.GTC) -> FuturesOrder:
+        symbol = self.to_exchange_symbol(symbol)
+        if time_in_force == TimeInForce.GTC:
+            ioc = False
+        elif time_in_force == TimeInForce.IOC:
+            ioc = True
+        else:
+            raise ValueError(
+                f'time in force {time_in_force} not supported on FTX Futures')
+        if position != PositionMode.BOTH:
+            raise ValueError(
+                f'position mode {position} not supported on FTX Futures')
+        res = self.calls.place_order(symbol,
+                                     side,
+                                     price,
+                                     size,
+                                     'limit',
+                                     reduce_only,
+                                     ioc=ioc)
+        return self.parse_order_response(res)
+
+    def take_profit(
+            self,
+            symbol: str,
+            side: Side,
+            price: float,
+            size: float,
+            position: PositionMode = PositionMode.BOTH) -> FuturesOrder:
+        symbol = self.to_exchange_symbol(symbol)
+        if position != PositionMode.BOTH:
+            raise ValueError(
+                f'position mode {position} not supported on FTX Futures')
+        res = self.calls.place_conditional_order(symbol,
+                                                 side,
+                                                 size,
+                                                 'takeProfit',
+                                                 trigger_price=price)
+        return self.parse_order_response(res)
+
+    def stop_loss(self,
+                  symbol: str,
+                  side: Side,
+                  price: float,
+                  size: float,
+                  position: PositionMode = PositionMode.BOTH) -> FuturesOrder:
+        symbol = self.to_exchange_symbol(symbol)
+        if position != PositionMode.BOTH:
+            raise ValueError(
+                f'position mode {position} not supported on FTX Futures')
+        res = self.calls.place_conditional_order(symbol,
+                                                 side,
+                                                 size,
+                                                 'stop',
+                                                 trigger_price=price)
+        return self.parse_order_response(res)
+
+    @utils.order_protection
+    def set_hedge_mode(self, hedge_mode: HedgeMode):
+        if hedge_mode == HedgeMode.HEDGE:
+            raise Exception('HEDGE mode not supported on FTX Futures')
+        pass  # FTX only has ONEWAY mode
+
+    def get_hedge_mode(self):
+        return HedgeMode.ONEWAY
+
+    @utils.order_protection
+    def set_leverage(self, leverage: int, symbol: str = None):
+        if symbol:
+            raise Exception(
+                'FTX Futures does not allow setting leverage per symbol. Use interface.set_leverage(leverage) to set '
+                'account-wide leverage instead. ')
+        self.calls.change_account_leverage(leverage)
+
+    def get_leverage(self, symbol: str = None) -> float:
+        # doesn't matter if symbol passed or not, ftx leverage is same for all
+        return self.calls.get_account_info()['leverage']
+
+    @utils.order_protection
+    def set_margin_type(self, symbol: str, type: MarginType):
+        if type == MarginType.ISOLATED:
+            raise Exception('isolated margin not supported on FTX Futures')
+        pass
+
+    def get_margin_type(self, symbol: str):
+        return MarginType.CROSSED
+
+    def cancel_order(self, symbol: str, order_id: int) -> FuturesOrder:
+        res = self.get_order(symbol, order_id)
+        res.status = OrderStatus.CANCELED
+        self.calls.cancel_order(str(order_id))
+        return res
+
+    def get_open_orders(self, symbol: str = None) -> list:
+        return [
+            self.parse_order_response(o) for o in self.calls.get_open_orders()
+        ]
+
+    def get_order(self, symbol: str, order_id: int) -> FuturesOrder:
+        response = self.calls.get_order_by_id(str(order_id))
+        symbol = self.to_exchange_symbol(symbol)
+        if response['symbol'] != symbol:
+            raise Exception(
+                'response symbol did not match parameter -- this should never happen'
+            )
+        return self.parse_order_response(response)
+
+    def get_price(self, symbol: str) -> float:
+        symbol = self.to_exchange_symbol(symbol)
+        return float(self.calls.get_future(symbol)['mark'])
+
+    def get_funding_rate_history(self, symbol: str, epoch_start: int,
+                                 epoch_stop: int) -> list:
+        symbol = self.to_exchange_symbol(symbol)
+        # TODO dedup binance_futures_exchange maybe?
+        history = []
+        resolution = self.get_funding_rate_resolution()
+        LIMIT = 500
+        window_start = epoch_start
+        window_end = epoch_start + LIMIT * resolution
+
+        response = True
+        while response:
+            response = self.calls.get_funding_rates(window_start, window_end,
+                                                    symbol)
+
+            history.extend({
+                'rate': float(e['rate']),
+                'time': self.parse_timestamp(e['time'])
+            } for e in response)
+
+            if response:
+                window_start = window_end
+                window_end = min(epoch_stop, window_start + LIMIT * resolution)
+
+        return sorted(history, key=operator.itemgetter('time'))
+
+    def get_funding_rate_resolution(self) -> int:
+        return time_builder.build_hour()
+
+    def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
+        raise NotImplementedError
+
+
diff --git a/blankly/exchanges/interfaces/futures_exchange_interface.py b/blankly/exchanges/interfaces/futures_exchange_interface.py
new file mode 100644
index 00000000..4c95806f
--- /dev/null
+++ b/blankly/exchanges/interfaces/futures_exchange_interface.py
@@ -0,0 +1,208 @@
+"""
+    Base class for FuturesExchangeInterfaces.
+    Provides common functionality for Futures Exchanges.
+    Copyright (C) 2022 Matias Kotlik
+
+    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 typing import Union, Optional, List
+
+import numpy
+import pandas
+
+from datetime import datetime as dt
+
+from dateutil.parser import parser
+
+import blankly.utils.utils as utils
+from blankly.enums import MarginType, HedgeMode, PositionMode, OrderType, Side, TimeInForce, ContractType
+from blankly.exchanges.interfaces.abc_base_exchange_interface import ABCBaseExchangeInterface
+from blankly.exchanges.orders.futures.futures_order import FuturesOrder
+
+
+class FuturesExchangeInterface(ABCBaseExchangeInterface, abc.ABC):
+    exchange_name: str
+
+    def __init__(self,
+                 exchange_name,
+                 authenticated_api,
+                 preferences_path=None):
+        self.exchange_name = exchange_name
+        self.calls = authenticated_api
+
+        self.user_preferences = utils.load_user_preferences(preferences_path)
+
+        self.exchange_properties = None
+        self.available_currencies = {}
+
+        if self.user_preferences['settings']['test_connectivity_on_auth']:
+            self.init_exchange()
+
+    @staticmethod
+    @abc.abstractmethod
+    def to_blankly_symbol(symbol: str):
+        pass
+
+    @staticmethod
+    @abc.abstractmethod
+    def to_exchange_symbol(symbol: str):
+        pass
+
+    def get_exchange_type(self) -> str:
+        """Returns the exchange type (ex. 'binance', 'coinbase', 'alpaca')"""
+        return self.exchange_name
+
+    @abc.abstractmethod
+    def init_exchange(self):
+        """Initializes the exchange"""
+        pass
+
+    @abc.abstractmethod
+    def get_products(self, symbol: str = None) -> dict:
+        """Returns a list of all products traded on this exchange"""
+        pass
+
+    @abc.abstractmethod
+    def get_account(self, symbol: str = None) -> utils.AttributeDict:
+        """Returns account information, or information for only one `symbol` if one is given."""
+        pass
+
+    # TODO this metohd name might need to change to get_position ?
+    @abc.abstractmethod
+    def get_positions(self, symbol: str = None) -> Optional[dict]:
+        """Returns position information, or information for only one `symbol` if one is given"""
+        pass
+
+    @abc.abstractmethod
+    def market_order(self,
+                     symbol: str,
+                     side: Side,
+                     size: float,
+                     position: PositionMode = None,
+                     reduce_only: bool = None) -> FuturesOrder:
+        """Places a market order"""
+        pass
+
+    @abc.abstractmethod
+    def limit_order(self,
+                    symbol: str,
+                    side: Side,
+                    price: float,
+                    size: float,
+                    position: PositionMode = None,
+                    reduce_only: bool = None,
+                    time_in_force: TimeInForce = None) -> FuturesOrder:
+        """Places a limit order"""
+        pass
+
+    @abc.abstractmethod
+    def take_profit(self,
+                    symbol: str,
+                    side: Side,
+                    price: float,
+                    size: float,
+                    position: PositionMode = None) -> FuturesOrder:
+        """Place a take-profit order for a position"""
+        pass
+
+    @abc.abstractmethod
+    def stop_loss(self,
+                  symbol: str,
+                  side: Side,
+                  price: float,
+                  size: float,
+                  position: PositionMode = None) -> FuturesOrder:
+        """Place a stop-loss order for a position"""
+        pass
+
+    @abc.abstractmethod
+    def set_hedge_mode(self, hedge_mode: HedgeMode):
+        pass
+
+    @abc.abstractmethod
+    def get_hedge_mode(self):
+        pass
+
+    @abc.abstractmethod
+    def set_leverage(self, leverage: int, symbol: str = None):
+        pass
+
+    @abc.abstractmethod
+    def get_leverage(self, symbol: str = None) -> float:
+        pass
+
+    @abc.abstractmethod
+    def set_margin_type(self, symbol: str, type: MarginType):
+        pass
+
+    @abc.abstractmethod
+    def get_margin_type(self, symbol: str):
+        pass
+
+    @abc.abstractmethod
+    def cancel_order(self, symbol: str, order_id: int) -> FuturesOrder:
+        """Cancels an order"""
+        pass
+
+    @abc.abstractmethod
+    def get_open_orders(self, symbol: str = None) -> List[FuturesOrder]:
+        """Returns all currently open orders, filtered by `symbol` if one is provided."""
+        pass
+
+    @abc.abstractmethod
+    def get_order(self, symbol: str, order_id: int) -> FuturesOrder:
+        """Returns information for the order corresponding to `order_id`"""
+        pass
+
+    @abc.abstractmethod
+    def get_price(self, symbol: str) -> float:
+        """Returns the current price of an asset"""
+        pass
+
+    @property
+    def account(self) -> utils.AttributeDict:
+        """Account information"""
+        return self.get_account()
+
+    @property
+    def positions(self) -> utils.AttributeDict:
+        """Position information"""
+        return self.get_positions()
+
+    @property
+    def orders(self) -> list:
+        """All currently open orders"""
+        return self.get_open_orders()
+
+    @property
+    def cash(self) -> float:
+        """The amount of cash in a portfolio. The currency for 'cash' is set in settings.json"""
+        using_setting = self.user_preferences['settings'][
+            self.exchange_name]['cash']
+        return self.get_account(using_setting)['available']
+
+    @abc.abstractmethod
+    def get_funding_rate_history(self, symbol: str, epoch_start: int,
+                                 epoch_stop: int) -> list:
+        """
+        Get the funding rate history between `epoch_start` and `epoch_end`.
+        Returns a list of {'rate': int, 'time': int}
+        """
+        pass
+
+    @abc.abstractmethod
+    def get_funding_rate_resolution(self) -> int:
+        pass
diff --git a/blankly/exchanges/interfaces/keyless/keyless.py b/blankly/exchanges/interfaces/keyless/keyless.py
index c2bf88c2..ab16d413 100644
--- a/blankly/exchanges/interfaces/keyless/keyless.py
+++ b/blankly/exchanges/interfaces/keyless/keyless.py
@@ -24,12 +24,12 @@
 
 
 class KeylessExchange(Exchange):
-    def __init__(self, dataset_path: str, initial_account_values: dict, portfolio_name=None, settings_path=None):
+    def __init__(self, maker_fee=0, taker_fee=0, portfolio_name=None, settings_path=None):
         Exchange.__init__(self, "keyless", portfolio_name, settings_path)
 
-        self.calls = KeylessAPI(dataset_path)
+        self.calls = KeylessAPI(maker_fee, taker_fee)
 
-        self.interface = PaperTradeInterface(self.calls, initial_account_values=initial_account_values)
+        self.interface = PaperTradeInterface(self.calls)
 
         # This one must be exported manually
         blankly.reporter.export_used_exchange("keyless")
@@ -51,7 +51,7 @@ def get_exchange_state(self):
         Exchange state is the external properties for the exchange block
         """
         # TODO Populate this with useful information
-        return self.interface.get_fees()
+        return self.interface.get_products()
 
     def get_direct_calls(self) -> ABCExchangeInterface:
         return self.calls
diff --git a/blankly/exchanges/interfaces/keyless/keyless_api.py b/blankly/exchanges/interfaces/keyless/keyless_api.py
index f5f8791a..1bc6aa48 100644
--- a/blankly/exchanges/interfaces/keyless/keyless_api.py
+++ b/blankly/exchanges/interfaces/keyless/keyless_api.py
@@ -17,33 +17,26 @@
 """
 
 
-import pandas as pd
 from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
-from blankly.utils.utils import AttributeDict, get_base_asset, get_quote_asset, add_all_products
+from blankly.utils.utils import AttributeDict, get_base_asset, get_quote_asset
 
 
 # This just happens to also inherit from the exchange interface
 class KeylessAPI(ExchangeInterface):
-    def __init__(self, dataset):
+    def __init__(self, maker_fee, taker_fee):
 
-        self.__dataset: pd.DataFrame = pd.read_csv(dataset)
+        self.__products = None
+        self.__accounts = None
 
-        self.__products = []
-        self.__accounts = {}
+        self.fees = {
+            'maker_fee_rate': maker_fee,
+            'taker_fee_rate': taker_fee
+        }
 
         super().__init__('keyless', self)
 
     def init_exchange(self):
-        for i in self.__dataset.columns:
-            if i == "time":
-                continue
-            else:
-                self.__products.append({
-                    'symbol': i
-                })
-
-        # Now pull out the single assets
-        self.__accounts = add_all_products({}, self.__products)
+        pass
 
     def __invalid_live(self):
         raise RuntimeError("Cannot use a keyless exchange in live trading. Please insert keys and begin using exchanges"
@@ -56,28 +49,10 @@ def get_account(self, symbol: str = None) -> AttributeDict:
         return AttributeDict(self.__accounts)
 
     def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
-        # Ignore resolution because the backtest engine will deal with that for us
-        symbol_series = self.__dataset[symbol]
-        time_series = self.__dataset['time']
-
-        symbol_series = symbol_series[time_series >= epoch_start]
-        symbol_series = symbol_series[time_series <= epoch_stop]
-
-        time_series = time_series[time_series >= epoch_start]
-        time_series = time_series[time_series <= epoch_stop]
-
-        df = pd.DataFrame({
-            'open': symbol_series,
-            'high': symbol_series,
-            'low': symbol_series,
-            'close': symbol_series,
-            'volume': symbol_series,
-            'time': time_series
-        })
-        return df
+        self.__invalid_live()
 
-    def get_products(self) -> list:
-        return self.__products
+    def get_products(self):
+        self.__invalid_live()
 
     def market_order(self, symbol: str, side: str, size: float):
         self.__invalid_live()
@@ -94,11 +69,8 @@ def get_open_orders(self, symbol: str = None):
     def get_order(self, symbol: str, order_id: str):
         self.__invalid_live()
 
-    def get_fees(self) -> dict:
-        return {
-            'maker_fee_rate': .005,
-            'taker_fee_rate': .005
-        }
+    def get_fees(self, symbol) -> dict:
+        return self.fees
 
     def get_order_filter(self, symbol: str) -> dict:
         return {
diff --git a/blankly/exchanges/interfaces/kucoin/kucoin.py b/blankly/exchanges/interfaces/kucoin/kucoin.py
index ea504f06..0ec487d5 100644
--- a/blankly/exchanges/interfaces/kucoin/kucoin.py
+++ b/blankly/exchanges/interfaces/kucoin/kucoin.py
@@ -61,7 +61,7 @@ def get_exchange_state(self):
         Exchange state is the external properties for the exchange block
         """
         # TODO Populate this with useful information
-        return self.interface.get_fees()
+        return self.interface.get_products()
 
     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
index 5d20fb11..91ed8a74 100644
--- a/blankly/exchanges/interfaces/kucoin/kucoin_interface.py
+++ b/blankly/exchanges/interfaces/kucoin/kucoin_interface.py
@@ -16,7 +16,6 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-
 import time
 
 import pandas as pd
@@ -45,17 +44,34 @@ def __init__(self, exchange_name, authenticated_api):
         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 \'sandbox\' setting "
-                                  "in your keys.json or check if the keys were input correctly into your "
+                                  "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
 
+    @staticmethod
+    def __correct_api_call(response):
+        if isinstance(response, dict):
+            if 'code' in response:
+                if response['code'] == "200000":
+                    return response['data']
+                else:
+                    try:
+                        raise APIException(response['msg'])
+                    except KeyError:
+                        raise KeyError("Query failed but no exchange message found.")
+            else:
+                return response
+        else:
+            return response
+
     def get_products(self):
         needed = self.needed['get_products']
         """
@@ -80,6 +96,7 @@ def get_products(self):
         ]
         """
         products = self._market.get_symbol_list()
+        products = self.__correct_api_call(products)
         for i in range(len(products)):
             products[i]["base_asset"] = products[i].pop("baseCurrency")
             products[i]["quote_asset"] = products[i].pop("quoteCurrency")
@@ -113,6 +130,7 @@ def get_account(self, symbol=None) -> utils.AttributeDict:
               }
         """
         accounts = self._user.get_account_list()
+        accounts = self.__correct_api_call(accounts)
         trade_accounts = []
         # Filter for only the trade accounts
         for i in accounts:
@@ -174,6 +192,7 @@ def market_order(self, symbol, side, size) -> MarketOrder:
         }
 
         response = self._trade.create_market_order(symbol, side, size=size)
+        response = self.__correct_api_call(response)
         if "msg" in response:
             raise InvalidOrder("Invalid Order: " + response["msg"])
 
@@ -238,10 +257,12 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
             'type': 'limit',
         }
         response = self._trade.create_limit_order(symbol, side, size, price)
+        response = self.__correct_api_call(response)
         if "msg" in response:
             raise InvalidOrder("Invalid Order: " + response["msg"])
 
         response_details = self._trade.get_order_details(response['orderId'])
+        response_details = self.__correct_api_call(response_details)
         """
                {
                    "id": "5c35c02703aa673ceec2a168", //
@@ -293,7 +314,7 @@ def cancel_order(self, symbol, order_id) -> dict:
            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]}
+        return {"order_id": self.__correct_api_call(self._trade.cancel_order(order_id)['cancelledOrderIds'][0])}
 
     def get_open_orders(self, symbol=None):
         """
@@ -342,9 +363,9 @@ def get_open_orders(self, symbol=None):
          }
         """
         if symbol is None:
-            open_orders = list(self._trade.get_order_list(status='active')["items"])
+            open_orders = list(self.__correct_api_call(self._trade.get_order_list(status='active')["items"]))
         else:
-            open_orders = list(self._trade.get_order_list(status='active', symbol=symbol)["items"])
+            open_orders = list(self.__correct_api_call(self._trade.get_order_list(status='active', symbol=symbol)["items"]))
 
         if len(open_orders) == 0:
             return []
@@ -402,6 +423,7 @@ def get_order(self, symbol, order_id) -> dict:
         }
         """
         response = self._trade.get_order_details(order_id)
+        response = self.__correct_api_call(response)
 
         if 'msg' in response:
             raise APIException("Invalid: " + str(response['msg']) + ", was the order canceled?")
@@ -426,7 +448,7 @@ def get_order(self, symbol, order_id) -> dict:
 
         return utils.isolate_specific(needed, response)
 
-    def get_fees(self) -> dict:
+    def get_fees(self, symbol) -> dict:
         needed = self.needed['get_fees']
         """
             {
@@ -438,6 +460,7 @@ def get_fees(self) -> dict:
             }
         """
         fees = self._user.get_base_fee()
+        fees = self.__correct_api_call(fees)
         fees["taker_fee_rate"] = fees.pop('takerFeeRate')
         fees["maker_fee_rate"] = fees.pop('makerFeeRate')
         return utils.isolate_specific(needed, fees)
@@ -454,7 +477,7 @@ def overridden_history(self, symbol, epoch_start, epoch_stop, resolution, **kwar
         elif isinstance(to, int):
             epoch_start = epoch_stop - (to * resolution)
 
-        return self.get_product_history(symbol, epoch_start, epoch_stop, resolution)
+        return self.__correct_api_call(self.get_product_history(symbol, epoch_start, epoch_stop, resolution))
 
     def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
         """
@@ -511,18 +534,20 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
             window_close = int(window_open + 1500 * resolution)
             response = self._market.get_kline(symbol, gran_string,
                                               startAt=window_open, endAt=window_close)
+            response = self.__correct_api_call(response)
             if isinstance(response, dict):
                 raise APIException(response['msg'])
             history = history + response
 
             window_open = window_close
             need -= 1500
-            time.sleep(1.5)
+            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)
+        response = self.__correct_api_call(response)
         if isinstance(response, dict):
             raise APIException(response['msg'])
 
@@ -570,6 +595,7 @@ def get_order_filter(self, symbol: str):
            ]
         """
         response = self._market.get_symbol_list()
+        response = self.__correct_api_call(response)
         products = None
         for i in response:
             if i["symbol"] == symbol:
@@ -638,6 +664,7 @@ def get_price(self, symbol) -> float:
         }       
         """
         response = self._market.get_ticker(symbol)
+        response = self.__correct_api_call(response)
         if 'msg' in response:
             raise APIException("Error: " + response['msg'])
-        return float(response['price'])
+        return float(response['price'])
\ No newline at end of file
diff --git a/blankly/exchanges/interfaces/kucoin/kucoin_notes.md b/blankly/exchanges/interfaces/kucoin/kucoin_notes.md
new file mode 100644
index 00000000..47d67be1
--- /dev/null
+++ b/blankly/exchanges/interfaces/kucoin/kucoin_notes.md
@@ -0,0 +1,2 @@
+## get_product_history
+- Kucoin seems to only return up to 100 days back
\ No newline at end of file
diff --git a/blankly/exchanges/interfaces/kucoin/kucoin_websocket.py b/blankly/exchanges/interfaces/kucoin/kucoin_websocket.py
new file mode 100644
index 00000000..2266b42b
--- /dev/null
+++ b/blankly/exchanges/interfaces/kucoin/kucoin_websocket.py
@@ -0,0 +1,119 @@
+"""
+    Implementation for Kucoin Websockets
+    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 json
+import time
+import traceback
+
+import blankly.exchanges.interfaces.kucoin.kucoin_websocket_utils as websocket_utils
+from blankly.exchanges.interfaces.websocket import Websocket
+from blankly.utils.utils import info_print
+
+
+class Tickers(Websocket):
+    def __init__(self, symbol, stream, websocket_url, log=None,
+                 pre_event_callback=None, initially_stopped=False,
+                 id_=None, **kwargs):
+        """
+        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.id = id_
+        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, kwargs)
+
+        # 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):
+        # Main thread to sit here and run
+        self.ws.run_forever()
+
+    def on_message(self, ws, message):
+        """
+        Exchange specific actions to perform when receiving a message
+        """
+        # print(message)
+        message = json.loads(message)
+
+        if message['type'] == 'subscribe':
+            channel = message['topic'].split(":", 1)[0].split("/", 2)[2]
+            info_print(f"Subscribed to {channel}")
+            return
+        elif message['type'] == 'welcome' or message['type'] == 'ack':
+            return
+
+        # parsed_received_trades = websocket_utils.trade_interface(message)
+
+        # for received in parsed_received_trades:
+            # ISO8601 is converted to epoch in process_trades
+        self.most_recent_time = time.time()
+        self.time_feed.append(self.most_recent_time)
+
+        self.log_response(self.__logging_callback, message)
+
+        # Manage price events and fire for each manager attached
+        interface_message = self.__interface_callback(message)
+        self.ticker_feed.append(interface_message)
+        self.most_recent_tick = interface_message
+
+        try:
+            for i in self.callbacks:
+                i(interface_message, **self.kwargs)
+        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({
+            'id': self.id,
+            'type': 'subscribe',
+            'topic': f'/market/{self.stream}:{self.symbol}',
+            'privateChannel': False,
+            'response': True
+        })
+        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/kucoin/kucoin_websocket_utils.py b/blankly/exchanges/interfaces/kucoin/kucoin_websocket_utils.py
new file mode 100644
index 00000000..33f2521d
--- /dev/null
+++ b/blankly/exchanges/interfaces/kucoin/kucoin_websocket_utils.py
@@ -0,0 +1,53 @@
+import time
+
+from blankly.utils.utils import isolate_specific
+
+
+def switch_type(stream):
+    if stream == "ticker":
+        return trade, \
+               trade_interface, \
+               "time, sequence, price, size, bestAsk, bestAskSize, bestBid, bestBidSize\n"
+    elif stream == "level2":
+        return no_callback, \
+               no_callback, \
+               ""
+    else:
+        return no_callback, no_callback, ""
+
+
+def no_callback(message):
+    return message
+
+
+def trade(message):
+    line = str(time.time()) + "," + message["sequence"] + "," + message["price"] + "," + \
+           message["size"] + "," + message["bestAsk"] + "," + message["bestAskSize"] + "," + \
+           message["bestBid"] + "," + message["bestBidSize"] + "\n"
+    return line
+
+
+def trade_interface(message):
+    # renames = [
+    #     ["sequence", "trade_id"]
+    # ]
+    #
+    # message = rename_to(renames, message)
+
+    needed = [
+        ["symbol", str],
+        ["price", float],
+        ["time", float],
+        ["trade_id", int],
+        ["size", float]
+    ]
+    message['symbol'] = message['topic'].split(":", 1)[1]
+    message['trade_id'] = message['data'].pop('sequence')
+    message['price'] = message['data'].pop('price')
+    message['size'] = message['data'].pop('size')
+
+    message["time"] = time.time()
+
+    isolated = isolate_specific(needed, message)
+
+    return isolated
diff --git a/blankly/exchanges/interfaces/oanda/oanda.py b/blankly/exchanges/interfaces/oanda/oanda.py
index 9f2b32ba..5e73f6f3 100644
--- a/blankly/exchanges/interfaces/oanda/oanda.py
+++ b/blankly/exchanges/interfaces/oanda/oanda.py
@@ -21,7 +21,7 @@ def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=Non
         super().construct_interface_and_cache(calls)
 
     def get_exchange_state(self):
-        return self.interface.get_fees()
+        return self.interface.get_products()
 
     def get_asset_state(self, symbol):
         return self.interface.get_account(symbol)
diff --git a/blankly/exchanges/interfaces/oanda/oanda_interface.py b/blankly/exchanges/interfaces/oanda/oanda_interface.py
index 8f08daac..e513ee54 100644
--- a/blankly/exchanges/interfaces/oanda/oanda_interface.py
+++ b/blankly/exchanges/interfaces/oanda/oanda_interface.py
@@ -28,7 +28,7 @@
 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.exceptions import APIException, InvalidOrder
 
 
 class OandaInterface(ExchangeInterface):
@@ -139,6 +139,7 @@ def get_products(self) -> list:
 
         return instruments
 
+    @utils.enforce_base_asset
     def get_account(self, symbol=None) -> utils.AttributeDict:
         if symbol is not None:
             symbol = self.__convert_blankly_to_oanda(symbol)
@@ -267,7 +268,13 @@ def limit_order(self, symbol: str, side: str, price: float, size: float) -> Limi
             'type': 'limit'
         }
 
-        resp['symbol'] = self.__convert_symbol_to_blankly(resp['orderCreateTransaction']['instrument'])
+        try:
+            resp['symbol'] = self.__convert_symbol_to_blankly(resp['orderCreateTransaction']['instrument'])
+        except KeyError as e:
+            if 'orderRejectTransaction' in resp:
+                raise InvalidOrder(resp['orderRejectTransaction']['rejectReason'])
+            else:
+                raise e
         resp['id'] = resp['orderCreateTransaction']['id']
         resp['created_at'] = resp['orderCreateTransaction']['time']
         resp['price'] = price
@@ -296,14 +303,18 @@ def get_open_orders(self, symbol=None):
 
         for i in range(len(orders)):
             orders[i] = self.homogenize_order(orders[i])
+            orders[i]['symbol'] = self.__convert_symbol_to_blankly(orders[i]['symbol'])
         return orders
 
     def get_order(self, symbol, order_id) -> dict:
         # Either the Order’s OANDA-assigned OrderID or the Order’s client-provided ClientID prefixed by the “@” symbol
         order = self.calls.get_order(order_id)
-        return self.homogenize_order(order['order'])
+        homogenized = self.homogenize_order(order['order'])
+        homogenized['symbol'] = self.__convert_symbol_to_blankly(homogenized['symbol'])
+        homogenized['size'] = abs(homogenized['size'])
+        return homogenized
 
-    def get_fees(self):
+    def get_fees(self, symbol):
         return {
             'maker_fee_rate': 0.0,
             'taker_fee_rate': 0.0
diff --git a/blankly/exchanges/interfaces/okx/__init__.py b/blankly/exchanges/interfaces/okx/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/blankly/exchanges/interfaces/okx/okx.py b/blankly/exchanges/interfaces/okx/okx.py
new file mode 100644
index 00000000..d496c9d1
--- /dev/null
+++ b/blankly/exchanges/interfaces/okx/okx.py
@@ -0,0 +1,56 @@
+from blankly.exchanges.exchange import Exchange
+from blankly.exchanges.auth.auth_constructor import AuthConstructor
+from blankly.exchanges.interfaces.okx.okx_api import Client as OkxAPI, MarketAPI, AccountAPI, TradeAPI, ConvertAPI, \
+    FundingAPI, PublicAPI
+
+
+class Okx(Exchange):
+    def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=None):
+        Exchange.__init__(self, "okx", portfolio_name, settings_path)
+
+        # Load the auth from the keys file
+        auth = AuthConstructor(keys_path, portfolio_name, 'okx', ['API_KEY', 'API_SECRET', 'API_PASS', 'sandbox'])
+        keys = auth.keys
+
+        sandbox = super().evaluate_sandbox(auth)
+
+        # if self.preferences["settings"]["use_sandbox"]:
+        calls = {
+            'market': MarketAPI(api_key=keys['API_KEY'], api_secret_key=keys['API_SECRET'],passphrase=keys['API_PASS'],
+                                sandbox=sandbox),
+            'account': AccountAPI(api_key=keys['API_KEY'], api_secret_key=keys['API_SECRET'], passphrase=keys['API_PASS'],
+                                  sandbox=sandbox),
+            'trade': TradeAPI(api_key=keys['API_KEY'], api_secret_key=keys['API_SECRET'], passphrase=keys['API_PASS'],
+                              sandbox=sandbox),
+            'convert': ConvertAPI(api_key=keys['API_KEY'], api_secret_key=keys['API_SECRET'], passphrase=keys['API_PASS'],
+                                  sandbox=sandbox),
+            'funding': FundingAPI(api_key=keys['API_KEY'], api_secret_key=keys['API_SECRET'], passphrase=keys['API_PASS'],
+                                  sandbox=sandbox),
+            'public': PublicAPI(api_key=keys['API_KEY'], api_secret_key=keys['API_SECRET'], passphrase=keys['API_PASS'],
+                                sandbox=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_products()
+
+    def get_direct_calls(self) -> dict:
+        return self.calls
diff --git a/blankly/exchanges/interfaces/okx/okx_api.py b/blankly/exchanges/interfaces/okx/okx_api.py
new file mode 100644
index 00000000..e038b193
--- /dev/null
+++ b/blankly/exchanges/interfaces/okx/okx_api.py
@@ -0,0 +1,876 @@
+import requests
+import hmac
+import base64
+import datetime
+# import time
+import json
+
+CONTENT_TYPE = 'Content-Type'
+OK_ACCESS_KEY = 'OK-ACCESS-KEY'
+OK_ACCESS_SIGN = 'OK-ACCESS-SIGN'
+OK_ACCESS_TIMESTAMP = 'OK-ACCESS-TIMESTAMP'
+OK_ACCESS_PASSPHRASE = 'OK-ACCESS-PASSPHRASE'
+
+ACEEPT = 'Accept'
+COOKIE = 'Cookie'
+LOCALE = 'Locale='
+
+APPLICATION_JSON = 'application/json'
+
+GET = "GET"
+POST = "POST"
+
+SERVER_TIMESTAMP_URL = '/api/v5/public/time'
+
+# account
+POSITION_RISK = '/api/v5/account/account-position-risk'
+ACCOUNT_INFO = '/api/v5/account/balance'
+POSITION_INFO = '/api/v5/account/positions'
+BILLS_DETAIL = '/api/v5/account/bills'
+BILLS_ARCHIVE = '/api/v5/account/bills-archive'
+ACCOUNT_CONFIG = '/api/v5/account/config'
+POSITION_MODE = '/api/v5/account/set-position-mode'
+SET_LEVERAGE = '/api/v5/account/set-leverage'
+MAX_TRADE_SIZE = '/api/v5/account/max-size'
+MAX_AVAIL_SIZE = '/api/v5/account/max-avail-size'
+ADJUSTMENT_MARGIN = '/api/v5/account/position/margin-balance'
+GET_LEVERAGE = '/api/v5/account/leverage-info'
+MAX_LOAN = '/api/v5/account/max-loan'
+FEE_RATES = '/api/v5/account/trade-fee'
+INTEREST_ACCRUED = '/api/v5/account/interest-accrued'
+INTEREST_RATE = '/api/v5/account/interest-rate'
+SET_GREEKS = '/api/v5/account/set-greeks'
+ISOLATED_MODE = '/api/v5/account/set-isolated-mode'
+MAX_WITHDRAWAL = '/api/v5/account/max-withdrawal'
+ACCOUNT_RISK = '/api/v5/account/risk-state'
+BORROW_REPAY = '/api/v5/account/borrow-repay'
+BORROW_REPAY_HISTORY = '/api/v5/account/borrow-repay-history'
+INTEREST_LIMITS = '/api/v5/account/interest-limits'
+SIMULATED_MARGIN = '/api/v5/account/simulated_margin'
+GREEKS = '/api/v5/account/greeks'
+
+# funding
+DEPOSIT_ADDRESS = '/api/v5/asset/deposit-address'
+GET_BALANCES = '/api/v5/asset/balances'
+FUNDS_TRANSFER = '/api/v5/asset/transfer'
+TRANSFER_STATE = '/api/v5/asset/transfer-state'
+WITHDRAWAL_COIN = '/api/v5/asset/withdrawal'
+DEPOSIT_HISTORIY = '/api/v5/asset/deposit-history'
+WITHDRAWAL_HISTORIY = '/api/v5/asset/withdrawal-history'
+CURRENCY_INFO = '/api/v5/asset/currencies'
+PURCHASE_REDEMPT = '/api/v5/asset/purchase_redempt'
+BILLS_INFO = '/api/v5/asset/bills'
+PIGGY_BALANCE = '/api/v5/asset/piggy-balance'
+DEPOSIT_LIGHTNING = '/api/v5/asset/deposit-lightning'
+WITHDRAWAL_LIGHTNING = '/api/v5/asset/withdrawal-lightning'
+ASSET_VALUATION = '/api/v5/asset/asset-valuation'
+SET_LENDING_RATE = '/api/v5/asset/set-lending-rate'
+LENDING_HISTORY = '/api/v5/asset/lending-history'
+LENDING_RATE_HISTORY = '/api/v5/asset/lending-rate-history'
+LENDING_RATE_SUMMARY = '/api/v5/asset/lending-rate-summary'
+
+# Market Data
+TICKERS_INFO = '/api/v5/market/tickers'
+TICKER_INFO = '/api/v5/market/ticker'
+INDEX_TICKERS = '/api/v5/market/index-tickers'
+ORDER_BOOKS = '/api/v5/market/books'
+MARKET_CANDLES = '/api/v5/market/candles'
+HISTORY_CANDLES = '/api/v5/market/history-candles'
+INDEX_CANSLES = '/api/v5/market/index-candles'
+MARKPRICE_CANDLES = '/api/v5/market/mark-price-candles'
+MARKET_TRADES = '/api/v5/market/trades'
+VOLUMNE = '/api/v5/market/platform-24-volume'
+ORACLE = '/api/v5/market/oracle'
+Components = '/api/v5/market/index-components'
+EXCHANGE_RATE = '/api/v5/market/exchange-rate'
+
+# Public Data
+INSTRUMENT_INFO = '/api/v5/public/instruments'
+DELIVERY_EXERCISE = '/api/v5/public/delivery-exercise-history'
+OPEN_INTEREST = '/api/v5/public/open-interest'
+FUNDING_RATE = '/api/v5/public/funding-rate'
+FUNDING_RATE_HISTORY = '/api/v5/public/funding-rate-history'
+PRICE_LIMIT = '/api/v5/public/price-limit'
+OPT_SUMMARY = '/api/v5/public/opt-summary'
+ESTIMATED_PRICE = '/api/v5/public/estimated-price'
+DICCOUNT_INTETEST_INFO = '/api/v5/public/discount-rate-interest-free-quota'
+SYSTEM_TIME = '/api/v5/public/time'
+LIQUIDATION_ORDERS = '/api/v5/public/liquidation-orders'
+MARK_PRICE = '/api/v5/public/mark-price'
+TIER = '/api/v5/public/position-tiers'
+INTEREST_LOAN = '/api/v5/public/interest-rate-loan-quota'
+UNDERLYING = '/api/v5/public/underlying'
+VIP_INTEREST_RATE_LOAN_QUOTA = '/api/v5/public/vip-interest-rate-loan-quota'
+
+# TRADING DATA
+SUPPORT_COIN = '/api/v5/rubik/stat/trading-data/support-coin'
+TAKER_VOLUME = '/api/v5/rubik/stat/taker-volume'
+MARGIN_LENDING_RATIO = '/api/v5/rubik/stat/margin/loan-ratio'
+LONG_SHORT_RATIO = '/api/v5/rubik/stat/contracts/long-short-account-ratio'
+CONTRACTS_INTEREST_VOLUME = '/api/v5/rubik/stat/contracts/open-interest-volume'
+OPTIONS_INTEREST_VOLUME = '/api/v5/rubik/stat/option/open-interest-volume'
+PUT_CALL_RATIO = '/api/v5/rubik/stat/option/open-interest-volume-ratio'
+OPEN_INTEREST_VOLUME_EXPIRY = '/api/v5/rubik/stat/option/open-interest-volume-expiry'
+INTEREST_VOLUME_STRIKE = '/api/v5/rubik/stat/option/open-interest-volume-strike'
+TAKER_FLOW = '/api/v5/rubik/stat/option/taker-block-volume'
+
+# TRADE
+PLACR_ORDER = '/api/v5/trade/order'
+BATCH_ORDERS = '/api/v5/trade/batch-orders'
+CANAEL_ORDER = '/api/v5/trade/cancel-order'
+CANAEL_BATCH_ORDERS = '/api/v5/trade/cancel-batch-orders'
+AMEND_ORDER = '/api/v5/trade/amend-order'
+AMEND_BATCH_ORDER = '/api/v5/trade/amend-batch-orders'
+CLOSE_POSITION = '/api/v5/trade/close-position'
+ORDER_INFO = '/api/v5/trade/order'
+ORDERS_PENDING = '/api/v5/trade/orders-pending'
+ORDERS_HISTORY = '/api/v5/trade/orders-history'
+ORDERS_HISTORY_ARCHIVE = '/api/v5/trade/orders-history-archive'
+ORDER_FILLS = '/api/v5/trade/fills'
+ORDERS_FILLS_HISTORY = '/api/v5/trade/fills-history'
+PLACE_ALGO_ORDER = '/api/v5/trade/order-algo'
+CANCEL_ALGOS = '/api/v5/trade/cancel-algos'
+Cancel_Advance_Algos = '/api/v5/trade/cancel-advance-algos'
+ORDERS_ALGO_OENDING = '/api/v5/trade/orders-algo-pending'
+ORDERS_ALGO_HISTORY = '/api/v5/trade/orders-algo-history'
+
+# SubAccount
+BALANCE = '/api/v5/account/subaccount/balances'
+BILLs = '/api/v5/asset/subaccount/bills'
+DELETE = '/api/v5/users/subaccount/delete-apikey'
+RESET = '/api/v5/users/subaccount/modify-apikey'
+CREATE = '/api/v5/users/subaccount/apikey'
+WATCH = '/api/v5/users/subaccount/apikey'
+VIEW_LIST = '/api/v5/users/subaccount/list'
+SUBACCOUNT_TRANSFER = '/api/v5/asset/subaccount/transfer'
+ENTRUST_SUBACCOUNT_LIST = '/api/v5/users/entrust-subaccount-list'
+
+# Broker
+BROKER_INFO = '/api/v5/broker/nd/info'
+CREATE_SUBACCOUNT = '/api/v5/broker/nd/create-subaccount'
+DELETE_SUBACCOUNT = '/api/v5/broker/nd/delete-subaccount'
+SUBACCOUNT_INFO = '/api/v5/broker/nd/subaccount-info'
+SET_SUBACCOUNT_LEVEL = '/api/v5/broker/nd/set-subaccount-level'
+SET_SUBACCOUNT_FEE_REAT = '/api/v5/broker/nd/set-subaccount-fee-rate'
+SUBACCOUNT_DEPOSIT_ADDRESS = '/api/v5/asset/broker/nd/subaccount-deposit-address'
+SUBACCOUNT_DEPOSIT_HISTORY = '/api/v5/asset/broker/nd/subaccount-deposit-history'
+REBATE_DAILY = '/api/v5/broker/nd/rebate-daily'
+# BROKER_INFO = '/api/v5/broker/nd/info' Broker 获取充值地址文档无法打开，预留位置
+
+# Convert
+GET_CURRENCIES = '/api/v5/asset/convert/currencies'
+GET_CURRENCY_PAIR = '/api/v5/asset/convert/currency-pair'
+ESTIMATE_QUOTE = '/api/v5/asset/convert/estimate-quote'
+CONVERT_TRADE = '/api/v5/asset/convert/trade'
+CONVERT_HISTORY = '/api/v5/asset/convert/history'
+
+# status
+STATUS = '/api/v5/system/status'
+
+
+def sign(message, secretKey):
+    mac = hmac.new(bytes(secretKey, encoding='utf8'), bytes(message, encoding='utf-8'), digestmod='sha256')
+    d = mac.digest()
+    return base64.b64encode(d)
+
+
+def pre_hash(timestamp, method, request_path, body):
+    return str(timestamp) + str.upper(method) + request_path + body
+
+
+def get_header(api_key, sign_, timestamp, passphrase, flag):
+    header = dict()
+    header[CONTENT_TYPE] = APPLICATION_JSON
+    header[OK_ACCESS_KEY] = api_key
+    header[OK_ACCESS_SIGN] = sign_
+    header[OK_ACCESS_TIMESTAMP] = str(timestamp)
+    header[OK_ACCESS_PASSPHRASE] = passphrase
+    header['x-simulated-trading'] = flag
+    return header
+
+
+def parse_params_to_str(params):
+    url = '?'
+    for key, value in params.items():
+        url = url + str(key) + '=' + str(value) + '&'
+    return url[0:-1]
+
+
+def get_timestamp():
+    now = datetime.datetime.utcnow()
+    t = now.isoformat("T", "milliseconds")
+    return t + "Z"
+
+
+def signature(timestamp, method, request_path, body, secret_key):
+    if str(body) == '{}' or str(body) == 'None':
+        body = ''
+    message = str(timestamp) + str.upper(method) + request_path + str(body)
+
+    mac = hmac.new(bytes(secret_key, encoding='utf8'), bytes(message, encoding='utf-8'), digestmod='sha256')
+    d = mac.digest()
+
+    return base64.b64encode(d)
+
+
+class OkxAPIException(Exception):
+
+    def __init__(self, response):
+        print(response.text + ', ' + str(response.status_code))
+        self.code = 0
+        try:
+            json_res = response.json()
+        except ValueError:
+            self.message = 'Invalid JSON error message from Okx: {}'.format(response.text)
+        else:
+            if "code" in json_res.keys() and "msg" in json_res.keys():
+                self.code = json_res['code']
+                self.message = json_res['msg']
+            else:
+                self.code = 'None'
+                self.message = 'System error'
+
+        self.status_code = response.status_code
+        self.response = response
+        self.request = getattr(response, 'request', None)
+
+    def __str__(self):  # pragma: no cover
+        return 'API Request Error(code=%s): %s' % (self.code, self.message)
+
+
+class OkxRequestException(Exception):
+
+    def __init__(self, message):
+        self.message = message
+
+    def __str__(self):
+        return 'OkxRequestException: %s' % self.message
+
+
+class OkxParamsException(Exception):
+
+    def __init__(self, message):
+        self.message = message
+
+    def __str__(self):
+        return 'OkxParamsException: %s' % self.message
+
+
+class Client(object):
+
+    def __init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time=False, flag='1'):
+
+        self.API_KEY = api_key
+        self.API_SECRET_KEY = api_secret_key
+        self.PASSPHRASE = passphrase
+        self.use_server_time = use_server_time
+        self.flag = flag
+        self._sandbox = sandbox
+
+        self.api_url = 'https://www.okx.com'
+
+    def _request(self, method, request_path, params):
+
+        if method == GET:
+            request_path = request_path + parse_params_to_str(params)
+        # url
+        url = self.api_url + request_path
+
+        timestamp = get_timestamp()
+
+        # sign & header
+        if self.use_server_time:
+            timestamp = self._get_timestamp()
+
+        body = json.dumps(params) if method == POST else ""
+
+        sign_ = sign(pre_hash(timestamp, method, request_path, str(body)), self.API_SECRET_KEY)
+        header = get_header(self.API_KEY, sign_, timestamp, self.PASSPHRASE, self.flag)
+        # header["Content-Type"] = 'application/json'
+        # header["OK_ACCESS_KEY"] = '141b8c40-bad4-4f6a-98f0-fd5944a529b1'
+        if self._sandbox:
+            header["x-simulated-trading"] = '1'
+        # header["OK_ACCESS_SIGN"] = 'TLWvAznkX8GUequwuvAb/weIFpV5ZXKlcSGvcwGPbl8='
+        # header["OK_ACCESS_TIMESTAMP"] = '2022-03-17T05:36:39.355Z'
+        # header["OK_ACCESS_PASSPHRASE"] = '123456'
+
+        # send request
+        response = None
+
+        #print("url:", url)
+        # print("headers:", header)
+        #print("body:", body)
+        if method == GET:
+            response = requests.get(url, headers=header)
+        elif method == POST:
+            response = requests.post(url, data=body, headers=header)
+
+        # exception handle
+        # print(response.headers)
+
+        if not str(response.status_code).startswith('2'):
+            raise OkxAPIException(response)
+
+        return response.json()
+
+    def _request_without_params(self, method, request_path):
+        return self._request(method, request_path, {})
+
+    def _request_with_params(self, method, request_path, params):
+        return self._request(method, request_path, params)
+
+    def _get_timestamp(self):
+        url = self.api_url + SERVER_TIMESTAMP_URL
+        response = requests.get(url)
+        if response.status_code == 200:
+            return response.json()['ts']
+        else:
+            return ""
+
+
+class MarketAPI(Client):
+
+    def __init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time=False, flag='1'):
+        Client.__init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time, flag)
+
+    # Get Tickers
+    def get_tickers(self, instType, uly=''):
+        if uly:
+            params = {'instType': instType, 'uly': uly}
+        else:
+            params = {'instType': instType}
+        return self._request_with_params(GET, TICKERS_INFO, params)
+
+    # Get Ticker
+    def get_ticker(self, instId):
+        params = {'instId': instId}
+        return self._request_with_params(GET, TICKER_INFO, params)
+
+    # Get Index Tickers
+    def get_index_ticker(self, quoteCcy='', instId=''):
+        params = {'quoteCcy': quoteCcy, 'instId': instId}
+        return self._request_with_params(GET, INDEX_TICKERS, params)
+
+    # Get Order Book
+    def get_orderbook(self, instId, sz=''):
+        params = {'instId': instId, 'sz': sz}
+        return self._request_with_params(GET, ORDER_BOOKS, params)
+
+    # Get Candlesticks
+    def get_candlesticks(self, instId, after='', before='', bar='', limit=''):
+        params = {'instId': instId, 'after': after, 'before': before, 'bar': bar, 'limit': limit}
+        return self._request_with_params(GET, MARKET_CANDLES, params)
+
+    # GGet Candlesticks History（top currencies only）
+    def get_history_candlesticks(self, instId, after='', before='', bar='', limit=''):
+        params = {'instId': instId, 'after': after, 'before': before, 'bar': bar, 'limit': limit}
+        return self._request_with_params(GET, HISTORY_CANDLES, params)
+
+    # Get Index Candlesticks
+    def get_index_candlesticks(self, instId, after='', before='', bar='', limit=''):
+        params = {'instId': instId, 'after': after, 'before': before, 'bar': bar, 'limit': limit}
+        return self._request_with_params(GET, INDEX_CANSLES, params)
+
+    # Get Mark Price Candlesticks
+    def get_markprice_candlesticks(self, instId, after='', before='', bar='', limit=''):
+        params = {'instId': instId, 'after': after, 'before': before, 'bar': bar, 'limit': limit}
+        return self._request_with_params(GET, MARKPRICE_CANDLES, params)
+
+    # Get Index Candlesticks
+    def get_trades(self, instId, limit=''):
+        params = {'instId': instId, 'limit': limit}
+        return self._request_with_params(GET, MARKET_TRADES, params)
+
+    # Get Volume
+    def get_volume(self):
+        return self._request_without_params(GET, VOLUMNE)
+
+    # Get Oracle
+    def get_oracle(self):
+        return self._request_without_params(GET, ORACLE)
+
+    # Get Index Components
+    def get_index_components(self, index):
+        params = {'index': index}
+        return self._request_with_params(GET, Components, params)
+
+    # Get Tier
+    def get_tier(self, instType='', tdMode='', uly='', instId='', ccy='', tier=''):
+        params = {'instType': instType, 'tdMode': tdMode, 'uly': uly, 'instId': instId, 'ccy': ccy, 'tier': tier}
+        return self._request_with_params(GET, TIER, params)
+
+    # Get exchange rate
+    def get_exchange_rate(self):
+        params = {}
+        return self._request_with_params(GET, BORROW_REPAY, params)
+
+
+class TradeAPI(Client):
+
+    def __init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time=False, flag='1'):
+        Client.__init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time, flag)
+
+    # Place Order
+    def place_order(self, instId, tdMode, side, ordType, sz, ccy='', clOrdId='', tag='', posSide='', px='',
+                    reduceOnly='', tgtCcy=''):
+        params = {'instId': instId, 'tdMode': tdMode, 'side': side, 'ordType': ordType, 'sz': sz, 'ccy': ccy,
+                  'clOrdId': clOrdId, 'tag': tag, 'posSide': posSide, 'px': px, 'reduceOnly': reduceOnly,
+                  'tgtCcy': tgtCcy}
+        return self._request_with_params(POST, PLACR_ORDER, params)
+
+    # Place Multiple Orders
+    def place_multiple_orders(self, orders_data):
+        return self._request_with_params(POST, BATCH_ORDERS, orders_data)
+
+    # Cancel Order
+    def cancel_order(self, instId, ordId='', clOrdId=''):
+        params = {'instId': instId, 'ordId': ordId, 'clOrdId': clOrdId}
+        return self._request_with_params(POST, CANAEL_ORDER, params)
+
+    # Cancel Multiple Orders
+    def cancel_multiple_orders(self, orders_data):
+        return self._request_with_params(POST, CANAEL_BATCH_ORDERS, orders_data)
+
+    # Amend Order
+    def amend_order(self, instId, cxlOnFail='', ordId='', clOrdId='', reqId='', newSz='', newPx=''):
+        params = {'instId': instId, 'cxlOnFailc': cxlOnFail, 'ordId': ordId, 'clOrdId': clOrdId, 'reqId': reqId,
+                  'newSz': newSz,
+                  'newPx': newPx}
+        return self._request_with_params(POST, AMEND_ORDER, params)
+
+    # Amend Multiple Orders
+    def amend_multiple_orders(self, orders_data):
+        return self._request_with_params(POST, AMEND_BATCH_ORDER, orders_data)
+
+    # Close Positions
+    def close_positions(self, instId, mgnMode, posSide='', ccy='', autoCxl=''):
+        params = {'instId': instId, 'mgnMode': mgnMode, 'posSide': posSide, 'ccy': ccy, 'autoCxl': autoCxl}
+        return self._request_with_params(POST, CLOSE_POSITION, params)
+
+    # Get Order Details
+    def get_orders(self, instId, ordId='', clOrdId=''):
+        params = {'instId': instId, 'ordId': ordId, 'clOrdId': clOrdId}
+        return self._request_with_params(GET, ORDER_INFO, params)
+
+    # Get Order List
+    def get_order_list(self, instType='', uly='', instId='', ordType='', state='', after='', before='', limit=''):
+        params = {'instType': instType, 'uly': uly, 'instId': instId, 'ordType': ordType, 'state': state,
+                  'after': after, 'before': before, 'limit': limit}
+        return self._request_with_params(GET, ORDERS_PENDING, params)
+
+    # Get Order History (last 7 days）
+    def get_orders_history(self, instType, uly='', instId='', ordType='', state='', after='', before='', limit=''):
+        params = {'instType': instType, 'uly': uly, 'instId': instId, 'ordType': ordType, 'state': state,
+                  'after': after, 'before': before, 'limit': limit}
+        return self._request_with_params(GET, ORDERS_HISTORY, params)
+
+    # Get Order History (last 3 months)
+    def orders_history_archive(self, instType, uly='', instId='', ordType='', state='', after='', before='', limit=''):
+        params = {'instType': instType, 'uly': uly, 'instId': instId, 'ordType': ordType, 'state': state,
+                  'after': after, 'before': before, 'limit': limit}
+        return self._request_with_params(GET, ORDERS_HISTORY_ARCHIVE, params)
+
+    # Get Transaction Details
+    def get_fills(self, instType='', uly='', instId='', ordId='', after='', before='', limit=''):
+        params = {'instType': instType, 'uly': uly, 'instId': instId, 'ordId': ordId, 'after': after, 'before': before,
+                  'limit': limit}
+        return self._request_with_params(GET, ORDER_FILLS, params)
+
+    # Place Algo Order
+    def place_algo_order(self, instId='', tdMode='', side='', ordType='', sz='', ccy='',
+                         posSide='', reduceOnly='', tpTriggerPx='',
+                         tpOrdPx='', slTriggerPx='', slOrdPx='',
+                         triggerPx='', orderPx='', tgtCcy='', pxVar='',
+                         pxSpread='',
+                         szLimit='', pxLimit='', timeInterval='', tpTriggerPxType='', slTriggerPxType='',
+                         callbackRatio='', callbackSpread='', activePx='', tag='', triggerPxType=''):
+        params = {'instId': instId, 'tdMode': tdMode, 'side': side, 'ordType': ordType, 'sz': sz, 'ccy': ccy,
+                  'posSide': posSide, 'reduceOnly': reduceOnly, 'tpTriggerPx': tpTriggerPx, 'tpOrdPx': tpOrdPx,
+                  'slTriggerPx': slTriggerPx, 'slOrdPx': slOrdPx, 'triggerPx': triggerPx, 'orderPx': orderPx,
+                  'tgtCcy': tgtCcy, 'pxVar': pxVar, 'szLimit': szLimit, 'pxLimit': pxLimit,
+                  'timeInterval': timeInterval,
+                  'pxSpread': pxSpread, 'tpTriggerPxType': tpTriggerPxType, 'slTriggerPxType': slTriggerPxType,
+                  'callbackRatio': callbackRatio, 'callbackSpread': callbackSpread, 'activePx': activePx,
+                  'tag': tag, 'triggerPxType': triggerPxType, }
+        return self._request_with_params(POST, PLACE_ALGO_ORDER, params)
+
+    # Cancel Algo Order
+    def cancel_algo_order(self, params):
+        return self._request_with_params(POST, CANCEL_ALGOS, params)
+
+    # Cancel Advance Algos
+    def cancel_advance_algos(self, params):
+        return self._request_with_params(POST, Cancel_Advance_Algos, params)
+
+    # Get Algo Order List
+    def order_algos_list(self, ordType, algoId='', instType='', instId='', after='', before='', limit=''):
+        params = {'ordType': ordType, 'algoId': algoId, 'instType': instType, 'instId': instId, 'after': after,
+                  'before': before, 'limit': limit}
+        return self._request_with_params(GET, ORDERS_ALGO_OENDING, params)
+
+    # Get Algo Order History
+    def order_algos_history(self, ordType, state='', algoId='', instType='', instId='', after='', before='', limit=''):
+        params = {'ordType': ordType, 'state': state, 'algoId': algoId, 'instType': instType, 'instId': instId,
+                  'after': after, 'before': before, 'limit': limit}
+        return self._request_with_params(GET, ORDERS_ALGO_HISTORY, params)
+
+    # Get Transaction Details History
+    def get_fills_history(self, instType, uly='', instId='', ordId='', after='', before='', limit=''):
+        params = {'instType': instType, 'uly': uly, 'instId': instId, 'ordId': ordId, 'after': after, 'before': before,
+                  'limit': limit}
+        return self._request_with_params(GET, ORDERS_FILLS_HISTORY, params)
+
+
+class ConvertAPI(Client):
+    def __init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time=False, flag='1'):
+        Client.__init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time, flag)
+
+    def get_currencies(self):
+        params = {}
+        return self._request_with_params(GET, GET_CURRENCIES, params)
+
+    def get_currency_pair(self, fromCcy='', toCcy=''):
+        params = {"fromCcy": fromCcy, 'toCcy': toCcy}
+        return self._request_with_params(GET, GET_CURRENCY_PAIR, params)
+
+    def estimate_quote(self, baseCcy='', quoteCcy='', side='', rfqSz='', rfqSzCcy='', clQReqId=''):
+        params = {'baseCcy': baseCcy, 'quoteCcy': quoteCcy, 'side': side, 'rfqSz': rfqSz, 'rfqSzCcy': rfqSzCcy,
+                  'clQReqId': clQReqId}
+        return self._request_with_params(POST, ESTIMATE_QUOTE, params)
+
+    def convert_trade(self, quoteId='', baseCcy='', quoteCcy='', side='', sz='', szCcy='', clTReqId=''):
+        params = {'quoteId': quoteId, 'baseCcy': baseCcy, 'quoteCcy': quoteCcy, 'side': side, 'sz': sz, 'szCcy': szCcy,
+                  'clTReqId': clTReqId}
+        return self._request_with_params(POST, CONVERT_TRADE, params)
+
+    def get_convert_history(self, after='', before='', limit=''):
+        params = {'after': after, 'before': before, 'limit': limit}
+        return self._request_with_params(GET, CONVERT_HISTORY, params)
+
+
+class AccountAPI(Client):
+
+    def __init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time=False, flag='1'):
+        Client.__init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time, flag)
+
+    # Get Positions
+    def get_position_risk(self, instType=''):
+        params = {}
+        if instType:
+            params['instType'] = instType
+        return self._request_with_params(GET, POSITION_RISK, params)
+
+    # Get Balance
+    def get_account(self, ccy=''):
+        params = {}
+        if ccy:
+            params['ccy'] = ccy
+        return self._request_with_params(GET, ACCOUNT_INFO, params)
+
+    # Get Positions
+    def get_positions(self, instType='', instId=''):
+        params = {'instType': instType, 'instId': instId}
+        return self._request_with_params(GET, POSITION_INFO, params)
+
+    # Get Bills Details (recent 7 days)
+    def get_bills_detail(self, instType='', ccy='', mgnMode='', ctType='', type='', subType='', after='', before='',
+                         limit=''):
+        params = {'instType': instType, 'ccy': ccy, 'mgnMode': mgnMode, 'ctType': ctType, 'type': type,
+                  'subType': subType, 'after': after, 'before': before, 'limit': limit}
+        return self._request_with_params(GET, BILLS_DETAIL, params)
+
+    # Get Bills Details (recent 3 months)
+    def get_bills_details(self, instType='', ccy='', mgnMode='', ctType='', type='', subType='', after='', before='',
+                          limit=''):
+        params = {'instType': instType, 'ccy': ccy, 'mgnMode': mgnMode, 'ctType': ctType, 'type': type,
+                  'subType': subType, 'after': after, 'before': before, 'limit': limit}
+        return self._request_with_params(GET, BILLS_ARCHIVE, params)
+
+    # Get Account Configuration
+    def get_account_config(self):
+        return self._request_without_params(GET, ACCOUNT_CONFIG)
+
+    # Get Account Configuration
+    def get_position_mode(self, posMode):
+        params = {'posMode': posMode}
+        return self._request_with_params(POST, POSITION_MODE, params)
+
+    # Get Account Configuration
+    def set_leverage(self, lever, mgnMode, instId='', ccy='', posSide=''):
+        params = {'lever': lever, 'mgnMode': mgnMode, 'instId': instId, 'ccy': ccy, 'posSide': posSide}
+        return self._request_with_params(POST, SET_LEVERAGE, params)
+
+    # Get Maximum Tradable Size For Instrument
+    def get_maximum_trade_size(self, instId, tdMode, ccy='', px='', leverage=''):
+        params = {'instId': instId, 'tdMode': tdMode, 'ccy': ccy, 'px': px, 'leverage': leverage}
+        return self._request_with_params(GET, MAX_TRADE_SIZE, params)
+
+    # Get Maximum Available Tradable Amount
+    def get_max_avail_size(self, instId, tdMode, ccy='', reduceOnly=''):
+        params = {'instId': instId, 'tdMode': tdMode, 'ccy': ccy, 'reduceOnly': reduceOnly}
+        return self._request_with_params(GET, MAX_AVAIL_SIZE, params)
+
+    # Increase / Decrease margin
+    def Adjustment_margin(self, instId, posSide, type, amt, loanTrans=''):
+        params = {'instId': instId, 'posSide': posSide, 'type': type, 'amt': amt, 'loanTrans': loanTrans}
+        return self._request_with_params(POST, ADJUSTMENT_MARGIN, params)
+
+    # Get Leverage
+    def get_leverage(self, instId, mgnMode):
+        params = {'instId': instId, 'mgnMode': mgnMode}
+        return self._request_with_params(GET, GET_LEVERAGE, params)
+
+    # Get the maximum loan of isolated MARGIN
+    def get_max_load(self, instId, mgnMode, mgnCcy):
+        params = {'instId': instId, 'mgnMode': mgnMode, 'mgnCcy': mgnCcy}
+        return self._request_with_params(GET, MAX_LOAN, params)
+
+    # Get Fee Rates
+    def get_fee_rates(self, instType, instId='', uly='', category=''):
+        params = {'instType': instType, 'instId': instId, 'uly': uly, 'category': category}
+        return self._request_with_params(GET, FEE_RATES, params)
+
+    # Get interest-accrued
+    def get_interest_accrued(self, instId='', ccy='', mgnMode='', after='', before='', limit='', type=''):
+        params = {'instId': instId, 'ccy': ccy, 'mgnMode': mgnMode, 'after': after, 'before': before, 'limit': limit,
+                  'type': type}
+        return self._request_with_params(GET, INTEREST_ACCRUED, params)
+
+    # Get interest-accrued
+    def get_interest_rate(self, ccy=''):
+        params = {'ccy': ccy}
+        return self._request_with_params(GET, INTEREST_RATE, params)
+
+    # Set Greeks (PA/BS)
+    def set_greeks(self, greeksType):
+        params = {'greeksType': greeksType}
+        return self._request_with_params(POST, SET_GREEKS, params)
+
+    # Set Isolated Mode
+    def set_isolated_mode(self, isoMode, type):
+        params = {'isoMode': isoMode, 'type': type}
+        return self._request_with_params(POST, ISOLATED_MODE, params)
+
+    # Get Maximum Withdrawals
+    def get_max_withdrawal(self, ccy=''):
+        params = {'ccy': ccy}
+        return self._request_with_params(GET, MAX_WITHDRAWAL, params)
+
+    # Get account risk state
+    def get_account_risk(self):
+        return self._request_without_params(GET, ACCOUNT_RISK)
+
+    # Get borrow repay
+    def borrow_repay(self, ccy='', side='', amt=''):
+        params = {'ccy': ccy, 'side': side, 'amt': amt}
+        return self._request_with_params(POST, BORROW_REPAY, params)
+
+    # Get borrow repay history
+    def get_borrow_repay_history(self, ccy='', after='', before='', limit=''):
+        params = {'ccy': ccy, 'after': after, 'before': before, 'limit': limit}
+        return self._request_with_params(GET, BORROW_REPAY_HISTORY, params)
+
+    # Get Obtain borrowing rate and limit
+    def get_interest_limits(self, type='', ccy=''):
+        params = {'type': type, 'ccy': ccy}
+        return self._request_with_params(GET, INTEREST_LIMITS, params)
+
+    # Get Simulated Margin
+    def get_simulated_margin(self, instType='', inclRealPos='', instId='', pos=''):
+        params = {'instType': instType, 'inclRealPos': inclRealPos, 'instId': instId, 'pos': pos, }
+        return self._request_with_params(GET, SIMULATED_MARGIN, params)
+
+    # Get  Greeks
+    def get_greeks(self, ccy=''):
+        params = {'ccy': ccy, }
+        return self._request_with_params(GET, GREEKS, params)
+
+class FundingAPI(Client):
+
+    def __init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time=False, flag='1'):
+        Client.__init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time, flag)
+
+    # Get Deposit Address
+    def get_deposit_address(self, ccy):
+        params = {'ccy': ccy}
+        return self._request_with_params(GET, DEPOSIT_ADDRESS, params)
+
+    # Get Balance
+    def get_balances(self, ccy=''):
+        params = {'ccy': ccy}
+        return self._request_with_params(GET, GET_BALANCES, params)
+
+    # POST Account Configuration
+    def funds_transfer(self, ccy, amt, froms, to, type='0', subAcct='', instId='', toInstId='',loanTrans=''):
+        params = {'ccy': ccy, 'amt': amt, 'from': froms, 'to': to, 'type': type, 'subAcct': subAcct, 'instId': instId,
+                  'toInstId': toInstId,'loanTrans':loanTrans}
+        return self._request_with_params(POST, FUNDS_TRANSFER, params)
+
+    # Get Transfer State
+    def transfer_state(self, transId,type=''):
+        params = {'transId': transId, 'type': type}
+        return self._request_with_params(POST, TRANSFER_STATE, params)
+
+    # Withdrawal
+    def coin_withdraw(self, ccy, amt, dest, toAddr, pwd, fee,chain=''):
+        params = {'ccy': ccy, 'amt': amt, 'dest': dest, 'toAddr': toAddr, 'pwd': pwd, 'fee': fee,'chain': chain}
+        return self._request_with_params(POST, WITHDRAWAL_COIN, params)
+
+    # Get Deposit History
+    def get_deposit_history(self, ccy='', state='', after='', before='', limit='',txId=''):
+        params = {'ccy': ccy, 'state': state, 'after': after, 'before': before, 'limit': limit,'txId':txId}
+        return self._request_with_params(GET, DEPOSIT_HISTORIY, params)
+
+    # Get Withdrawal History
+    def get_withdrawal_history(self, ccy='', state='', after='', before='', limit='',txId=''):
+        params = {'ccy': ccy, 'state': state, 'after': after, 'before': before, 'limit': limit,'txId':txId}
+        return self._request_with_params(GET, WITHDRAWAL_HISTORIY, params)
+
+    # Get Currencies
+    def get_currency(self):
+        return self._request_without_params(GET, CURRENCY_INFO)
+
+    # PiggyBank Purchase/Redemption
+    def purchase_redempt(self, ccy, amt, side,rate):
+        params = {'ccy': ccy, 'amt': amt, 'side': side,'rate': rate}
+        return self._request_with_params(POST, PURCHASE_REDEMPT, params)
+
+    # Get Withdrawal History
+    def get_bills(self, ccy='', type='', after='', before='', limit=''):
+        params = {'ccy': ccy, 'type': type, 'after': after, 'before': before, 'limit': limit}
+        return self._request_with_params(GET, BILLS_INFO, params)
+
+
+    #Get Piggy Balance
+    def get_piggy_balance(self, ccy=''):
+        params = {}
+        if ccy:
+            params = {'ccy':ccy}
+        return self._request_with_params(GET, PIGGY_BALANCE, params)
+
+
+    #Get Deposit Lightning
+    def get_deposit_lightning(self, ccy,amt,to=""):
+        params = {'ccy':ccy,'amt':amt}
+        if to:
+            params = {'to':to}
+        return self._request_with_params(GET, DEPOSIT_LIGHTNING, params)
+
+    # Withdrawal Lightning
+    def withdrawal_lightning(self, ccy,invoice,pwd):
+        params = {'ccy':ccy, 'invoice':invoice, 'pwd':pwd}
+        return self._request_with_params(POST, WITHDRAWAL_LIGHTNING, params)
+
+
+    # GET Obtain account asset valuation
+    def get_asset_valuation(self, ccy):
+        params = {'ccy':ccy}
+        return self._request_with_params(GET, ASSET_VALUATION, params)
+
+    # POST SET LENDING RATE
+    def set_lending_rate(self, ccy,rate):
+        params = {'ccy':ccy,'rate':rate}
+        return self._request_with_params(POST, SET_LENDING_RATE, params)
+
+
+    # GET LENDING HISTORY
+    def get_lending_rate(self, ccy='',before='',after='',limit='',):
+        params = {'ccy': ccy, 'after': after, 'before': before, 'limit': limit,}
+        return self._request_with_params(GET, LENDING_HISTORY, params)
+
+
+    # GET LENDING RATE HISTORY
+    def get_lending_rate_history(self, ccy='',):
+        params = {'ccy': ccy,}
+        return self._request_with_params(GET, LENDING_RATE_HISTORY, params)
+
+    # GET LENDING RATE SUMMARY
+    def get_lending_rate_summary(self, ccy='',before='',after='',limit='',):
+        params = {'ccy': ccy, 'after': after, 'before': before, 'limit': limit,}
+        return self._request_with_params(GET,LENDING_RATE_SUMMARY, params)
+
+class PublicAPI(Client):
+
+    def __init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time=False, flag='1'):
+        Client.__init__(self, api_key, api_secret_key, passphrase, sandbox, use_server_time, flag)
+
+    # Get Instruments
+    def get_instruments(self, instType, uly='', instId=''):
+        params = {'instType': instType, 'uly': uly, 'instId': instId}
+        return self._request_with_params(GET, INSTRUMENT_INFO, params)
+
+    # Get Delivery/Exercise History
+    def get_deliver_history(self, instType, uly, after='', before='', limit=''):
+        params = {'instType': instType, 'uly': uly, 'after': after, 'before': before, 'limit': limit}
+        return self._request_with_params(GET, DELIVERY_EXERCISE, params)
+
+    # Get Open Interest
+    def get_open_interest(self, instType, uly='', instId=''):
+        params = {'instType': instType, 'uly': uly, 'instId': instId}
+        return self._request_with_params(GET, OPEN_INTEREST, params)
+
+    # Get Funding Rate
+    def get_funding_rate(self, instId):
+        params = {'instId': instId}
+        return self._request_with_params(GET, FUNDING_RATE, params)
+
+    # Get Funding Rate History
+    def funding_rate_history(self, instId, after='', before='', limit=''):
+        params = {'instId': instId, 'after': after, 'before': before, 'limit': limit}
+        return self._request_with_params(GET, FUNDING_RATE_HISTORY, params)
+
+    # Get Limit Price
+    def get_price_limit(self, instId):
+        params = {'instId': instId}
+        return self._request_with_params(GET, PRICE_LIMIT, params)
+
+    # Get Option Market Data
+    def get_opt_summary(self, uly, expTime=''):
+        params = {'uly': uly, 'expTime': expTime}
+        return self._request_with_params(GET, OPT_SUMMARY, params)
+
+    # Get Estimated Delivery/Excercise Price
+    def get_estimated_price(self, instId):
+        params = {'instId': instId}
+        return self._request_with_params(GET, ESTIMATED_PRICE, params)
+
+    # Get Discount Rate And Interest-Free Quota
+    def discount_interest_free_quota(self, ccy=''):
+        params = {'ccy': ccy}
+        return self._request_with_params(GET, DICCOUNT_INTETEST_INFO, params)
+
+    # Get System Time
+    def get_system_time(self):
+        return self._request_without_params(GET, SYSTEM_TIME)
+
+    # Get Liquidation Orders
+    def get_liquidation_orders(self, instType, mgnMode='', instId='', ccy='', uly='', alias='', state='', before='',
+                               after='', limit=''):
+        params = {'instType': instType, 'mgnMode': mgnMode, 'instId': instId, 'ccy': ccy, 'uly': uly,
+                  'alias': alias, 'state': state, 'before': before, 'after': after, 'limit': limit}
+        return self._request_with_params(GET, LIQUIDATION_ORDERS, params)
+
+    # Get Mark Price
+    def get_mark_price(self, instType, uly='', instId=''):
+        params = {'instType': instType, 'uly': uly, 'instId': instId}
+        return self._request_with_params(GET, MARK_PRICE, params)
+
+    # Get Tier
+    def get_tier(self, instType, tdMode, uly='', instId='', ccy='', tier=''):
+        params = {'instType': instType, 'tdMode': tdMode, 'uly': uly, 'instId': instId, 'ccy': ccy, 'tier': tier}
+        return self._request_with_params(GET, TIER, params)
+
+    # Get Interest Rate and Loan Quota
+    def get_interest_loan(self):
+        return self._request_without_params(GET, INTEREST_LOAN)
+
+    # Get underlying
+
+    def get_underlying(self, instType):
+        params = {'instType': instType}
+        return self._request_with_params(GET, UNDERLYING, params)
+
+    # GET Obtain the privileged currency borrowing leverage rate and currency borrowing limit
+    def get_vip_interest_rate_loan_quota(self):
+        params = {}
+        return self._request_with_params(GET, VIP_INTEREST_RATE_LOAN_QUOTA, params)
\ No newline at end of file
diff --git a/blankly/exchanges/interfaces/okx/okx_interface.py b/blankly/exchanges/interfaces/okx/okx_interface.py
new file mode 100644
index 00000000..cb235e7d
--- /dev/null
+++ b/blankly/exchanges/interfaces/okx/okx_interface.py
@@ -0,0 +1,402 @@
+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.interfaces.okx.okx_api import MarketAPI, AccountAPI, TradeAPI, ConvertAPI, FundingAPI, PublicAPI
+from blankly.exchanges.orders.limit_order import LimitOrder
+from blankly.exchanges.orders.market_order import MarketOrder
+from blankly.utils.exceptions import APIException, InvalidOrder
+
+
+class OkxInterface(ExchangeInterface):
+    def __init__(self, exchange_name, authenticated_api):
+        self._market: MarketAPI = authenticated_api['market']
+        self._account: AccountAPI = authenticated_api['account']
+        self._trade: TradeAPI = authenticated_api['trade']
+        self._convert: ConvertAPI = authenticated_api['convert']
+        self._funding: FundingAPI = authenticated_api['funding']
+        self._public: PublicAPI = authenticated_api['public']
+        super().__init__(exchange_name, authenticated_api, valid_resolutions=[60, 180, 300, 1800, 3600, 7200,
+                                                                              14400, 21600, 43200, 86400, 604800,
+                                                                              2629746, 7889238, 15778476, 31556952])
+
+    def init_exchange(self):
+        # This is purely an authentication check which can be disabled in settings
+        # todo: change get_fee_rates
+        fees = self._account.get_fee_rates(instType='SPOT', instId='BTC-USDT')
+        try:
+            if fees['msg'] == "Invalid API Key":
+                raise LookupError("Invalid API Key. Please check if the keys were input correctly into your "
+                                  "keys.json.")
+        except KeyError:
+            pass
+
+    def get_products(self):
+        instrument_type = "SPOT"
+        needed = self.needed['get_products']
+        products = self._public.get_instruments(instrument_type)
+
+        for i in range(len(products['data'])):
+            products['data'][i]["symbol"] = products['data'][i]["instId"]
+            products['data'][i]["base_asset"] = products['data'][i]["baseCcy"]
+            products['data'][i]["quote_asset"] = products['data'][i]["quoteCcy"]
+            products['data'][i]["base_min_size"] = products['data'][i]["minSz"]
+            products['data'][i]["base_max_size"] = 1000000000
+            products['data'][i]["base_increment"] = products['data'][i]["tickSz"]
+            products['data'][i] = utils.isolate_specific(needed, products['data'][i])
+        return products['data']
+
+
+    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)
+        needed = self.needed['get_account']
+        accounts = self._account.get_account()
+
+        parsed_dictionary = utils.AttributeDict({})
+        #We have to sort through it if the accounts are none
+        if symbol is not None:
+            accounts_specific = self._account.get_account(symbol)
+            if accounts_specific['code'] == 51000 or accounts_specific['code'] == 51001:
+                raise ValueError("Symbol not found")
+            for i in accounts_specific['data']:
+                parsed_value = utils.isolate_specific(needed, i)
+                dictionary = utils.AttributeDict({
+                    'available': float(parsed_value['exchange_specific']['details'][0]['availBal']), # accounts_specific['data'][0]['details'][0]['availBal'],
+                    'hold': float(parsed_value['exchange_specific']['details'][0]['frozenBal'])
+                })
+                return dictionary
+        for i in range(len(accounts['data'][0]['details'])):
+            parsed_dictionary[accounts['data'][0]['details'][i]['ccy']] = utils.AttributeDict({
+                'available': float(accounts['data'][0]['details'][i]['availBal']),
+                'hold': float(accounts['data'][0]['details'][i]['frozenBal'])
+            })
+
+        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']
+        """
+            {
+                "result": "true",
+                "request_id": "",
+                "client_oid": "b1234",
+                "order_id": "131801215763791872",
+                "error_message": "",
+                "error_code": "0"
+            }
+        """
+        order = {
+            'symbol': symbol,
+            'size': size,
+            'side': side,
+            'type': 'market'
+        }
+
+        response = self._trade.place_order(symbol, 'cash', side, 'market', size)
+        if len(response['data'][0]['sMsg']) != 0:
+            raise InvalidOrder("Invalid Order: " + response['data'][0]["sMsg"])
+        response["created_at"] = time.time()
+        response["id"] = response['data'][0]['ordId']
+        response["status"] = response['data'][0]['sCode']
+        response["symbol"] = symbol
+        response["size"] = size
+        response["side"] = side
+        response["type"] = 'market'
+        response = utils.isolate_specific(needed, response)
+        return MarketOrder(order, response, 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.place_order(symbol, 'cash', side, 'limit', size, px=price)
+        if len(response['data'][0]['sMsg']) != 0:
+            raise InvalidOrder("Invalid Order: " + response['data'][0]["sMsg"])
+
+        response_details = self._trade.get_orders(symbol, ordId=response['data'][0]['ordId'])
+
+        response_details["created_at"] = response_details['data'][0]['cTime']
+        response_details["id"] = response_details['data'][0]['ordId']
+        response_details["status"] = response_details['data'][0]['state']
+        response_details["symbol"] = response_details['data'][0]['instId']
+        response_details["size"] = response_details['data'][0]['sz']
+        response_details["side"] = response_details['data'][0]['side']
+        response_details["price"] = response_details['data'][0]['px']
+        response_details["type"] = response_details['data'][0]['ordType']
+        response_details["time_in_force"] = 'GTC'
+
+        response_details = utils.isolate_specific(needed, response_details)
+        return LimitOrder(order, response_details, self)
+
+    def cancel_order(self, symbol: str, order_id: str) -> dict:
+        """
+        Returns:
+            dict: Containing the order_id of cancelled order. Example::
+            { "client_oid": "order123", }
+        """
+        response = self._trade.cancel_order(symbol, ordId=order_id)
+        return {"order_id": response['data'][0]['ordId']}
+
+    def get_open_orders(self,
+                        symbol: str = None) -> list:
+        """
+        List open orders
+        """
+        """
+        [
+            {
+                "client_oid":"oktspot86",
+                "created_at":"2019-03-20T03:28:14.000Z",
+                "filled_notional":"0",
+                "filled_size":"0",
+                "funds":"",
+                "instrument_id":"BTC-USDT",
+                "notional":"",
+                "order_id":"2511109744100352",
+                "order_type":"0",
+                "price":"3594.7",
+                "price_avg":"",
+                "product_id":"BTC-USDT",
+                "side":"buy",
+                "size":"0.001",
+                "status":"open",
+                "fee_currency":"BTC",
+                "fee":"-0.01",
+                "rebate_currency":"open",
+                "rebate":"0.05",
+                "state":"0",
+                "timestamp":"2019-03-20T03:28:14.000Z",
+                "type":"limit"
+            }
+        ]
+        """
+        if symbol is None:
+            orders = self._trade.get_order_list()
+        else:
+            orders = self._trade.get_order_list(instId=symbol)
+
+        if len(orders['data']) == 0:
+            return []
+        if orders['code'].startswith('5'):
+            raise InvalidOrder("Invalid Order: " + str(orders))
+
+        for i in range(len(orders['data'])):
+            orders['data'][i]["created_at"] = orders['data'][i]["cTime"]
+            needed = self.choose_order_specificity(orders['data'][i]['ordType'])
+            orders['data'][i]["symbol"] = orders['data'][i]['instId']
+            orders['data'][i]["id"] = orders['data'][i]['ordId']
+            orders['data'][i]["status"] = orders['data'][i]['state']
+            orders['data'][i]["size"] = orders['data'][i]['sz']
+            orders['data'][i]["price"] = orders['data'][i]['px']
+            orders['data'][i]["type"] = orders['data'][i]["ordType"]
+            if orders['data'][i]["ordType"] == "limit":
+                orders['data'][i]["time_in_force"] = 'GTC'
+            orders['data'][i] = utils.isolate_specific(needed, orders['data'][i])
+
+        return orders['data']
+
+    def get_order(self, symbol, order_id) -> dict:
+        response = self._trade.get_orders(symbol, ordId=order_id)
+
+        if 'message' in response:
+            raise APIException("Invalid: " + str(response['msg']) + ", was the order canceled?")
+
+        if response['data'][0]['ordType'] == 'market':
+            needed = self.needed['market_order']
+        elif response['data'][0]['ordType'] == 'limit':
+            needed = self.needed['limit_order']
+            response['price'] = response['data'][0]['px']
+        else:
+            needed = self.needed['market_order']
+
+        response["symbol"] = response['data'][0]['instId']
+        response["created_at"] = response['data'][0]['cTime']
+        response["size"] = response['data'][0]['sz']
+        response["side"] = response['data'][0]['side']
+        response["type"] = response['data'][0]['ordType']
+        response["id"] = response['data'][0]['ordId']
+        response["status"] = response['data'][0]['state']
+        response["time_in_force"] = 'GTC'
+        return utils.isolate_specific(needed, response)
+
+    def get_fees(self, symbol) -> dict:
+        needed = self.needed['get_fees']
+        """
+        {
+            "category": "1",
+            "maker": "0.0002",
+            "taker": "0.0005",
+            "timestamp": "2019-12-11T11:02:31.360Z"
+        }
+        """
+        if symbol is None:
+            raise ValueError("There was no symbol inputted, please try again.")
+        else:
+            fees = self._account.get_fee_rates(instType="SPOT", instId=symbol)
+        fees['taker_fee_rate'] = fees['data'][0]['taker']
+        fees['maker_fee_rate'] = fees['data'][0]['maker']
+        return utils.isolate_specific(needed, fees)
+
+    def overridden_history(self, symbol, epoch_start, epoch_stop, resolution, **kwargs) -> pd.DataFrame:
+        """
+        Okx is strange
+        """
+        to = kwargs['to']
+
+        if isinstance(to, str):
+            epoch_start = epoch_start - resolution
+        elif isinstance(to, int):
+            epoch_start = epoch_start - resolution
+
+        return self.get_product_history(symbol, epoch_start, epoch_stop, resolution)
+
+    def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
+        if epoch_stop > time.time():
+            epoch_stop = int(time.time())
+
+        resolution = blankly.time_builder.time_interval_to_seconds(resolution)
+
+        init_epoch_start = int(epoch_start * 1000)
+        init_epoch_stop = int(epoch_stop * 1000)
+
+        accepted_grans = [60, 180, 300, 1800, 3600, 7200, 14400, 21600, 43200, 86400, 604800, 2629746, 7889238, 15778476, 31556952]
+
+        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)
+
+        lookup_dict = {
+            60: '1m',
+            180: '3m',
+            300: '5m',
+            900: '15m',
+            1800: '30m',
+            3600: '1H',
+            7200: '2H',
+            14400: '4H',
+            21600: '6H',
+            43200: '12H',
+            86400: '1D',
+            604800: '1W',
+            2629746: '1M',
+            7889238: '3M',
+            15778476: '6M',
+            31556952: '1Y'
+        }
+
+        gran_string = lookup_dict[resolution]
+
+        history = []
+        while init_epoch_start < init_epoch_stop + resolution * 100 * 1100:
+            response = self._market.get_history_candlesticks(symbol, after=init_epoch_start, bar=gran_string, limit=100)
+
+            if len(response['data']) != 0:
+                history = history + response['data']
+            init_epoch_start = init_epoch_start + (100 * 1000 * resolution)
+
+        history.sort(key=lambda x: x[0])
+        new_history = list(set(tuple(sub) for sub in history))
+        new_history.sort(key=lambda x: x[0])
+        df = pd.DataFrame(new_history, columns=['time', 'open', 'high', 'low', 'close', 'volume', 'volume_currency'])
+        df = df.drop(columns=['volume_currency'])
+        df[['time']] = df[['time']].astype('int64').div(1000).astype(int)
+        df[['open', 'high', 'low', 'close', 'volume']] = df[['open', 'high', 'low', 'close', 'volume']].astype(float)
+        # df.plot(x='time', y='close', kind='scatter')
+        df = df[df['time'] >= epoch_start]
+        df = df[df['time'] <= epoch_stop]
+        # plt.show()
+
+        return df
+
+    def get_order_filter(self, symbol: str) -> dict:
+        instrument_type = "SPOT"
+        response = self._public.get_instruments(instrument_type)
+        products = None
+        for i in response["data"]:
+            if i["instId"] == symbol:
+                products = i
+                break
+
+        base_min_size = float(products.pop('minSz'))
+        base_increment = float(products.pop('tickSz'))
+
+        return {
+            "symbol": products.pop('uly'),
+            "base_asset": products.pop('baseCcy'),
+            "quote_asset": products.pop('quoteCcy'),
+            "max_orders": 1000000000000,
+            "limit_order": {
+                "base_min_size": base_min_size,  # Minimum size to buy
+                "base_max_size": 100000000,  # Maximum size to buy, this value was hardcoded
+                "base_increment": base_increment,  # Specifies the minimum increment for the base_asset.
+                "price_increment": 0.1,
+
+                "min_price": 0.1,
+                "max_price": 9999999999,
+            },
+            'market_order': {
+                "fractionable": True,
+
+                "base_min_size": base_min_size,  # Minimum size to buy
+                "base_max_size": 100000000,  # Maximum size to buy, this value was hardcoded
+                "base_increment": base_increment,  # Specifies the minimum increment
+
+                "quote_increment": 0.1,  # Specifies the min order price as well as the price increment.
+                "buy": {
+                    "min_funds": 0.1,  # This value was hardcoded
+                    "max_funds": 1000000,  # This value was hardcoded
+                },
+                "sell": {
+                    "min_funds": 0.1,  # This value was hardcoded
+                    "max_funds": 1000000,  # This value was hardcoded
+                },
+            },
+            "exchange_specific": {**products}
+        }
+
+    def get_price(self, symbol) -> float:
+        """
+        Returns just the price of a currency pair.
+        """
+        response = self._market.get_index_ticker(instId=symbol)
+        if len(response['msg']) != 0:
+            raise APIException("Error: " + response['msg'])
+        return float(response['data'][0]['idxPx'])
+
+
diff --git a/blankly/exchanges/interfaces/okx/okx_websocket.py b/blankly/exchanges/interfaces/okx/okx_websocket.py
new file mode 100644
index 00000000..c4d406d3
--- /dev/null
+++ b/blankly/exchanges/interfaces/okx/okx_websocket.py
@@ -0,0 +1,122 @@
+"""
+    Implementation for Okx Websockets
+    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 json
+import traceback
+
+import blankly.exchanges.interfaces.okx.okx_websocket_utils as websocket_utils
+from blankly.exchanges.interfaces.websocket import Websocket
+from blankly.utils.utils import info_print
+
+
+class Tickers(Websocket):
+    def __init__(self, symbol, stream, log=None,
+                 pre_event_callback=None, initially_stopped=False, WEBSOCKET_URL="wss://ws.okx.com:8443/ws/v5/public",
+                 **kwargs):
+        """
+        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, kwargs)
+
+        self.__pre_event_callback_filled = False
+
+        # 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
+            )
+        self.checked = False
+
+    def read_websocket(self):
+        # Main thread to sit here and run
+        self.ws.run_forever()
+
+    def on_message(self, ws, message):
+        received_dict = json.loads(message)
+        if len(received_dict) == 2 and self.checked is not True:
+            info_print(f"Subscribed to {received_dict['arg']['channel']}")
+            self.checked = True
+            return
+        if (self.pre_event_callback is not None) and (not self.__pre_event_callback_filled) and \
+                (self.stream == "books"):
+            if received_dict['action'] == 'snapshot':
+                try:
+                    self.pre_event_callback(received_dict)
+                except Exception:
+                    traceback.print_exc()
+
+            self.__pre_event_callback_filled = True
+            return
+
+        self.most_recent_time = received_dict['data'][0]["ts"]
+        received_dict['data'][0]["ts"] = self.most_recent_time
+        self.time_feed.append(self.most_recent_time)
+        self.log_response(self.__logging_callback, received_dict['data'][0])
+
+        # Manage price events and fire for each manager attached
+        if self.stream == "books":
+            interface_message = self.__interface_callback(received_dict)
+        else: # self.stream == 'tickers':
+            interface_message = self.__interface_callback(received_dict['data'][0])
+        self.ticker_feed.append(interface_message)
+        self.most_recent_tick = interface_message
+
+        try:
+            for i in self.callbacks:
+                i(interface_message, **self.kwargs)
+        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',
+            'args': [{
+                'channel': self.stream,
+                'instId': 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/okx/okx_websocket_utils.py b/blankly/exchanges/interfaces/okx/okx_websocket_utils.py
new file mode 100644
index 00000000..7805e78a
--- /dev/null
+++ b/blankly/exchanges/interfaces/okx/okx_websocket_utils.py
@@ -0,0 +1,51 @@
+import time
+import blankly.utils.utils as utils
+
+
+def switch_type(stream):
+    if stream == "tickers":
+        return trade, \
+               trade_interface, \
+               "time, system_time, price, open_24h, volume_24h, low_24h, high_24h, best_bid, best_ask," \
+               "last_traded_price, last_traded_size\n"
+
+    elif stream == "books":
+        return no_callback, \
+               no_callback, \
+               ""
+    else:
+        return no_callback, no_callback, ""
+
+
+def no_callback(message):
+    return message
+
+
+def trade(received):
+    line = str(received['data']['ts'] / 1000) + "," + str(time.time()) + "," + received['data']['sodUtc0'] + "," + \
+           received[
+               'data']['open24h'] + "," + received['data']['volCcy24h'] + "," + received['data']['low24h'] + "," + \
+           received[
+               'data']['high24h'] + "," + received['data']['bidSz'] + "," + received['data']['askSz'] + "," + received[
+               'data']['last'] + "," + received['data']['lastSz'] + "\n"
+    return line
+
+
+def trade_interface(message):
+    needed = [
+        ["symbol", str],
+        ["price", float],
+        ["time", float],
+        ["trade_id", int],
+        ["size", float]
+    ]
+
+    symbol = message['instId']
+    new_symbol = '-'.join(symbol.split('-')[:2])
+    message['symbol'] = new_symbol
+    message['size'] = message['lastSz']
+    message['trade_id'] = None
+    message['price'] = message['last']
+    message['time'] = message['ts']
+
+    return utils.isolate_specific(needed, message)
diff --git a/blankly/exchanges/interfaces/paper_trade/paper_trade.py b/blankly/exchanges/interfaces/paper_trade/paper_trade.py
index 5d3f2d62..e2bc3ef8 100644
--- a/blankly/exchanges/interfaces/paper_trade/paper_trade.py
+++ b/blankly/exchanges/interfaces/paper_trade/paper_trade.py
@@ -63,7 +63,7 @@ def get_exchange_state(self):
         Exchange state is the external properties for the exchange block
         """
         # TODO Populate this with useful information
-        return self.interface.get_fees()
+        return self.interface.get_products()
 
     def get_direct_calls(self):
         return self.calls
diff --git a/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py b/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
index 7f2af8e7..7c768332 100644
--- a/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
+++ b/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
@@ -43,11 +43,9 @@ def __init__(self, derived_interface: ABCExchangeInterface, initial_account_valu
         self.market_order_execution_details = []
 
         self.get_products_cache = None
-        self.get_fees_cache = None
+        self.get_fees_cache = {}
         self.get_order_filter_cache = {}
 
-        self.__exchange_properties = None
-
         self.__run_watchdog = True
 
         self.__thread = None
@@ -56,37 +54,48 @@ def __init__(self, derived_interface: ABCExchangeInterface, initial_account_valu
         ExchangeInterface.__init__(self, derived_interface.get_exchange_type(), derived_interface)
         BacktestingWrapper.__init__(self)
 
-        # Write in the accounts to our local account. This involves getting the values directly from the exchange
-        accounts = self.calls.get_account()
-
-        # If it's given an initial account value dictionary, write all the account values that we passed in to the
-        # function to the account, then default everything else to zero.
-        if initial_account_values is not None:
-            for i in accounts.keys():
-                if i in initial_account_values.keys():
-                    accounts[i] = utils.AttributeDict({
-                        'available': initial_account_values[i],
-                        'hold': 0.0
-                    })
-                else:
-                    accounts[i] = utils.AttributeDict({
-                        'available': 0.0,
-                        'hold': 0.0
-                    })
-
-        # Initialize the local account
-        self.local_account = LocalAccount(accounts)
+        self.__local_account_cache = None
+        self.__initial_account_values = initial_account_values
 
         # Initialize our traded assets list
         self.traded_assets = []
 
-        self.evaluate_traded_account_assets()
+        # self.evaluate_traded_account_assets()
 
         self.__enable_shorting = self.user_preferences['settings']['alpaca']['enable_shorting']
 
+    @property
+    def local_account(self):
+        if self.__local_account_cache is None:
+            if self.get_exchange_type() == 'keyless':
+                accounts = utils.add_all_products({}, self.get_products())
+            else:
+                # Write in the accounts to our local account. This involves getting the values directly from the
+                # exchange
+                accounts = self.calls.get_account()
+            # If it's given an initial account value dictionary, write all the account values that we passed in to the
+            # function to the account, then default everything else to zero.
+            if self.__initial_account_values is not None:
+                for i in accounts.keys():
+                    if i in self.__initial_account_values.keys():
+                        accounts[i] = utils.AttributeDict({
+                            'available': self.__initial_account_values[i],
+                            'hold': 0.0
+                        })
+                    else:
+                        accounts[i] = utils.AttributeDict({
+                            'available': 0.0,
+                            'hold': 0.0
+                        })
+
+            # Initialize the local account
+            self.__local_account_cache = LocalAccount(accounts)
+            return self.__local_account_cache
+        else:
+            return self.__local_account_cache
+
     def evaluate_traded_account_assets(self):
         # Because alpaca has so many columns we need to optimize to perform an accurate backtest
-        self.traded_assets = []
         accounts = self.get_account()
 
         for i in accounts.keys():
@@ -122,19 +131,11 @@ def __check_trading_assets(self, symbol: str):
 
     def init_exchange(self):
         try:
-            fees = self.calls.get_fees()
+            self.calls.get_exchange_type()
         except AttributeError:
             traceback.print_exc()
             raise AttributeError("Are you passing a non-exchange object into the paper trade constructor?")
 
-        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):
@@ -312,20 +313,14 @@ def evaluate_paper_trade(self, order, current_price):
             order (dict): Order dictionary to derive the order attributes
             current_price (float): The current price of the currency pair the limit order was created on
         """
-        if self.__exchange_properties is None:
-            self.init_exchange()
-
         funds = order['size'] * current_price
-        executed_value = funds - funds * float((self.__exchange_properties["maker_fee_rate"]))
-        fill_fees = funds * float((self.__exchange_properties["maker_fee_rate"]))
-        fill_size = order['size'] - order['size'] * float((self.__exchange_properties["maker_fee_rate"]))
-
-        # order['executed_value'] = str(executed_value)
-        # order['fill_fees'] = str(fill_fees)
-        # order['filled_size'] = str(fill_size)
+        executed_value = funds - funds * float((self.get_fees(order['symbol'])["maker_fee_rate"]))
+        fill_fees = funds * float((self.get_fees(order['symbol'])["maker_fee_rate"]))
+        fill_size = order['size'] - order['size'] * float((self.get_fees(order['symbol'])["maker_fee_rate"]))
 
         return order, funds, executed_value, fill_fees, fill_size
 
+    @utils.enforce_base_asset
     def get_account(self, symbol=None) -> utils.AttributeDict:
         if symbol is None:
             return self.local_account.get_accounts()
@@ -419,16 +414,16 @@ def market_order(self, symbol, side, size) -> MarketOrder:
         }
         response = utils.isolate_specific(needed, response)
         self.paper_trade_orders.append(response)
-
-        if self.__exchange_properties is None:
-            self.init_exchange()
+        # Identify the trade also by exchange
+        if self.backtesting:
+            self.paper_trade_orders[-1]['exchange'] = self.get_exchange_type()
 
         if side == "buy":
             self.local_account.trade_local(symbol=symbol,
                                            side=side,
                                            base_delta=utils.trunc(qty -
                                                                   qty *
-                                                                  float((self.__exchange_properties["taker_fee_rate"])),
+                                                                  float((self.get_fees(symbol)["taker_fee_rate"])),
                                                                   quantity_decimals),
                                            # Gain filled size after fees
                                            quote_delta=utils.trunc(funds * -1,
@@ -440,7 +435,7 @@ def market_order(self, symbol, side, size) -> MarketOrder:
                                            base_delta=utils.trunc(float(qty * -1), quantity_decimals),
                                            # Loose size before any fees
                                            quote_delta=utils.trunc(funds - funds *
-                                                                   float((self.__exchange_properties[
+                                                                   float((self.get_fees(symbol)[
                                                                        "taker_fee_rate"])),
                                                                    base_decimals),  # Gain executed value after fees
                                            quote_resolution=base_decimals, base_resolution=quantity_decimals)
@@ -558,6 +553,8 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
         }
         response = utils.isolate_specific(needed, response)
         self.paper_trade_orders.append(response)
+        # Identify the trade also by exchange
+        self.paper_trade_orders[-1]['exchange'] = self.get_exchange_type()
 
         base = utils.get_base_asset(symbol)
         quote = utils.get_quote_asset(symbol)
@@ -578,12 +575,15 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
             # Gain size on hold when selling
             hold = self.local_account.get_account(base)['hold']
             self.local_account.update_hold(base, hold + size)
+        else:
+            raise APIException(f"Invalid side {side}")
         return LimitOrder(order, response, self)
 
     def cancel_order(self, symbol, order_id) -> dict:
         """
         This block could potentially work for both exchanges
         """
+        del symbol
         order_index = None
         for i in range(len(self.paper_trade_orders)):
             index = self.paper_trade_orders[i]
@@ -592,7 +592,34 @@ def cancel_order(self, symbol, order_id) -> dict:
                 break
 
         if order_index is not None:
-            order_id = self.paper_trade_orders[order_index]['id']
+            # Now that we found it make sure that we move the funds back on available
+            order = self.paper_trade_orders[order_index]
+            side = order['side']
+            size = order['size']
+            symbol = order['symbol']
+            price = order['price']
+            base_asset = utils.get_base_asset(symbol)
+            quote_asset = utils.get_quote_asset(symbol)
+
+            if side == 'buy':
+                # When you cancel on the buy side you get those funds back in available
+                available = self.local_account.get_account(quote_asset)['available']
+                self.local_account.update_available(quote_asset, available + (size * price))
+
+                # And loose them on hold
+                hold = self.local_account.get_account(quote_asset)['hold']
+                self.local_account.update_hold(quote_asset, hold - (size * price))
+            elif side == 'sell':
+                # Canceling a sell you gain the size back into available
+                available = self.local_account.get_account(base_asset)['available']
+                self.local_account.update_available(base_asset, available + size)
+
+                # And you loose it in the hold
+                hold = self.local_account.get_account(base_asset)['hold']
+                self.local_account.update_hold(base_asset, hold - size)
+
+            # This saves the order
+            order_id = order['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({
@@ -602,6 +629,8 @@ def cancel_order(self, symbol, order_id) -> dict:
 
             del self.paper_trade_orders[order_index]
             return {"order_id": order_id}
+        else:
+            raise APIException("Order ID not found.")
 
     def get_open_orders(self, symbol=None):
         open_orders = []
@@ -616,23 +645,60 @@ def get_order(self, symbol, order_id) -> dict:
                 return i
 
     def get_products(self):
+        def get_keyless_products():
+            symbols_ = []
+            for full_symbol in self.full_prices:
+                symbols_.append({
+                    'symbol': full_symbol
+                })
+            self.get_products_cache = symbols_
         if self.backtesting:
             if self.get_products_cache is None:
-                self.get_products_cache = self.calls.get_products()
+                if self.get_exchange_type() == 'keyless':
+                    get_keyless_products()
+                else:
+                    self.get_products_cache = self.calls.get_products()
             return self.get_products_cache
         else:
-            return self.calls.get_products()
+            if self.get_exchange_type() == 'keyless':
+                get_keyless_products()
+                return self.get_products_cache
+            else:
+                return self.calls.get_products()
 
-    def get_fees(self):
+    def get_fees(self, symbol):
         if self.backtesting:
-            if self.get_fees_cache is None:
-                self.get_fees_cache = self.calls.get_fees()
-            return self.get_fees_cache
+            # Add exchanges that actually require a symbol
+            type_ = self.get_exchange_type()
+            if type_ == 'okx' or type_ == 'binance':
+                if symbol not in self.get_fees_cache:
+                    self.get_fees_cache[symbol] = self.calls.get_fees(symbol)
+                    return self.get_fees_cache[symbol]
+                else:
+                    return self.get_fees_cache[symbol]
+            # If it doesn't require a symbol just store it in the root
+            else:
+                if self.get_fees_cache == {}:
+                    self.get_fees_cache = self.calls.get_fees(symbol)
+                    return self.get_fees_cache
+                else:
+                    return self.get_fees_cache
         else:
-            return self.calls.get_fees()
+            return self.calls.get_fees(symbol)
 
     def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
-        return self.calls.get_product_history(symbol, epoch_start, epoch_stop, resolution)
+        if self.backtesting:
+            if symbol in self.full_prices:
+                if resolution in self.full_prices[symbol]:
+                    price_set = self.full_prices[symbol][resolution]
+                else:
+                    raise LookupError(f"The resolution {resolution} not found or downloaded for {symbol}.")
+            else:
+                raise LookupError(f"Prices for this symbol ({symbol}) not found")
+
+            return utils.trim_df_time_column(price_set, epoch_start - resolution, epoch_stop)
+        else:
+            return self.calls.get_product_history(symbol, epoch_start, epoch_stop, resolution)
 
     def get_order_filter(self, symbol):
         # Don't re-query order filter if its cached
diff --git a/blankly/exchanges/interfaces/websocket.py b/blankly/exchanges/interfaces/websocket.py
index f51208ea..526a3f38 100644
--- a/blankly/exchanges/interfaces/websocket.py
+++ b/blankly/exchanges/interfaces/websocket.py
@@ -27,9 +27,10 @@
 
 
 class Websocket(ABCExchangeWebsocket, abc.ABC):
-    def __init__(self, symbol, stream, log, log_message, url, pre_event_callback):
+    def __init__(self, symbol, stream, log, log_message, url, pre_event_callback, kwargs):
         self.symbol = symbol
         self.stream = stream
+        self.kwargs = kwargs
 
         # Initialize log file
         if log is not None:
diff --git a/blankly/exchanges/managers/general_stream_manager.py b/blankly/exchanges/managers/general_stream_manager.py
index 3be2cfee..147e708d 100644
--- a/blankly/exchanges/managers/general_stream_manager.py
+++ b/blankly/exchanges/managers/general_stream_manager.py
@@ -15,10 +15,17 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
+import random
+
+import requests
+
 import blankly.utils.utils
 from blankly.exchanges.interfaces.alpaca.alpaca_websocket import Tickers as Alpaca_Websocket
 from blankly.exchanges.interfaces.binance.binance_websocket import Tickers as Binance_Websocket
 from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket import Tickers as Coinbase_Pro_Websocket
+from blankly.exchanges.interfaces.kucoin.kucoin_websocket import Tickers as Kucoin_Websocket
+from blankly.exchanges.interfaces.ftx.ftx_websocket import Tickers as Ftx_Websocket
+from blankly.exchanges.interfaces.okx.okx_websocket import Tickers as Okx_Websocket
 from blankly.exchanges.managers.websocket_manager import WebsocketManager
 
 
@@ -66,6 +73,49 @@ def create_general_connection(self, callback, channel, log=None, override_symbol
             self.__websockets[channel][exchange_cache][asset_id_cache] = websocket
 
             return websocket
+        elif exchange_cache == "kucoin":
+            if use_sandbox:
+                response = requests.post('https://trade-sandbox.kucoin.com/_api/bullet-usercenter/v1/bullet-public'
+                                         ).json()
+
+                base_endpoint = response['data']['instanceServers'][0]['endpoint']
+                token = response['data']['token']
+                websocket = Kucoin_Websocket(asset_id_cache, channel, f"{base_endpoint}/socket.io/?token={token}", log)
+                # wss://push-socketio-sandbox.kucoin.com
+            else:
+                response = requests.post('https://api.kucoin.com/api/v1/bullet-public').json()
+
+                base_endpoint = response['data']['instanceServers'][0]['endpoint']
+                token = response['data']['token']
+
+                websocket = Kucoin_Websocket(asset_id_cache, channel,
+                                             f"{base_endpoint}/api/v1/?token={token}&[connectId="
+                                             f"{random.randint(1, 100000000) * 100000000}]", log)
+            websocket.append_callback(callback)
+
+            self.__websockets[channel][exchange_cache][asset_id_cache] = websocket
+
+            return websocket
+        elif exchange_cache == "ftx":
+            if use_sandbox:
+                raise ValueError("Error: FTX does not have a sandbox mode")
+            else:
+                websocket = Ftx_Websocket(override_symbol, asset_id_cache, channel, log)
+
+            websocket.append_callback(callback)
+
+            self.__websockets[channel][exchange_cache][asset_id_cache] = websocket
+
+            return websocket
+        elif exchange_cache == "okx":
+            if use_sandbox:
+                websocket = Okx_Websocket(asset_id_cache, channel, log,
+                                          WEBSOCKET_URL="wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999")
+            else:
+                websocket = Okx_Websocket(asset_id_cache, channel, log)
+            websocket.append_callback(callback)
+
+            self.__websockets[channel][exchange_cache][asset_id_cache] = websocket
         elif exchange_cache == "binance":
             # Lower this to subscribe
             asset_id_cache = blankly.utils.to_exchange_symbol(asset_id_cache, "binance").lower()
diff --git a/blankly/exchanges/managers/orderbook_manager.py b/blankly/exchanges/managers/orderbook_manager.py
index 801767c9..a1b05e61 100644
--- a/blankly/exchanges/managers/orderbook_manager.py
+++ b/blankly/exchanges/managers/orderbook_manager.py
@@ -17,6 +17,7 @@
 """
 import traceback
 import warnings
+import random
 from typing import List
 
 import requests
@@ -26,6 +27,9 @@
 from blankly.exchanges.interfaces.alpaca.alpaca_websocket import Tickers as Alpaca_Websocket
 from blankly.exchanges.interfaces.binance.binance_websocket import Tickers as Binance_Orderbook
 from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket import Tickers as Coinbase_Pro_Orderbook
+from blankly.exchanges.interfaces.kucoin.kucoin_websocket import Tickers as Kucoin_Orderbook
+from blankly.exchanges.interfaces.ftx.ftx_websocket import Tickers as Ftx_Orderbook
+from blankly.exchanges.interfaces.okx.okx_websocket import Tickers as Okx_Orderbook
 from blankly.exchanges.managers.websocket_manager import WebsocketManager
 
 
@@ -150,6 +154,90 @@ def create_orderbook(self, callback,
                 "asks": []
             }
             return websocket
+        elif exchange_name == "ftx":
+            if override_symbol is None:
+                override_symbol = self.__default_currency
+
+            override_symbol = blankly.utils.to_exchange_symbol(override_symbol, "ftx")
+
+            if use_sandbox:
+                raise ValueError("Error: FTX does not have a sandbox mode")
+            else:
+                websocket = Ftx_Orderbook(override_symbol, "orderbook",
+                                          pre_event_callback=self.ftx_snapshot_update,
+                                          initially_stopped=initially_stopped,
+                                          )
+
+            websocket.append_callback(self.ftx_update)
+
+            # Store this object
+            self.__websockets['ftx'][override_symbol] = websocket
+            self.__websockets_callbacks['ftx'][override_symbol] = [callback]
+            self.__websockets_kwargs['ftx'][override_symbol] = kwargs
+            self.__orderbooks['ftx'][override_symbol] = {
+                "bids": [],
+                "asks": []
+            }
+            return websocket
+        elif exchange_name == "kucoin":
+            if override_symbol is None:
+                override_symbol = self.__default_currency
+
+            request_data = (requests.post('https://api.kucoin.com/api/v1/bullet-public').json())
+
+            if use_sandbox:
+                base_endpoint = request_data['data']['instanceServers'][0]['endpoint']
+                token = request_data['data']['token']
+                websocket = Kucoin_Orderbook(override_symbol, "level2",
+                                             pre_event_callback=self.kucoin_snapshot_update,
+                                             initially_stopped=initially_stopped,
+                                             websocket_url=f"{base_endpoint}/socket.io/?token={token}")
+            else:
+                base_endpoint = request_data['data']['instanceServers'][0]['endpoint']
+                token = request_data['data']['token']
+                websocket = Kucoin_Orderbook(override_symbol, "level2",
+                                             pre_event_callback=self.kucoin_snapshot_update,
+                                             initially_stopped=initially_stopped,
+                                             websocket_url=f"{base_endpoint}?token={token}&[connectId="
+                                                           f"{random.randint(1, 200000000) * 100000000}]"
+                                             )
+            # This is where the sorting magic happens
+            websocket.append_callback(self.kucoin_update)
+
+            # Store this object
+            self.__websockets['kucoin'][override_symbol] = websocket
+            self.__websockets_callbacks['kucoin'][override_symbol] = [callback]
+            self.__websockets_kwargs['kucoin'][override_symbol] = kwargs
+            self.__orderbooks['kucoin'][override_symbol] = {
+                "bids": [],
+                "asks": []
+            }
+
+        elif exchange_name == "okx":
+            if override_symbol is None:
+                override_symbol = self.__default_currency
+
+            if use_sandbox:
+                websocket = Okx_Orderbook(override_symbol, "books",
+                                          pre_event_callback=self.okx_snapshot_update,
+                                          initially_stopped=initially_stopped,
+                                          WEBSOCKET_URL="wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999")
+            else:
+                websocket = Okx_Orderbook(override_symbol, "books",
+                                          pre_event_callback=self.okx_snapshot_update,
+                                          initially_stopped=initially_stopped
+                                          )
+
+            websocket.append_callback(self.okx_update)
+            self.__websockets['okx'][override_symbol] = websocket
+            self.__websockets_callbacks['okx'][override_symbol] = [callback]
+            self.__websockets_kwargs['okx'][override_symbol] = kwargs
+            self.__orderbooks['okx'][override_symbol] = {
+                "bids": [],
+                "asks": []
+            }
+            return websocket
+
         elif exchange_name == "binance":
             if override_symbol is None:
                 override_symbol = self.__default_currency
@@ -208,6 +296,66 @@ def create_orderbook(self, callback,
         else:
             print(exchange_name + " ticker not supported, skipping creation")
 
+    def ftx_update(self, update):
+        symbol = update['symbol']
+
+        book_buys = self.__orderbooks['ftx'][symbol]['bids']
+        book_sells = self.__orderbooks['ftx'][symbol]['asks']  # type: list
+
+        new_buys = update['bids'][::-1]  # type: list
+        for i in new_buys:
+            i[0] = float(i[0])
+            i[1] = float(i[1])
+            if i[1] == 0:
+                book_buys = remove_price(book_buys, i[0])
+            else:
+                book_buys.append((i[0], i[1]))
+
+        new_sells = update['asks']  # type: list
+        for i in new_sells:
+            i[0] = float(i[0])
+            i[1] = float(i[1])
+            if i[1] == 0:
+                book_sells = remove_price(book_sells, i[0])
+            else:
+                book_sells.append((i[0], i[1]))
+
+        # Now sort them
+        book_buys = sort_list_tuples(book_buys)
+        book_sells = sort_list_tuples(book_sells)
+
+        self.__orderbooks['ftx'][symbol]['bids'] = book_buys
+        self.__orderbooks['ftx'][symbol]['asks'] = book_sells
+
+        # Pass in this new updated orderbook
+        callbacks = self.__websockets_callbacks['ftx'][symbol]
+        for i in callbacks:
+            i(self.__orderbooks['ftx'][symbol],
+              **self.__websockets_kwargs['ftx'][symbol])
+
+    def ftx_snapshot_update(self, update):
+        market = update['market'].replace('/', '-')
+        print("Orderbook snapshot acquired for: " + market)
+        book = {
+            "bids": [],
+            "asks": []
+        }
+        buys = update['data']['bids']
+
+        for i in range(len(buys)):
+            buy = buys[i]
+            book['bids'].append((float(buy[0]), float(buy[1])))
+
+        sells = update['data']['asks']
+        for i in range(len(sells)):
+            sell = sells[i]
+            book['asks'].append((float(sell[0]), float(sell[1])))
+
+        book["bids"] = sort_list_tuples(book["bids"])
+        book["asks"] = sort_list_tuples(book["asks"])
+
+        self.__orderbooks['ftx'][update['market']] = book
+
     def coinbase_snapshot_update(self, update):
         print("Orderbook snapshot acquired for: " + update['product_id'])
         # Clear whatever book we had
@@ -262,6 +410,147 @@ def coinbase_update(self, update):
             i(self.__orderbooks['coinbase_pro'][update['product_id']],
               **self.__websockets_kwargs['coinbase_pro'][update['product_id']])
 
+    def okx_update(self, update):
+
+        symbol = update['arg']['instId']
+
+        book_buys = self.__orderbooks['okx'][symbol]['bids']  # type: list
+        book_sells = self.__orderbooks['okx'][symbol]['asks']  # type: list
+
+        # Buys are b, count from low to high with reverse (which is the ::-1 thing)
+        new_buys = update['data'][0]['bids'][::-1]  # type: list
+        for i in new_buys:
+            i[0] = float(i[0])
+            i[1] = float(i[1])
+            if i[1] == 0:
+                book_buys = remove_price(book_buys, i[0])
+            else:
+                book_buys.append((i[0], i[1]))
+
+        # Asks are sells, these are also counted from low to high
+        new_sells = update['data'][0]['asks']  # type: list
+        for i in new_sells:
+            i[0] = float(i[0])
+            i[1] = float(i[1])
+            if i[1] == 0:
+                book_sells = remove_price(book_sells, i[0])
+            else:
+                book_sells.append((i[0], i[1]))
+
+        # Now sort them
+        book_buys = sort_list_tuples(book_buys)
+        book_sells = sort_list_tuples(book_sells)
+
+        self.__orderbooks['okx'][symbol]['bids'] = book_buys
+        self.__orderbooks['okx'][symbol]['asks'] = book_sells
+
+        # Pass in this new updated orderbook
+        callbacks = self.__websockets_callbacks['okx'][symbol]
+        for i in callbacks:
+            i(self.__orderbooks['okx'][symbol],
+              **self.__websockets_kwargs['okx'][symbol])
+
+    def okx_snapshot_update(self, update):
+        print("Orderbook snapshot acquired for: " + update['arg']['instId'])
+
+        book = {
+            "bids": [],
+            "asks": []
+        }
+
+        buys = update['data'][0]['bids'] # [0][:-1]
+        buys[0] = buys[0][:-1]
+        # Convert these to float and write to our order dictionaries
+        for i in range(len(buys)):
+            buy = buys[i]
+            book['bids'].append((float(buy[0]), float(buy[1])))
+
+        sells = update['data'][0]['asks'] # [0][:-1]
+        sells[0] = sells[0][:-1]
+        for i in range(len(sells)):
+            sell = sells[i]
+            book['asks'].append((float(sell[0]), float(sell[1])))
+
+        book["bids"] = sort_list_tuples(book["bids"])
+        book["asks"] = sort_list_tuples(book["asks"])
+
+        self.__orderbooks['okx'][update['arg']['instId']] = book
+
+    def kucoin_update(self, update):
+        symbol = update['data']['symbol']
+
+        # Get symbol for orderbook
+        book_buys = self.__orderbooks['kucoin'][symbol]['bids']
+        book_sells = self.__orderbooks['kucoin'][symbol]['asks']  # type: list
+
+        new_buys = update['data']['changes']['bids'][::-1]  # type: list
+        if len(new_buys) == 0:
+            pass
+        else:
+            new_buys[0] = new_buys[0][:-1]
+
+        for i in new_buys:
+            i[0] = float(i[0])  # price
+            i[1] = float(i[1])  # size
+            if i[1] == 0:
+                book_buys = remove_price(book_buys, i[1])
+            else:
+                book_buys.append((i[0], i[1]))
+
+        # Asks are sells, these are also counted from low to high
+        new_sells = update['data']['changes']['asks']  # type: list
+        if len(new_sells) == 0:
+            pass
+        else:
+            new_sells[0] = new_sells[0][:-1]
+
+        for i in new_sells:
+            i[0] = float(i[0])
+            i[1] = float(i[1])
+            if i[1] == 0:
+                book_sells = remove_price(book_sells, i[1])
+            else:
+                book_sells.append((i[0], i[1]))
+
+        # Now sort them
+        book_buys = sort_list_tuples(book_buys)
+        book_sells = sort_list_tuples(book_sells)
+
+        self.__orderbooks['kucoin'][symbol]['bids'] = book_buys
+        self.__orderbooks['kucoin'][symbol]['asks'] = book_sells
+
+        # Pass in this new updated orderbook
+        callbacks = self.__websockets_callbacks['kucoin'][symbol]
+        for i in callbacks:
+            i(self.__orderbooks['kucoin'][symbol],
+              **self.__websockets_kwargs['kucoin'][symbol])
+
+    def kucoin_snapshot_update(self, update):
+        print("Orderbook snapshot acquired for: " + update['data']['symbol'])
+        # Clear whatever book we had
+        book = {
+            "bids": [],
+            "asks": []
+        }
+        # Get all bids
+        buys = update['data']['changes']['bids']  # [0][:-1]
+        buys[0] = buys[0][:-1]
+        # Convert these to float and write to our order dictionaries
+        for i in range(len(buys)):
+            buy = buys[i]
+            book['bids'].append((float(buy[0]), float(buy[1])))
+
+        sells = update['data']['changes']['asks']  # [0][:-1]
+        sells[0] = sells[0][:-1]
+        for i in range(len(sells)):
+            sell = sells[i]
+            book['asks'].append((float(sell[0]), float(sell[1])))
+
+        book["bids"] = sort_list_tuples(book["bids"])
+        book["asks"] = sort_list_tuples(book["asks"])
+
+        self.__orderbooks['kucoin'][update['data']['symbol']] = book
+
     def binance_update(self, update):
         try:
             # TODO this needs a snapshot to work correctly, which needs arun's rest code
diff --git a/blankly/exchanges/managers/ticker_manager.py b/blankly/exchanges/managers/ticker_manager.py
index cd3dff79..1e6532c7 100644
--- a/blankly/exchanges/managers/ticker_manager.py
+++ b/blankly/exchanges/managers/ticker_manager.py
@@ -15,11 +15,16 @@
     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 random
+import requests
+
 import blankly.utils.utils
 from blankly.exchanges.interfaces.alpaca.alpaca_websocket import Tickers as Alpaca_Ticker
 from blankly.exchanges.interfaces.binance.binance_websocket import Tickers as Binance_Ticker
 from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro_websocket import Tickers as Coinbase_Pro_Ticker
+from blankly.exchanges.interfaces.kucoin.kucoin_websocket import Tickers as Kucoin_Ticker
 from blankly.exchanges.interfaces.ftx.ftx_websocket import Tickers as FTX_Ticker
+from blankly.exchanges.interfaces.okx.okx_websocket import Tickers as Okx_Ticker
 
 from blankly.exchanges.managers.websocket_manager import WebsocketManager
 
@@ -37,8 +42,11 @@ 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 == "kucoin":
+            default_symbol = blankly.utils.to_exchange_symbol(default_symbol, "kucoin")
         elif default_exchange == "ftx":
             default_symbol = blankly.utils.to_exchange_symbol(default_symbol, "ftx")
+
         self.__default_symbol = default_symbol
 
         self.__tickers = {default_exchange: {}}
@@ -50,7 +58,8 @@ def __init__(self, default_exchange: str, default_symbol: str):
     Manager Functions 
     """
 
-    def create_ticker(self, callback, log: str = None, override_symbol: str = None, override_exchange: str = None):
+    def create_ticker(self, callback, log: str = None, override_symbol: str = None, override_exchange: str = None,
+                      **kwargs):
         """
         Create a ticker on a given exchange.
         Args:
@@ -58,6 +67,7 @@ def create_ticker(self, callback, log: str = None, override_symbol: str = None,
             log: Fill this with a path to log the price updates.
             override_symbol: The currency to create a ticker for.
             override_exchange: Override the default exchange.
+            kwargs: Any keyword arguments to be passed into the callback besides the first positional message argument
         Returns:
             Direct ticker object
         """
@@ -78,9 +88,9 @@ def create_ticker(self, callback, log: str = None, override_symbol: str = None,
 
             if sandbox_mode:
                 ticker = Coinbase_Pro_Ticker(override_symbol, "ticker", log=log,
-                                             WEBSOCKET_URL="wss://ws-feed-public.sandbox.pro.coinbase.com")
+                                             websocket_url="wss://ws-feed-public.sandbox.pro.coinbase.com", **kwargs)
             else:
-                ticker = Coinbase_Pro_Ticker(override_symbol, "ticker", log=log)
+                ticker = Coinbase_Pro_Ticker(override_symbol, "ticker", log=log, **kwargs)
 
             ticker.append_callback(callback)
             # Store this object
@@ -95,15 +105,54 @@ def create_ticker(self, callback, log: str = None, override_symbol: str = None,
                 ticker = Binance_Ticker(override_symbol,
                                         "aggTrade",
                                         log=log,
-                                        WEBSOCKET_URL="wss://testnet.binance.vision/ws")
+                                        websocket_url="wss://testnet.binance.vision/ws", **kwargs)
             else:
                 ticker = Binance_Ticker(override_symbol,
                                         "aggTrade",
-                                        log=log)
+                                        log=log, **kwargs)
             ticker.append_callback(callback)
             override_symbol = override_symbol.upper()
             self.__tickers['binance'][override_symbol] = ticker
             return ticker
+
+        elif exchange_name == "kucoin":
+            if override_symbol is None:
+                override_symbol = self.__default_symbol
+
+            request_data = (requests.post('https://api.kucoin.com/api/v1/bullet-public').json())
+
+            override_symbol = blankly.utils.to_exchange_symbol(override_symbol, "kucoin")
+            if sandbox_mode:
+                base_endpoint = request_data['data']['instanceServers'][0]['endpoint']
+                token = request_data['data']['token']
+                ticker = Kucoin_Ticker(override_symbol,
+                                       "ticker",
+                                       log=log,
+                                       websocket_url=f"{base_endpoint}/socket.io/?token={token}", **kwargs)
+            else:
+                base_endpoint = request_data['data']['instanceServers'][0]['endpoint']
+                token = request_data['data']['token']
+                ticker = Kucoin_Ticker(override_symbol, "ticker",
+                                       log=log,
+                                       websocket_url=f"{base_endpoint}?token={token}&[connectId="
+                                                     f"{random.randint(1, 100000000) * 100000000}]", **kwargs)
+            ticker.append_callback(callback)
+            self.__tickers['kucoin'][override_symbol] = ticker
+        elif exchange_name == "okx":
+            if override_symbol is None:
+                override_symbol = self.__default_symbol
+
+            if sandbox_mode:
+                ticker = Okx_Ticker(override_symbol, "tickers", log=log,
+                                    WEBSOCKET_URL="wss://wspap.okx.com:8443/ws/v5/public?brokerId=9999", **kwargs)
+            else:
+                ticker = Okx_Ticker(override_symbol, "tickers", log=log, **kwargs)
+
+            ticker.append_callback(callback)
+            # Store this object
+            self.__tickers['okx'][override_symbol] = ticker
+            return ticker
+
         elif exchange_name == "alpaca":
             stream = self.preferences['settings']['alpaca']['websocket_stream']
             if override_symbol is None:
@@ -114,12 +163,14 @@ def create_ticker(self, callback, log: str = None, override_symbol: str = None,
                 ticker = Alpaca_Ticker(override_symbol,
                                        "trades",
                                        log=log,
-                                       WEBSOCKET_URL="wss://paper-api.alpaca.markets/stream/v2/{}/".format(stream))
+                                       websocket_url="wss://paper-api.alpaca.markets/stream/v2/{}/".format(stream),
+                                       **kwargs)
             else:
                 ticker = Alpaca_Ticker(override_symbol,
                                        "trades",
                                        log=log,
-                                       WEBSOCKET_URL="wss://stream.data.alpaca.markets/v2/{}/".format(stream))
+                                       websocket_url="wss://stream.data.alpaca.markets/v2/{}/".format(stream),
+                                       **kwargs)
             ticker.append_callback(callback)
             self.__tickers['alpaca'][override_symbol] = ticker
             return ticker
@@ -133,7 +184,7 @@ def create_ticker(self, callback, log: str = None, override_symbol: str = None,
             if sandbox_mode:
                 raise ValueError("Error: FTX does not have a sandbox mode")
             else:
-                ticker = FTX_Ticker(override_symbol, "trades", log=log)
+                ticker = FTX_Ticker(override_symbol, "trades", log=log, **kwargs)
 
             ticker.append_callback(callback)
             # Store this object
diff --git a/blankly/exchanges/orders/__init__.py b/blankly/exchanges/orders/__init__.py
index e69de29b..3f94f56c 100644
--- a/blankly/exchanges/orders/__init__.py
+++ b/blankly/exchanges/orders/__init__.py
@@ -0,0 +1,1 @@
+from .futures import *
diff --git a/blankly/exchanges/orders/futures/__init__.py b/blankly/exchanges/orders/futures/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/blankly/exchanges/orders/futures/futures_order.py b/blankly/exchanges/orders/futures/futures_order.py
new file mode 100644
index 00000000..f611ec0a
--- /dev/null
+++ b/blankly/exchanges/orders/futures/futures_order.py
@@ -0,0 +1,51 @@
+"""
+    Base Order class
+    Copyright (C) 2022 Matias Kotlik
+
+    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 dataclasses import dataclass, field
+
+from blankly.enums import OrderStatus, OrderType, Side, PositionMode, ContractType, TimeInForce
+from blankly.exchanges.interfaces.abc_base_exchange_interface import ABCBaseExchangeInterface
+
+
+@dataclass
+class FuturesOrder:
+    symbol: str
+    id: int
+    size: float
+    status: OrderStatus
+    type: OrderType
+    contract_type: ContractType
+    side: Side
+    position: PositionMode
+    price: float
+    limit_price: float
+    time_in_force: TimeInForce
+    response: dict = field(compare=False)
+    interface: ABCBaseExchangeInterface = field(compare=False)
+
+    def __str__(self):
+        return f"""Order Parameters:
+Status: {self.status}
+ID: {self.id}
+Symbol: {self.symbol}
+Type: {self.type}
+Contract Type: {self.contract_type}
+Side: {self.side}
+Size: {self.size}
+Position: {self.position}
+Price: {self.price}
+Time In Force: {self.time_in_force}"""
diff --git a/blankly/exchanges/orders/market_order.py b/blankly/exchanges/orders/market_order.py
index b26cde36..842ad6b2 100644
--- a/blankly/exchanges/orders/market_order.py
+++ b/blankly/exchanges/orders/market_order.py
@@ -59,7 +59,6 @@ def __init__(self, order, response, interface):
         ]
         """
         self.Interface = interface
-        self.__exchange = self.Interface.get_exchange_type()
         self.__order = order
         self.__response = response
         self.__homogenized_result = None
diff --git a/blankly/exchanges/orders/order.py b/blankly/exchanges/orders/order.py
index ebc83fa7..2c62b6a4 100644
--- a/blankly/exchanges/orders/order.py
+++ b/blankly/exchanges/orders/order.py
@@ -48,7 +48,8 @@ def __init__(self, response, order, interface):
         """
         self.__response = response
         self.__order = order
-        self.Interface = interface
+        self.interface = interface
+        self.exchange = interface.get_exchange_type()
 
     @staticmethod
     def add_new_line(input_string, *components, newline=True):
@@ -71,7 +72,7 @@ def __str__(self):
 
         return_string = self.add_new_line(return_string, "ID: ", self.get_id())
 
-        return_string = self.add_new_line(return_string, "Symbol: ", self.get_asset_id())
+        return_string = self.add_new_line(return_string, "Symbol: ", self.get_symbol())
 
         return_string = self.add_new_line(return_string, "Purchase Time: ", self.get_purchase_time())
 
@@ -96,7 +97,7 @@ def get_id(self) -> str:
         """
         return self.__response["id"]
 
-    def get_asset_id(self) -> str:
+    def get_symbol(self) -> str:
         """
         Get the asset id of the exchange
         """
@@ -117,9 +118,9 @@ def get_status(self, full=True) -> dict:
             field as (example) {"status:" "NEW"}
         """
         if full:
-            return self.Interface.get_order(self.__order["symbol"], self.get_id())
+            return self.interface.get_order(self.__order["symbol"], self.get_id())
         else:
-            return {"status": self.Interface.get_order(self.__order["symbol"], self.get_id())["status"]}
+            return {"status": self.interface.get_order(self.__order["symbol"], self.get_id())["status"]}
 
     def get_type(self) -> str:
         """
diff --git a/blankly/exchanges/strategy_logger.py b/blankly/exchanges/strategy_logger.py
index c0ba8f7a..97c985bf 100644
--- a/blankly/exchanges/strategy_logger.py
+++ b/blankly/exchanges/strategy_logger.py
@@ -85,14 +85,33 @@ 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_market_order(out, self.__type)
+        try:
+            blankly.reporter.log_market_order({
+                'symbol': symbol,
+                'exchange': self.__type,
+                'size': size,
+                'id': out.get_id(),
+                'side': side
+            }, self.__type)
+        except Exception:
+            pass
         return out
     
     def limit_order(self, symbol: str, side: str, price: float, size: float) -> LimitOrder:
         out = self.interface.limit_order(symbol, side, price, size)
 
         # Record limit order along with the arguments
-        blankly.reporter.log_limit_order(out, self.__type)
+        try:
+            blankly.reporter.log_limit_order({
+                'symbol': symbol,
+                'exchange': self.__type,
+                'size': size,
+                'id': out.get_id(),
+                'side': side,
+                'price': price
+            }, self.__type)
+        except Exception:
+            pass
         return out
     
     def cancel_order(self, symbol: str, order_id: str) -> dict:
@@ -114,14 +133,20 @@ def get_order(self, symbol: str, order_id: str) -> dict:
         out = self.interface.get_order(symbol, order_id)
 
         # Record the arguments
-        blankly.reporter.update_order(out, self.__type)
+        try:
+            blankly.reporter.update_order({
+                'id': order_id,
+                'status': out['status']
+            }, self.__type)
+        except Exception:
+            pass
         return out
 
-    def get_fees(self) -> dict:
+    def get_fees(self, symbol) -> dict:
         """
         No logging implemented
         """
-        return self.interface.get_fees()
+        return self.interface.get_fees(symbol)
 
     def get_order_filter(self, symbol: str):
         """
diff --git a/blankly/frameworks/model/__init__.py b/blankly/frameworks/model/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/blankly/frameworks/model/model.py b/blankly/frameworks/model/model.py
new file mode 100644
index 00000000..f796cb5e
--- /dev/null
+++ b/blankly/frameworks/model/model.py
@@ -0,0 +1,96 @@
+"""
+    An improved strategy simulation layer built for spot and futures trading
+    Copyright (C) 2021  Emerson Dove
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+import threading
+import typing
+
+from blankly.exchanges.interfaces.paper_trade.backtest_controller import BackTestController, BacktestResult
+from blankly.exchanges.interfaces.paper_trade.abc_backtest_controller import ABCBacktestController
+from blankly.exchanges.interfaces.paper_trade.paper_trade import PaperTrade
+from blankly.exchanges.exchange import Exchange
+from blankly.utils.time_builder import time_interval_to_seconds
+import time
+
+
+class Model:
+    def __init__(self, exchange: Exchange):
+        self.__exchange = exchange
+        self.__exchange_cache = self.__exchange
+        self.is_backtesting = False
+
+        self.interface = exchange.interface
+
+        self.has_data = True
+
+        self.backtester: ABCBacktestController = BackTestController(self)
+        # Type these internal calls to the specific backtester
+        self.__backtester: BackTestController = self.backtester
+
+    def backtest(self, args, initial_values: dict = None, settings_path: str = None, kwargs=None) -> BacktestResult:
+        # Construct the backtest controller
+        if not isinstance(self.__exchange, Exchange):
+            raise NotImplementedError
+
+        if kwargs is None:
+            kwargs = {}
+
+        # Toggle backtesting
+        self.is_backtesting = True
+        self.__exchange = PaperTrade(self.__exchange)
+        self.interface = self.__exchange.interface
+        backtest = self.__backtester.run(args,
+                                         initial_account_values=initial_values,
+                                         exchange=self.__exchange,
+                                         backtest_settings_path=settings_path,
+                                         **kwargs
+                                         )
+        self.is_backtesting = False
+        self.__exchange = self.__exchange_cache
+        self.has_data = True
+
+        return backtest
+
+    def run(self, args: typing.Any = None) -> threading.Thread:
+        thread = threading.Thread(target=self.main, args=(args,))
+        thread.start()
+        return thread
+
+    def main(self, args):
+        raise NotImplementedError("Add a main function to your strategy to run the model.")
+
+    def event(self, type_: str, data: any):
+        """
+        Override this to gain access to any custom events passed in
+        """
+        pass
+
+    def websocket_update(self, data):
+        pass
+
+    @property
+    def time(self):
+        if not self.is_backtesting:
+            return time.time()
+        else:
+            return self.__backtester.time
+
+    def sleep(self, seconds: [int, float, str]):
+        seconds = time_interval_to_seconds(seconds)
+        if not self.is_backtesting:
+            time.sleep(seconds)
+        else:
+            self.__backtester.sleep(seconds)
diff --git a/blankly/frameworks/strategy/__init__.py b/blankly/frameworks/strategy/__init__.py
index b5d9bb03..bd291774 100644
--- a/blankly/frameworks/strategy/__init__.py
+++ b/blankly/frameworks/strategy/__init__.py
@@ -1,2 +1,5 @@
-from blankly.frameworks.strategy.strategy_base import *
-from blankly.frameworks.strategy.strategy_state import *
+from blankly.frameworks.strategy.strategy_base import StrategyBase
+from blankly.frameworks.strategy.strategy_state import StrategyState
+from blankly.frameworks.strategy.strategy import Strategy
+from blankly.frameworks.strategy.futures_strategy import FuturesStrategy
+from blankly.frameworks.strategy.futures_strategy_state import FuturesStrategyState
diff --git a/blankly/frameworks/strategy/futures_strategy.py b/blankly/frameworks/strategy/futures_strategy.py
new file mode 100644
index 00000000..572b7842
--- /dev/null
+++ b/blankly/frameworks/strategy/futures_strategy.py
@@ -0,0 +1,51 @@
+"""
+    Abstraction for creating interval driven user strategies for trading futures.
+    Copyright (C) 2022 Matias Kotlik
+
+    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 typing
+
+from blankly.exchanges.futures.futures_exchange import FuturesExchange
+from blankly.exchanges.futures.futures_strategy_logger import FuturesStrategyLogger
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+from blankly.exchanges.interfaces.paper_trade.backtest_result import BacktestResult
+from blankly.frameworks.strategy.strategy_base import StrategyBase
+
+
+class FuturesStrategy(StrategyBase):
+    exchange: FuturesExchange
+    interface: FuturesExchangeInterface
+
+    def __init__(self, exchange: FuturesExchange):
+        super().__init__(
+            exchange, FuturesStrategyLogger(exchange.interface, strategy=self))
+
+    def backtest(self,
+                 to: str = None,
+                 initial_values: dict = 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,
+                 **kwargs) -> BacktestResult:
+        raise NotImplementedError
+
+    def time(self) -> float:
+        """
+        Return the current time, or backtesting time if we are backtesting.
+        """
+        raise NotImplementedError
diff --git a/blankly/frameworks/strategy/futures_strategy_state.py b/blankly/frameworks/strategy/futures_strategy_state.py
new file mode 100644
index 00000000..b3454b9c
--- /dev/null
+++ b/blankly/frameworks/strategy/futures_strategy_state.py
@@ -0,0 +1,58 @@
+"""
+    Strategy State for FuturesStrategy objects.
+    Copyright (C) 2022 Matias Kotlik
+
+    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.futures_exchange_interface import FuturesExchangeInterface
+from blankly.utils.utils import AttributeDict, get_base_asset, get_quote_asset, pretty_print_json
+from blankly.frameworks.strategy.futures_strategy import FuturesStrategy
+
+
+class FuturesStrategyState:
+    strategy: FuturesStrategy
+    interface: FuturesExchangeInterface
+    variables: AttributeDict
+    resolution: int
+    symbol: str
+    base_asset: str
+    quote_asset: str
+
+    def __init__(self,
+                 strategy,
+                 variables: AttributeDict,
+                 symbol,
+                 resolution: [int, float] = None):
+        self.strategy = strategy
+        self.variables = variables
+        self.resolution = resolution
+        self.symbol = symbol
+        self.base_asset = get_base_asset(symbol)
+        self.quote_asset = get_quote_asset(symbol)
+
+    @property
+    def interface(self) -> FuturesExchangeInterface:
+        return self.strategy.interface
+
+    @property
+    def time(self) -> float:
+        return self.strategy.time()
+
+    def __str__(self):
+        return f"""Symbol: {self.symbol}
+Time: {self.time}
+Resolution: {self.resolution}
+Variables:
+{pretty_print_json(self.variables, actually_print=False)}
+"""
diff --git a/blankly/frameworks/strategy/recipes/recipes.py b/blankly/frameworks/strategy/recipes/recipes.py
deleted file mode 100644
index d60e174a..00000000
--- a/blankly/frameworks/strategy/recipes/recipes.py
+++ /dev/null
@@ -1,38 +0,0 @@
-import blankly
-
-
-def golden_cross(ticker, interface: blankly.Interface, state: StrategyState):
-    resolution: str = state.resolution  # get the resolution that this price event is stored at
-    variables = state.__variables  # each price event has it's own local variable state
-
-    account = interface.account  # get account holdings
-
-    historical_prices = blankly.historical(ticker, 50, resolution=resolution)
-    sma50 = blankly.analysis.calculate_sma(historical_prices, window=50)[
-        -1]  # last 50 day sma value is the value we want
-
-    if price > sma50 and variables['order_submitted']:
-        variables['order_submitted'] = True
-        interface.market_order(ticker, 'buy', account.cash * 0.25)
-    elif price < sma50:  # only sell if there's an open position
-        variables['order_submitted'] = False
-        stocks_available = account.holdings[ticker].available * 0.5  # sell 50% of available sures
-        interface.market_order(ticker, 'sell', stocks_available)
-
-
-def rsi(price, ticker, interface: blankly.Interface, state: StrategyState):
-    resolution: str = state.resolution  # get the resolution that this price event is stored at
-    variables = state.__variables  # each price event has it's own local variable state
-
-    account = interface.account  # get account holdings
-
-    historical_prices = blankly.historical(ticker, 50, resolution=resolution)
-    rsi = blankly.analysis.calculate_rsi(historical_prices)[-1]  # last 50 day sma value is the value we want
-
-    if rsi > 70 and variables['order_submitted']:
-        variables['order_submitted'] = True
-        interface.market_order(ticker, 'buy', account.cash * 0.25)
-    elif rsi < 30:  # only sell if there's an open position
-        variables['order_submitted'] = False
-        stocks_available = account.holdings[ticker].available * 0.5  # sell 50% of available sures
-        interface.market_order(ticker, 'sell', stocks_available)
diff --git a/blankly/frameworks/strategy/strategy.py b/blankly/frameworks/strategy/strategy.py
index b4385709..288f3226 100644
--- a/blankly/frameworks/strategy/strategy.py
+++ b/blankly/frameworks/strategy/strategy.py
@@ -1,246 +1,356 @@
 """
-This is a commentated but cleaner implementation of strategy_base.py
+    Spot specific class for strategy
+    Copyright (C) 2021  Emerson Dove
 
-Largely this project is going on the backburner
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
+import threading
+import time
+import traceback
+import typing
+import warnings
+
+import blankly
+from blankly.exchanges.exchange import Exchange
+from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface
+from blankly.exchanges.interfaces.paper_trade.backtest_result import BacktestResult
+from blankly.exchanges.strategy_logger import StrategyLogger
+from blankly.frameworks.model.model import Model
+from blankly.frameworks.strategy.strategy_base import StrategyBase, EventType
+from blankly.frameworks.strategy import StrategyState
+from blankly.utils.utils import info_print
+
+
+class StrategyStructure(Model):
+    def __init__(self, exchange: Exchange):
+        self.lock = threading.Lock()
+        super().__init__(exchange)
+
+        self.orderbook_websockets = None
+        self.ticker_websockets = None
+        self.ticker_manager = None
+        self.orderbook_manager = None
+        self.schedulers = None
+        self.remote_backtesting = None
+
+    def construct_strategy(self, schedulers, orderbook_websockets,
+                           ticker_websockets, orderbook_manager, ticker_manager):
+        self.schedulers = schedulers
+        self.orderbook_websockets = orderbook_websockets
+        self.ticker_websockets = ticker_websockets
+        self.orderbook_manager = orderbook_manager
+        self.ticker_manager = ticker_manager
+
+    def rest_event(self, **kwargs):
+        callback = kwargs['callback']  # type: callable
+        symbol = kwargs['symbol']  # type: str
+        resolution = kwargs['resolution']  # type: int
+        variables = kwargs['variables']  # type: dict
+        type_ = kwargs['type']  # type: EventType
+        state = kwargs['state']  # type: StrategyState
+
+        state.variables = variables
+        state.resolution = resolution
+
+        if type_ == EventType.bar_event:
+            if not self.is_backtesting:
+                bar_time = kwargs['bar_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 self.interface.get_exchange_type() == "alpaca":
+                            break
+                        else:
+                            if data['time'] + resolution == bar_time:
+                                break
+                    except IndexError:
+                        pass
+                    time.sleep(.5)
+            else:
+                # If we are backtesting always just grab the last point and hope for the best of course
+                try:
+                    data = self.interface.history(symbol=symbol, to=1, resolution=resolution).iloc[-1].to_dict()
+                except IndexError:
+                    warnings.warn("No bar found for this time range")
+                    return
+            args = [data, symbol, state]
+        elif type_ == EventType.price_event:
+            data = self.interface.get_price(symbol)
+            args = [data, symbol, state]
+        elif type_ == EventType.scheduled_event:
+            args = [state]
+        elif type_ == EventType.arbitrage_event:
+            prices = {}
+            # If we're backtesting loop through the symbol and just grab the price
+            if self.is_backtesting:
+                for sym in symbol:
+                    prices[sym] = self.interface.get_price(sym)
+
+            # We have to be a bit more strategy if we're live
+            else:
+                def grab_price(threaded_symbol):
+                    prices[threaded_symbol] = self.interface.get_price(threaded_symbol)
+
+                threadpool = []
+                for sym in symbol:
+                    threadpool.append(threading.Thread(target=grab_price, args=(sym,)))
+
+                # Start the threads
+                for thread in threadpool:
+                    thread.start()
+
+                # Join each of the threads because they've writen to the symbol
+                for thread in threadpool:
+                    thread.join()
+            args = [prices, symbol, state]
+        else:
+            return
+
+        try:
+            callback(*args)
+        except Exception:
+            traceback.print_exc()
+
+    def run_price_events(self, kwargs_list: list):
+        for events_definition in kwargs_list:
+            events_definition['next_run'] = self.backtester.initial_time
+        while self.has_data:
+            kwargs_list = sorted(kwargs_list, key=lambda d: d['next_run'])
+            next_event = kwargs_list[0]
+
+            # Sleep the difference
+            self.sleep(next_event['next_run'] - self.time)
+
+            # Run the event
+            self.rest_event(**next_event)
+
+            # Value the account after each run
+            self.backtester.value_account()
+            kwargs_list[0]['next_run'] += kwargs_list[0]['resolution']
+
+    def main(self, args):
+        if self.is_backtesting:
+            self.run_backtest()
+        else:
+            self.run_live()
+
+    def run_backtest(self):
+        # Write in the new interface, no matter which type it is
+        for scheduler in self.schedulers:
+            kwargs = scheduler.get_kwargs()
+            # Overwrite the internal interface in the created strategy
+            kwargs['state'].strategy.interface = self.interface
+        self.__run_init()
+
+        kwargs_list = []
+        for scheduler in self.schedulers:
+            kwargs_list.append(scheduler.get_kwargs())
+
+        self.run_price_events(kwargs_list)
+
+    def __run_init(self):
+        for i in self.schedulers:
+            kwargs = i.get_kwargs()
+            if kwargs['init'] is not None:
+                if kwargs['type'] != EventType.scheduled_event:
+                    kwargs['init'](kwargs['symbol'], kwargs['state'])
+                else:
+                    kwargs['init'](kwargs['state'])
+
+    def run_live(self):
+        self.__run_init()
+
+        for scheduler in self.schedulers:
+            scheduler.start()
+
+        for i in self.orderbook_websockets:
+            # Index 2 contains the initialization function for the assigned websockets array
+            if i[2] is not None:
+                i[2](i[0], i[3])
+            self.orderbook_manager.restart_ticker(i[0], i[1])
+
+        for i in self.ticker_websockets:
+            # The initialization function should have already been called for ticker websockets
+            # Notice this is different from orderbook websockets because these are put into the scheduler
+            self.ticker_manager.restart_ticker(i[0], i[1])
+
+    def teardown(self):
+        self.lock.acquire()
+        for i in self.schedulers:
+            i.stop_scheduler()
+            kwargs = i.get_kwargs()
+            teardown = kwargs['teardown']
+            state_object = kwargs['state']
+            if callable(teardown):
+                teardown(state_object)
+
+        for i in self.orderbook_websockets:
+            self.orderbook_manager.close_websocket(override_symbol=i[0], override_exchange=i[1])
+            # Call the stored teardown
+            teardown_func = i[4]
+            if callable(teardown_func):
+                teardown_func(i[3])
+
+        for i in self.ticker_websockets:
+            self.ticker_manager.close_websocket(override_symbol=i[0], override_exchange=i[1])
+        self.lock.release()
+
+
+class Strategy(StrategyBase):
+    __exchange: Exchange
+    interface: ABCExchangeInterface
+
+    def __init__(self, exchange: Exchange):
+        self.model = StrategyStructure(exchange)
+        super().__init__(exchange, StrategyLogger(exchange.get_interface(), strategy=self), model=self.model)
+        self._paper_trade_exchange = blankly.PaperTrade(exchange)
+        self.__prices_added = False
+
+    def backtest(self,
+                 to: str = None,
+                 initial_values: dict = None,
+                 start_date: typing.Union[str, float, int] = None,
+                 end_date: typing.Union[str, float, int] = None,
+                 settings_path: str = None,
+                 **kwargs
+                 ) -> BacktestResult:
+        """
+        Turn this strategy into a backtest.
+
+        Args:
+            ** We expect either an initial_value (in USD) or a dictionary of initial values, we also expect
+            either `to` to be set or `start_date` and `end_date` **
+
+            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". 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
+            settings_path (str): Path to the backtest.json file.
+
+            Keyword Arguments:
+                **Use these to override parameters in the backtest.json file**
+
+                use_price: str = 'close',
+                    Set which price column to use.
+
+                smooth_prices: bool = False,
+                    Create linear connections between downloaded prices
+
+                GUI_output: bool = True,
+                    Enable/disable GUI webpage display after backtest
+
+                show_tickers_with_zero_delta: bool = False,
+                    Exclude tickers that have no change to account value in the GUI
+
+                save_initial_account_value: bool = True,
+                    Put an extra frame which contains the initial account values before any trade
+
+                show_progress_during_backtest: bool = True,
+                    Show a progress bar as the backtest runs
+
+                cache_location: str = './price_caches'
+                    Set a location for the price cache csv's to be written to
+
+                continuous_caching: bool
+                    Utilize the advanced price caching system built into the backtest. Automatically aggregate and prune
+                    downloaded data.
+
+                resample_account_value_for_metrics: str or bool = '1d' or False
+                    Because backtest data can be input at a variety of resolutions, account value often needs to be
+                        recalculated at consistent intervals for use in metrics & indicators.
+                        This setting allows the specification of that consistent interval.
+                        The value can be set to `False` to skip any recalculation.
+
+                quote_account_value_in: str = 'USD'
+                    Manually set what valuation should be used when calculating account value.
+                        Multiple types of quote currency (ex: USD and EUR) are not supported because
+                        there is no datasource for quoting pairs such as EUR-USD until forex integration.
+
+                ignore_user_exceptions: bool = True
+                    Set this to True to handle user exceptions identically to how they're handled by strategy calls.
+                        False means that the backtest will immediately stop & attempt to generate a report if something
+                        in the user calls goes wrong. True will replicate strategy errors.
+
+                risk_free_return_rate: float = 0.0
+                    Set this to be the theoretical rate of return with no risk
+        """
+        self.setup_model()
+        if len(self.orderbook_websockets) != 0 or len(self.ticker_websockets) != 0:
+            info_print("Found websocket events added to this strategy. These are not yet backtestable without "
+                       "event based data")
+
+        self.__add_prices(to, start_date, end_date)
+        return self.model.backtest(args={}, initial_values=initial_values, settings_path=settings_path, kwargs=kwargs)
+
+    def __add_prices(self, to, start_date, end_date):
+        for scheduler in self.schedulers:
+            event_element = scheduler.get_kwargs()
+
+            # Skip any scheduled events
+            if event_element['symbol'] is None:
+                continue
+
+            # Loop through the symbols if it is a list
+            if isinstance(event_element['symbol'], list):
+                for symbol in event_element['symbol']:
+                    self.model.backtester.add_prices(to=to,
+                                                     start_date=start_date,
+                                                     stop_date=end_date,
+                                                     symbol=symbol,
+                                                     resolution=event_element['resolution'])
+            else:
+                self.model.backtester.add_prices(to=to,
+                                                 start_date=start_date,
+                                                 stop_date=end_date,
+                                                 symbol=event_element['symbol'],
+                                                 resolution=event_element['resolution'])
+
+            self.__prices_added = True
+
+        if not self.__prices_added:
+            raise LookupError("No prices added. If using scheduled events, create an empty price event or add prices"
+                              " manually using strategy.add_prices()")
+
+    def add_prices(self, symbol: str, resolution: [str, int, float], to: str = None,
+                   start_date: typing.Union[str, float, int] = None,
+                   stop_date: typing.Union[str, float, int] = None):
+        """
+        Directly add prices to the strategy
+        """
+        self.model.backtester.add_prices(symbol, resolution, to, start_date, stop_date)
+        self.__prices_added = True
+
+    def setup_model(self):
+        self.model.construct_strategy(self.schedulers, self.orderbook_websockets,
+                                      self.ticker_websockets, self.orderbook_manager,
+                                      self.ticker_manager)
+
+    def start(self):
+        """
+        Run your model live!
+
+        Simply call this function to take your strategy configuration live on your exchange
+        """
+        self.setup_model()
+        if self.remote_backtesting:
+            warnings.warn("Aborted attempt to start a live strategy a backtest configuration")
+            return
+        self.model.run()
 
-# """
-#     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)
+    def time(self) -> float:
+        return self.model.time
diff --git a/blankly/frameworks/strategy/strategy_base.py b/blankly/frameworks/strategy/strategy_base.py
index 211c3e45..7761a9e1 100644
--- a/blankly/frameworks/strategy/strategy_base.py
+++ b/blankly/frameworks/strategy/strategy_base.py
@@ -16,27 +16,33 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-import datetime
 import threading
-import time
 import typing
-import warnings
+import enum
 
-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.abc_base_exchange import ABCBaseExchange
+from blankly.exchanges.interfaces.abc_base_exchange_interface import ABCBaseExchangeInterface
 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
+from blankly.utils.utils import AttributeDict
 
 
-class Strategy:
-    def __init__(self, exchange: Exchange):
+class EventType(enum.Enum):
+    price_event = 'price_event'
+    bar_event = 'bar_event'
+    scheduled_event = 'scheduled_event'
+    arbitrage_event = 'arbitrage_event'
+    orderbook_event = "orderbook_event"
+
+
+class StrategyBase:
+    __exchange: ABCBaseExchange
+    interface: ABCBaseExchangeInterface
+
+    def __init__(self, exchange: ABCBaseExchange, interface: ABCBaseExchangeInterface, model):
         """
         Create a new strategy object. A strategy can be used to run your code live while be backtestable and modular
          across exchanges.
@@ -50,56 +56,29 @@ def __init__(self, exchange: Exchange):
         bar_event(bar: dict, symbol: str, state: blankly.StrategyState)
         teardown(blankly.StrategyState)
         """
-        self.__remote_backtesting = blankly._backtesting
+        self.remote_backtesting = blankly._backtesting
         self.__exchange = exchange
+        self.interface = interface
 
         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
-        self.__interface_cache = self.interface
-        self.__paper_trade_exchange = blankly.PaperTrade(self.__exchange)
-        self.__schedulers = []
-        self.__variables = {}
-        self.__hashes = []
-        self.__orderbook_websockets = []
-        self.__ticker_websockets = []
-
-        # Initialize backtesting attributes. This only used for sending times to the Strategy/StrategyState
-        # This is done because we switch the interface to a paper trade interface
-        self.backtesting_controller = None
-
-        # This will throw a warning if they're trying to use an orderbook in the backtest
-        self.__using_orderbook = False
-
         # Attempt to report the strategy
         blankly.reporter.export_strategy(self)
 
-        # Create a lock for the teardown so nothing happens while its going on
+        # Create a lock for the teardown so nothing happens while it's going on
         self.lock = threading.Lock()
 
         # This will be updated when the teardown() function completes
         self.torndown = False
 
-    @property
-    def variables(self):
-        return self.__variables
+        self.schedulers = []
 
-    def modify_variable(self, callable_: typing.Callable, key, value):
-        hashed = hash(callable_)
-        self.__variables[hashed][key] = value
+        self.interface = interface
 
-    # # 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.
-    #     """
+        self.orderbook_websockets = []
+        self.ticker_websockets = []
+        self.model = model
 
     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,
@@ -118,7 +97,45 @@ def add_price_event(self, callback: typing.Callable, symbol: str, resolution: ty
             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')
+                                  teardown=teardown, variables=variables, type_=EventType.price_event)
+
+    def add_scheduled_event(self, callback: typing.Callable, resolution: typing.Union[str, float],
+                            init: typing.Callable = None, teardown: typing.Callable = None,
+                            synced: bool = False, variables: dict = None):
+        """
+        Add a scheduled event. This will call the callback at the rate defined in the resolution
+
+        Args:
+            callback: The price event callback that will be added to the current ticker and run at the proper resolution
+            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=None, resolution=resolution, init=init, synced=synced,
+                                  teardown=teardown, variables=variables, type_=EventType.scheduled_event)
+
+    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 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
+            symbols: A list of symbols to get data 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=symbols, resolution=resolution, init=init, synced=synced,
+                                  teardown=teardown, variables=variables, type_=EventType.arbitrage_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):
@@ -134,17 +151,16 @@ 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(type_='bar', synced=True, bar=True, callback=callback, symbol=symbol,
+        self.__custom_price_event(type_=EventType.bar_event, synced=True, callback=callback, symbol=symbol,
                                   resolution=resolution, init=init, teardown=teardown, variables=variables)
 
     def __custom_price_event(self,
-                             type_: str,
+                             type_: EventType,
                              callback: typing.Callable = None,
-                             symbol: str = None,
+                             symbol: [str, list] = 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
@@ -158,7 +174,6 @@ def __custom_price_event(self,
             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
@@ -166,140 +181,64 @@ def __custom_price_event(self,
             variables = {}
         resolution = time_interval_to_seconds(resolution)
 
-        self.__scheduling_pair.append([symbol, resolution, type_])
-
         variables_ = AttributeDict(variables)
         state = StrategyState(self, variables_, symbol, resolution=resolution)
 
-        if resolution < 60:
-            # since it's less than 10 sec, we will just use the websocket feed - exchanges don't like fast calls
-            self.ticker_manager.create_ticker(self.__idle_event, override_symbol=symbol)
-            self.__schedulers.append(
-                blankly.Scheduler(self.__price_event_websocket, resolution,
-                                  initially_stopped=True,
-                                  callback=callback,
-                                  resolution=resolution,
-                                  variables=variables_,
-                                  state_object=state,
-                                  synced=synced,
-                                  init=init,
-                                  teardown=teardown,
-                                  ohlc=bar,
-                                  symbol=symbol)
-            )
-            exchange_type = self.__exchange.get_type()
-            self.__ticker_websockets.append([symbol, exchange_type, init, state, teardown])
-        else:
-            # Use the API
-            self.__schedulers.append(
-                blankly.Scheduler(self.__price_event_rest, resolution,
-                                  initially_stopped=True,
-                                  callback=callback,
-                                  resolution=resolution,
-                                  variables=variables_,
-                                  state_object=state,
-                                  synced=synced,
-                                  ohlc=bar,
-                                  init=init,
-                                  teardown=teardown,
-                                  symbol=symbol)
-            )
+        # Use the API
+        self.schedulers.append(
+            blankly.Scheduler(self.model.rest_event, resolution,
+                              initially_stopped=True,
+                              synced=synced,
+                              callback=callback,
+                              resolution=resolution,
+                              variables=variables_,
+                              state=state,
+                              type=type_,
+                              init=init,
+                              teardown=teardown,
+                              symbol=symbol)
+        )
 
         # Export a new symbol to the backend
         blankly.reporter.export_used_symbol(symbol)
 
-    def __idle_event(self, *args, **kwargs):
+    @staticmethod
+    def __websocket_callback(tick, symbol, user_callback, state_object):
+        user_callback(tick, symbol, state_object)
+
+    def add_tick_event(self, callback: callable, symbol: str, init: callable = None, teardown: callable = None,
+                       variables: dict = None):
         """
-        Function to skip & ignore callbacks
+        Add a tick event - This will call the callback everytime the exchange provides a change in the price due to a
+         trade occurring
+        Args:
+            callback: The price event callback that will be added to the current ticker and run at the proper resolution
+            symbol: Currency pair to create the orderbook for
+            init: Callback function to allow a setup for the strategy variable. This
+                can be used for accumulating price data
+            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
         """
-        pass
-
-    def __price_event_rest(self, **kwargs):
-        callback = kwargs['callback']
-        symbol = kwargs['symbol']
-        resolution = kwargs['resolution']
-        variables = kwargs['variables']
-        ohlc = kwargs['ohlc']
-        state = kwargs['state_object']  # type: StrategyState
-
-        state.variables = variables
-        state.resolution = resolution
-
-        if ohlc:
-            ohlcv_time = kwargs['ohlcv_time']
-            while True:
-                # Sometimes coinbase doesn't download recent data correctly
-                try:
-                    data = self.interface.history(symbol=symbol, to=1, resolution=resolution).iloc[-1].to_dict()
-                    if self.interface.get_exchange_type() == "alpaca":
-                        break
-                    else:
-                        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)
-
-        callback(data, symbol, state)
-
-    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)
+        if variables is None:
+            variables = {}
 
-    @staticmethod
-    def __orderbook_event(tick, symbol, user_callback, state_object):
-        user_callback(tick, symbol, state_object)
+        state = StrategyState(self, AttributeDict(variables), symbol=symbol)
+
+        self.ticker_manager.create_ticker(self.__websocket_callback, initially_stopped=True,
+                                          override_symbol=symbol,
+                                          symbol=symbol,
+                                          user_callback=callback,
+                                          variables=variables,
+                                          state=state)
+
+        self.ticker_websockets.append([symbol, self.__exchange.get_type(), init, state, teardown])
 
-    def add_orderbook_event(self, callback: typing.Callable, symbol: str, init: typing.Callable = None,
+    def add_orderbook_event(self, callback: callable, symbol: str, init: typing.Callable = None,
                             teardown: typing.Callable = None, variables: dict = None):
         """
-        Add Orderbook Event
+        Add Orderbook Event - This will call the given callback everytime the exchange provides a change in the
+         orderbook
         Args:
             callback: The price event callback that will be added to the current ticker and run at the proper resolution
             symbol: Currency pair to create the orderbook for
@@ -312,31 +251,18 @@ def add_orderbook_event(self, callback: typing.Callable, symbol: str, init: typi
         # Make sure variables is always an empty dictionary if None
         if variables is None:
             variables = {}
-        self.__scheduling_pair.append([symbol, None])
-        callback_hash = hash((callback, symbol))
-        if callback_hash in self.__hashes:
-            raise ValueError("A callback of the same type and resolution has already been made for the ticker: "
-                             "{}".format(symbol))
-        else:
-            self.__hashes.append(callback_hash)
-        self.__variables[callback_hash] = AttributeDict(variables)
-        state = StrategyState(self, self.__variables[callback_hash], symbol=symbol)
-
-        variables = self.__variables[callback_hash]
+
+        state = StrategyState(self, AttributeDict(variables), symbol=symbol)
 
         # since it's less than 10 sec, we will just use the websocket feed - exchanges don't like fast calls
-        self.orderbook_manager.create_orderbook(self.__orderbook_event, initially_stopped=True,
+        self.orderbook_manager.create_orderbook(self.__websocket_callback, initially_stopped=True,
                                                 override_symbol=symbol,
                                                 symbol=symbol,
                                                 user_callback=callback,
                                                 variables=variables,
-                                                state_object=state)
-
-        exchange_type = self.__exchange.get_type()
-        self.__orderbook_websockets.append([symbol, exchange_type, init, state, teardown])
+                                                state=state)
 
-        # Set this to true so that we can throw a warning in the backtest
-        self.__using_orderbook = True
+        self.ticker_websockets.append([symbol, self.__exchange.get_type(), init, state, teardown])
 
     def start(self):
         """
@@ -344,205 +270,20 @@ def start(self):
 
         Simply call this function to take your strategy configuration live on your exchange
         """
-        if self.__remote_backtesting:
-            warnings.warn("Aborted attempt to start a live strategy a backtest configuration")
-            return
-        for i in self.__schedulers:
-            kwargs = i.get_kwargs()
-            if kwargs['init'] is not None:
-                kwargs['init'](kwargs['symbol'], kwargs['state_object'])
-            i.start()
-
-        for i in self.__orderbook_websockets:
-            # Index 2 contains the initialization function for the assigned websockets array
-            if i[2] is not None:
-                i[2](i[0], i[3])
-            self.orderbook_manager.restart_ticker(i[0], i[1])
-
-        for i in self.__ticker_websockets:
-            # The initialization function should have already been called for ticker websockets
-            # Notice this is different from orderbook websockets because these are put into the scheduler
-            self.ticker_manager.restart_ticker(i[0], i[1])
+        raise NotImplementedError
 
     def teardown(self):
-        self.lock.acquire()
-        for i in self.__schedulers:
-            i.stop_scheduler()
-            kwargs = i.get_kwargs()
-            teardown = kwargs['teardown']
-            state_object = kwargs['state_object']
-            if callable(teardown):
-                teardown(state_object)
-
-        for i in self.__orderbook_websockets:
-            self.orderbook_manager.close_websocket(override_symbol=i[0], override_exchange=i[1])
-            # Call the stored teardown
-            teardown_func = i[4]
-            if callable(teardown_func):
-                teardown_func(i[3])
-
-        for i in self.__ticker_websockets:
-            self.ticker_manager.close_websocket(override_symbol=i[0], override_exchange=i[1])
-        self.lock.release()
-
-        # Show that all teardowns have finished
-        self.torndown = True
+        raise NotImplementedError
 
     def time(self) -> float:
-        if self.backtesting_controller is not None and self.backtesting_controller.time is not None:
-            return self.backtesting_controller.time
-        else:
-            return time.time()
+        raise NotImplementedError
 
     def backtest(self,
                  to: str = None,
                  initial_values: dict = 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,
                  **kwargs
                  ) -> BacktestResult:
-        """
-        Turn this strategy into a backtest.
-
-        Args:
-            ** We expect either an initial_value (in USD) or a dictionary of initial values, we also expect
-            either `to` to be set or `start_date` and `end_date` **
-
-            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". 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.
-            callbacks (list of callables): Custom functions that will be run at the end of the backtest
-
-            Keyword Arguments:
-                **Use these to override parameters in the backtest.json file**
-
-                use_price: str = 'close',
-                    Set which price column to use.
-
-                smooth_prices: bool = False,
-                    Create linear connections between downloaded prices
-
-                GUI_output: bool = True,
-                    Enable/disable GUI webpage display after backtest
-
-                show_tickers_with_zero_delta: bool = False,
-                    Exclude tickers that have no change to account value in the GUI
-
-                save_initial_account_value: bool = True,
-                    Put an extra frame which contains the initial account values before any trade
-
-                show_progress_during_backtest: bool = True,
-                    Show a progress bar as the backtest runs
-
-                cache_location: str = './price_caches'
-                    Set a location for the price cache csv's to be written to
-
-                continuous_caching: bool
-                    Utilize the advanced price caching system built into the backtest. Automatically aggregate and prune
-                    downloaded data.
-
-                resample_account_value_for_metrics: str or bool = '1d' or False
-                    Because backtest data can be input at a variety of resolutions, account value often needs to be
-                        recalculated at consistent intervals for use in metrics & indicators.
-                        This setting allows the specification of that consistent interval.
-                        The value can be set to `False` to skip any recalculation.
-
-                quote_account_value_in: str = 'USD'
-                    Manually set what valuation should be used when calculating account value.
-                        Multiple types of quote currency (ex: USD and EUR) are not supported because
-                        there is no datasource for quoting pairs such as EUR-USD until forex integration.
-
-                ignore_user_exceptions: bool = True
-                    Set this to True to handle user exceptions identically to how they're handled by strategy calls.
-                        False means that the backtest will immediately stop & attempt to generate a report if something
-                        in the user calls goes wrong. True will replicate strategy errors.
-
-                risk_free_return_rate: float = 0.0
-                    Set this to be the theoretical rate of return with no risk
-        """
-
-        start = None
-        end = None
-
-        # Even if they specified start/end unevenly it will be overwritten with any to argument
-        if to is not None:
-            start = time.time() - time_interval_to_seconds(to)
-            end = time.time()
-
-        if start_date is not None:
-            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:
-            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):
-            raise ValueError("Both start and end dates must be set or use the 'to' argument.")
-
-        self.interface = self.__paper_trade_exchange.get_interface()
-        self.backtesting_controller = BackTestController(self.__paper_trade_exchange,
-                                                         backtest_settings_path=settings_path,
-                                                         callbacks=callbacks
-                                                         )
-
-        # Write any kwargs as settings to the settings - save if enabled.
-        for k, v in kwargs.items():
-            self.backtesting_controller.write_setting(k, v, save)
-
-        if initial_values is not None:
-            self.backtesting_controller.write_initial_price_values(initial_values)
-
-        # Write each scheduling pair as a price event - save if enabled.
-        if start is not None and end is not None:
-            for i in self.__scheduling_pair:
-                # None means live which is orderbook, which we skip anyway
-                if i[1] is not None:
-                    self.backtesting_controller.add_prices(i[0], start, end, i[1], save=save)
-        else:
-            info_print("User-specified start and end time not given. Defaulting to using only cached data.")
-
-        if self.__using_orderbook:
-            warning_string = "Artificial orderbook generation is not yet supported for backtesting - " \
-                             "skipping orderbook callbacks."
-            warnings.warn(warning_string)
-
-        # Append each of the events the class defines into the backtest
-        for i in self.__schedulers:
-            kwargs = i.get_kwargs()
-            self.backtesting_controller.append_backtest_price_event(callback=kwargs['callback'],
-                                                                    asset_id=kwargs['symbol'],
-                                                                    time_interval=i.get_interval(),
-                                                                    state_object=kwargs['state_object'],
-                                                                    ohlc=kwargs['ohlc'],
-                                                                    init=kwargs['init'],
-                                                                    teardown=kwargs['teardown']
-                                                                    )
-
-        # Run the backtest & return results
-        results = self.backtesting_controller.run()
-
-        blankly.reporter.export_backtest_result(results)
-
-        # Clean up
-        self.interface = self.__interface_cache
-        return results
+        raise NotImplementedError
diff --git a/blankly/frameworks/strategy/strategy_state.py b/blankly/frameworks/strategy/strategy_state.py
index 0f15764e..fc444441 100644
--- a/blankly/frameworks/strategy/strategy_state.py
+++ b/blankly/frameworks/strategy/strategy_state.py
@@ -31,8 +31,13 @@ def __init__(self, strategy, variables: AttributeDict, symbol, resolution: [int,
         self.variables = variables
         self.resolution = resolution
         self.symbol = symbol
-        self.base_asset = get_base_asset(symbol)
-        self.quote_asset = get_quote_asset(symbol)
+
+        # Base & quotes are conditionally defined
+        self.base_asset = None
+        self.quote_asset = None
+        if isinstance(symbol, str):
+            self.base_asset = get_base_asset(symbol)
+            self.quote_asset = get_quote_asset(symbol)
 
     @property
     def interface(self) -> Interface:
diff --git a/blankly/frameworks/strategy/strategy_usage_example.py b/blankly/frameworks/strategy/strategy_usage_example.py
deleted file mode 100644
index d3c7a237..00000000
--- a/blankly/frameworks/strategy/strategy_usage_example.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import blankly
-from blankly.frameworks.strategy import Strategy, StrategyState
-
-
-def golden_cross(price, ticker, state: StrategyState):
-    resolution: float = state.resolution  # get the resolution that this price event is stored at
-    variables = state.variables  # each price event has it's own local variable state
-    interface: blankly.Interface = state.interface
-
-    account = interface.account  # get account holdings
-
-    historical_prices = blankly.historical(ticker, 50, resolution=resolution)
-    sma50 = blankly.analysis.calculate_sma(historical_prices, window=50)[
-        -1]  # last 50 day sma value is the value we want
-
-    if price > sma50 and variables['order_submitted']:
-        variables['order_submitted'] = True
-        interface.market_order(ticker, 'buy', account.cash * 0.25)
-    elif price < sma50:  # only sell if there's an open position
-        variables['order_submitted'] = False
-        stocks_available = account.holdings[ticker].available * 0.5  # sell 50% of available sures
-        interface.market_order(ticker, 'sell', stocks_available)
-
-
-def rsi(price, ticker, interface: blankly.Interface, state: StrategyState):
-    resolution: str = state.resolution  # get the resolution that this price event is stored at
-    variables = state.variables  # each price event has it's own local variable state
-
-    account = interface.account  # get account holdings
-
-    historical_prices = blankly.historical(ticker, 50, resolution=resolution)
-    rsi = blankly.analysis.calculate_rsi(historical_prices)[-1]  # last 50 day sma value is the value we want
-
-    if rsi > 70 and variables['order_submitted']:
-        variables['order_submitted'] = True
-        interface.market_order(ticker, 'buy', account.cash * 0.25)
-    elif rsi < 30:  # only sell if there's an open position
-        variables['order_submitted'] = False
-        stocks_available = account.holdings[ticker].available * 0.5  # sell 50% of available sures
-        interface.market_order(ticker, 'sell', stocks_available)
-
-
-if __name__ == "__main__":
-    coinbase_pro = blankly.CoinbasePro()
-    alpaca = blankly.Alpaca()
-
-    coinbase_strategy = Strategy(coinbase_pro)
-    alpaca_strategy = Strategy(alpaca)
-
-    coinbase_strategy.add_price_event(golden_cross, symbol='BTC-USD', resolution='15m')
-    coinbase_strategy.add_price_event(rsi, symbol='XLM-USD', resolution='15m')
-
-    alpaca_strategy.add_price_event(golden_cross, symbol='MSFT', resolution='15m')
-    alpaca_strategy.add_price_event(rsi, symbol='AAPL', resolution='15m')
diff --git a/blankly/futures.py b/blankly/futures.py
new file mode 100644
index 00000000..6c1f01c3
--- /dev/null
+++ b/blankly/futures.py
@@ -0,0 +1,6 @@
+from blankly.exchanges.interfaces.binance_futures.binance_futures import BinanceFutures
+from blankly.exchanges.interfaces.ftx_futures.ftx_futures import FTXFutures
+from blankly.frameworks.strategy import FuturesStrategy
+from blankly.frameworks.strategy import FuturesStrategyState
+
+from blankly.enums import MarginType, HedgeMode
\ No newline at end of file
diff --git a/blankly/metrics/portfolio.py b/blankly/metrics/portfolio.py
index 84d74ac1..b7fc74aa 100644
--- a/blankly/metrics/portfolio.py
+++ b/blankly/metrics/portfolio.py
@@ -27,9 +27,6 @@
 def cagr(start_value, end_value, years):
     if years == 0:
         return 0.0
-    if end_value < start_value:
-        info_print('End Value less than Start Value makes CAGR meaningless, returning 0')
-        return 0.0
     return (end_value / start_value) ** (1 / years) - 1
 
 
diff --git a/blankly/utils/__init__.py b/blankly/utils/__init__.py
index 1ce5392c..8f560f35 100644
--- a/blankly/utils/__init__.py
+++ b/blankly/utils/__init__.py
@@ -1,1 +1,1 @@
-from blankly.utils.utils import trunc, time_interval_to_seconds
+from blankly.utils.utils import *
diff --git a/blankly/utils/scheduler.py b/blankly/utils/scheduler.py
index d7332ae4..98b1e85f 100644
--- a/blankly/utils/scheduler.py
+++ b/blankly/utils/scheduler.py
@@ -119,18 +119,18 @@ def __threading_wait(self, func, interval, kwargs):
             base_time = ceil_date(dt.now(), seconds=interval).timestamp()
             offset = base_time - time.time()
             time.sleep(offset)
-            kwargs['ohlcv_time'] = base_time
+            kwargs['bar_time'] = base_time
         while True:
             if self.__stop:
                 break
-            # This try except is replicated in the backtesting framework
+            # This try except is replicated in the strategy structure
             try:
                 func(**kwargs)
             except Exception:
                 traceback.print_exc()
             base_time += interval
             if self.synced:
-                kwargs['ohlcv_time'] += interval
+                kwargs['bar_time'] += interval
 
             # The downside of this is that it keeps the thread running while waiting to stop
             # It's dependent on delay if its more efficient to just check more
diff --git a/blankly/utils/time_builder.py b/blankly/utils/time_builder.py
index 99de02cd..e015a6a5 100644
--- a/blankly/utils/time_builder.py
+++ b/blankly/utils/time_builder.py
@@ -17,6 +17,7 @@
 """
 
 from typing import Union
+from numpy import int64, int32
 
 
 def build_second() -> int:
@@ -63,7 +64,8 @@ def time_interval_to_seconds(interval: Union[str, float]) -> float:
     """
     Extract the number of seconds in an interval string
     """
-    if isinstance(interval, float) or isinstance(interval, int):
+    if isinstance(interval, float) or isinstance(interval, int) or isinstance(interval, int64) or \
+            isinstance(interval, int32):
         return float(interval)
     # Extract intervals
     if 'mo' in interval:
diff --git a/blankly/utils/utils.py b/blankly/utils/utils.py
index 929f02f4..21603e4b 100644
--- a/blankly/utils/utils.py
+++ b/blankly/utils/utils.py
@@ -12,7 +12,7 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-
+import math
 
 import blankly
 
@@ -50,7 +50,8 @@
             "cash": "USDT"
         },
         "ftx": {
-            "cash": "USD"
+            "cash": "USD",
+            "ftx_tld": "us"
         },
         "alpaca": {
             "websocket_stream": "iex",
@@ -134,7 +135,7 @@ def __init__(self, default_path: str, default_settings: dict, not_found_err: str
             default_settings: The default settings in which to compare the loaded settings to. This helps the user
              learn if they're missing important settings and avoids keyerrors later on
             not_found_err: A string that is shown if the file they specify is not found
-            allow_nonexistent: Enable this to create the file if not found
+            allow_nonexistent: Enable this to just get the defaults if the file isn't found
         """
         self.__settings_cache = {}
         self.__default_path = default_path
@@ -174,9 +175,10 @@ def load(self, override_path=None) -> dict:
                 preferences = load_json_file(self.__default_path)
             except FileNotFoundError:
                 if self.__allow_nonexistent:
-                    self.write(self.__default_settings)
-                    # Recursively run this
-                    return self.load(override_path)
+                    return self.__default_settings
+                    # self.write(self.__default_settings)
+                    # # Recursively run this
+                    # return self.load(override_path)
                 else:
                     raise FileNotFoundError(self.__not_found_err)
             preferences = self.__compare_dicts(self.__default_settings, preferences)
@@ -776,6 +778,19 @@ def check_backtesting() -> bool:
         return False
 
 
+def enforce_base_asset(func):
+    """
+    Used for get_account functions, this enforces that the user is always getting the base asset which is probably
+    what the user meant
+    """
+    def wrapper(self, symbol=None):
+        # Get the base asset if it was defined
+        if symbol is not None:
+            symbol = get_base_asset(symbol)
+        return func(self, symbol=symbol)
+    return wrapper
+
+
 def order_protection(func):
     """
     Decorator to provide protection against live orders inside backtest environment
@@ -802,3 +817,19 @@ def add_all_products(nonzero_products: dict, all_products: list):
             }
 
     return nonzero_products
+
+
+def increment_to_precision(increment: float) -> int:
+    # quick maths
+    # 0.0001 -> 4
+    # 0.025 -> 1
+    # 0.25 -> 0
+    return math.floor(-math.log10(increment))
+
+
+def trim_df_time_column(df, epoch_start: [int, float], epoch_stop: [int, float]):
+    df = df[df['time'] >= epoch_start]
+    df = df[df['time'] <= epoch_stop]
+
+    return df
+
diff --git a/examples/futures.py b/examples/futures.py
new file mode 100644
index 00000000..f797e1c6
--- /dev/null
+++ b/examples/futures.py
@@ -0,0 +1,21 @@
+import blankly
+
+
+def price_event(price, symbol, state: blankly.FuturesStrategyState):
+    state.interface.market_order(symbol, side='buy', position='short', size=1)
+
+
+if __name__ == "__main__":
+    # Authenticate Binance Futures exchange
+    exchange = blankly.BinanceFutures()
+
+    # Use our strategy helper on binance futures
+    strategy = blankly.FuturesStrategy(exchange)
+
+    strategy.add_price_event(price_event, symbol='BTC-USDT', resolution='1d')
+
+    # Start the strategy. This will begin each of the price event ticks
+    # strategy.start()
+    # Or backtest using this:
+    results = strategy.backtest(to='1M', initial_values={'USDT': 1000000})
+    print(results)
diff --git a/examples/futures_rsi.py b/examples/futures_rsi.py
new file mode 100644
index 00000000..d3b86091
--- /dev/null
+++ b/examples/futures_rsi.py
@@ -0,0 +1,81 @@
+"""
+    Futures RSI Example.
+    Copyright (C) 2022 Matias Kotlik
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+
+import blankly
+from blankly import Side
+from blankly.futures import FuturesStrategyState, FTXFutures, FuturesStrategy, MarginType
+
+
+def price_event(price, symbol, state: FuturesStrategyState):
+    state.variables['history'].append(price)
+    rsi = blankly.indicators.rsi(state.variables['history'])
+
+    if rsi[-1] < 30:
+        # rsi < 30 indicates the asset is undervalued or will rise in price
+        # we want to go long.
+        if state.interface.positions['BTC-USDT'].size <= 0:
+            close_position(symbol, state)
+            size = blankly.trunc(state.interface.cash / price, 2)
+            state.interface.market_order(symbol, side=Side.BUY, size=size)
+    elif rsi[-1] > 70:
+        # rsi < 70 indicates the asset is overvalued or will drop in price
+        # we want to short the asset.
+        if state.interface.positions['BTC-USDT'].size >= 0:
+            close_position(symbol, state)
+            size = blankly.trunc(state.interface.cash / price, 2)
+            state.interface.market_order(symbol, side=Side.SELL, size=size)
+
+
+def close_position(symbol, state: FuturesStrategyState):
+    size = state.interface.positions[symbol].size
+    if size < 0:
+        state.interface.market_order(symbol,
+                                     Side.BUY,
+                                     abs(size),
+                                     reduce_only=True)
+    elif size > 0:
+        state.interface.market_order(symbol,
+                                     Side.SELL,
+                                     abs(size),
+                                     reduce_only=True)
+
+
+def init(symbol, state: FuturesStrategyState):
+    close_position(symbol, state)
+
+    # Set initial leverage and margin type
+    state.interface.set_leverage(symbol, 1)
+
+    state.interface.set_margin_type(symbol, MarginType.ISOLATED)
+
+    state.variables['history'] = state.interface.history(
+        symbol, to=150, return_as='deque',
+        resolution=state.resolution)['close']
+
+
+if __name__ == "__main__":
+    exchange = FTXFutures()
+
+    strategy = FuturesStrategy(exchange)
+    strategy.add_price_event(price_event,
+                             init=init,
+                             teardown=close_position,
+                             symbol='BTC-USDT',
+                             resolution='1m')
+
+    strategy.start()
diff --git a/examples/golden_cross.py b/examples/golden_cross.py
index 6c7eead8..c29d930a 100644
--- a/examples/golden_cross.py
+++ b/examples/golden_cross.py
@@ -1,5 +1,5 @@
 """
-    This shows a simple golden cross example on MSFT, checking at a 1d resolution over two years
+    This shows a simple golden cross example on GME and SNAP checking at a 1h resolution over two years
 """
 
 from blankly import Strategy, StrategyState, Interface
diff --git a/examples/macd.py b/examples/macd.py
index 27169e19..377d454a 100644
--- a/examples/macd.py
+++ b/examples/macd.py
@@ -69,4 +69,4 @@ def price_event(price, symbol, state: StrategyState):
     s.add_price_event(price_event, 'D', resolution='1d', init=init)
     s.add_price_event(price_event, 'GME', resolution='1d', init=init)
 
-    print(s.backtest(initial_values={'USD': 10000}, to='2y').get_quantstats_metrics())
+    print(s.backtest(initial_values={'USD': 10000}, to='2y'))
diff --git a/examples/multicore_bot.py b/examples/multicore_bot.py
index f9e276fb..69264a26 100644
--- a/examples/multicore_bot.py
+++ b/examples/multicore_bot.py
@@ -19,7 +19,7 @@ def main(self, args):
         self.update_state("heartbeat", 0)
 
         # This will work on any supported exchange
-        print("Interface call: " + str(self.interface.get_fees()))
+        print("Interface call: " + str(self.interface.get_fees(args['symbol'])))
 
         # You can also bypass the interface and make calls directly to the exchange:
         try:
diff --git a/examples/rsi_screener.py b/examples/rsi_screener.py
index 298df10c..992cd36b 100644
--- a/examples/rsi_screener.py
+++ b/examples/rsi_screener.py
@@ -13,6 +13,7 @@ def is_stock_buy(symbol, state: ScreenerState):
     rsi_values = rsi(prices['close'], 14)
     return {'is_oversold': bool(rsi_values[-1] < 30), 'price': price, 'symbol': symbol}
 
+
 def formatter(results, state: ScreenerState):
     # results is a dictionary on a per-symbol basis
     result_string = 'These are all the stocks that are currently oversold: \n'
diff --git a/examples/settings.json b/examples/settings.json
index 9ba3a3f3..ff677fce 100644
--- a/examples/settings.json
+++ b/examples/settings.json
@@ -3,7 +3,6 @@
     "use_sandbox_websockets": false,
     "websocket_buffer_size": 10000,
     "test_connectivity_on_auth": true,
-
     "coinbase_pro": {
       "cash": "USD"
     },
@@ -11,6 +10,10 @@
       "cash": "USDT",
       "binance_tld": "us"
     },
+    "binance_futures": {
+      "cash": "USDT",
+      "margin_type": "USDT-M"
+    },
     "alpaca": {
       "websocket_stream": "iex",
       "cash": "USD",
@@ -20,6 +23,9 @@
     "oanda": {
       "cash": "USD"
     },
+    "okx": {
+      "cash": "USDT"
+    },
     "keyless": {
       "cash": "USD"
     },
@@ -27,7 +33,12 @@
       "cash": "USDT"
     },
     "ftx": {
-      "cash": "USD"
+      "cash": "USD",
+      "ftx_tld": "us"
+    },
+    "ftx_futures": {
+      "cash": "USD",
+      "ftx_tld": "com"
     }
   }
 }
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 224a7795..6b59daa7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,2 +1,6 @@
 [metadata]
-description-file = README.md
\ No newline at end of file
+description-file = README.md
+
+[yapf]
+based_on_style: pep8
+allow_split_before_dict_value: false
\ No newline at end of file
diff --git a/setup.py b/setup.py
index c6f54e5c..27935e9b 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.16.0-beta',
+    version='v1.16.20-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,
@@ -23,12 +23,15 @@
         # Extension("blankly.deployment.hello", [path.join("blankly", "deployment", "hello.cpp")])  # Disabled extension
     ],
     author='blankly',
-    entry_points={'console_scripts': ['blankly = blankly.deployment.cli:main']},
+    entry_points={'console_scripts': ['blankly_old = blankly.deployment.cli:main',
+                                      'blankly = blankly.deployment.new_cli:main']},
     author_email='contact@blankly.finance',
     url='https://github.com/Blankly-Finance/Blankly',  # Could be github or website
     # download_url='https://github.com/EmersonDove/Blankly/archive/v0.1.1-alpha.tar.gz',
     keywords=['Crypto', 'Stocks', 'Quantitative Finance', 'Exchanges', 'Bot'],  # Keywords
     install_requires=[
+        'questionary >= 1.10.0',
+        'yaspin >= 2.1.0',
         'alpaca-trade-api >= 1.4.2',
         'bokeh >= 2.4.2',
         'dateparser >= 1.1.0',


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index be5c127e..8c356c3c 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -7,20 +7,14 @@ on: [push, workflow_dispatch]
 jobs:
   build:
 
-    runs-on: ${{ matrix.os }}
-
-    strategy:
-      max-parallel: 1
-      matrix:
-        os: [windows-latest, macos-latest, ubuntu-latest]
-        python-version: ['3.7', '3.8', '3.9', '3.10']
+    runs-on: windows-latest
 
     steps:
     - uses: actions/checkout@v2
     - name: Set up Python
       uses: actions/setup-python@v2
       with:
-        python-version: ${{ matrix.python-version }}
+        python-version: 3.10
 
     - name: Install dependencies
       run: |
@@ -40,4 +34,3 @@ jobs:
       run: |
         pytest
         echo "Sleeping to reduce rate limit pressure..."
-        sleep 200
diff --git a/blankly/deployment/cpp_test.py b/blankly/deployment/cpp_test.py
deleted file mode 100644
index 86024679..00000000
--- a/blankly/deployment/cpp_test.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from blankly.deployment.hello import hello
-
-
-if __name__ == "__main__":
-    print("before")
-    print(hello("wassup"))
-    print("after")
\ No newline at end of file
diff --git a/blankly/exchanges/interfaces/paper_trade/abc_backtest_controller.py b/blankly/exchanges/interfaces/paper_trade/abc_backtest_controller.py
new file mode 100644
index 00000000..303e00e6
--- /dev/null
+++ b/blankly/exchanges/interfaces/paper_trade/abc_backtest_controller.py
@@ -0,0 +1,55 @@
+"""
+    Headers to provide a clean UI to users
+    Copyright (C) 2021  Emerson Dove
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+import abc
+import typing
+from blankly.data.data_reader import PriceReader, EventReader, TickReader
+
+
+class ABCBacktestController(abc.ABC):
+    # Only becomes defined when backtest is run
+    initial_time = None
+
+    @abc.abstractmethod
+    def add_prices(self, symbol: str,
+                   resolution: [str, int, float],
+                   to: str = None,
+                   start_date: typing.Union[str, float, int] = None,
+                   stop_date: typing.Union[str, float, int] = None):
+        """
+        Add prices directly to the backtest engine
+        """
+        pass
+
+    @abc.abstractmethod
+    def add_custom_prices(self, price_reader: PriceReader):
+        pass
+
+    @abc.abstractmethod
+    def add_custom_events(self, event_reader: EventReader):
+        pass
+
+    @abc.abstractmethod
+    def add_tick_events(self, tick_reader: TickReader):
+        pass
+
+    @abc.abstractmethod
+    def value_account(self):
+        """
+        Value your account at this time
+        """
+        pass
diff --git a/blankly/exchanges/interfaces/paper_trade/backtest/__init__.py b/blankly/exchanges/interfaces/paper_trade/backtest/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/blankly/exchanges/interfaces/paper_trade/backtest/format_platform_result.py b/blankly/exchanges/interfaces/paper_trade/backtest/format_platform_result.py
new file mode 100644
index 00000000..c252d729
--- /dev/null
+++ b/blankly/exchanges/interfaces/paper_trade/backtest/format_platform_result.py
@@ -0,0 +1,209 @@
+"""
+    Formatting for platform backtest result
+    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 uuid
+
+
+def __dict_from_df_dict(dict_of_df: dict) -> dict:
+    """
+    This creates a serializable dictionary from a dictionary with pandas df:
+    {
+        'AAPL': df.DataFrame
+        'MSFT': df.DataFrame
+    }
+    ->
+    {
+        'AAPL': df.DataFrame.to_dict()
+        'MSFT': df.DataFrame.to_dict()
+    }
+    The function does not check for nested dataframes
+
+    :arg dict_of_df: A dictionary with dataframes as values of the internal keys
+    :return: dict
+    """
+    output_dict = {}
+    for i in list(dict_of_df.keys()):
+        output_dict[i] = dict_of_df[i].to_dict()
+
+    return output_dict
+
+
+def __compress_dict_series(values_column: dict, time_dictionary: dict):
+    """
+    Remove duplicate keys from the account value or anything that is associated with a time series
+
+    :param values_column: A dictionary with index as key and any sort of value
+    :param time_dictionary: A dictionary with index also as key but a time that corresponds to the same index in the
+        values column
+    :return: None
+    """
+    values_keys = list(values_column.keys())
+    time_keys = list(time_dictionary.keys())
+
+    # Check to see if the dictionaries are longer than 0
+    if len(values_keys) == 0:
+        return {}
+
+    last_value = values_column[values_keys[0]]
+    output_dict = {
+        time_dictionary[time_keys[0]]: last_value
+    }
+    for i in range(len(values_column)):
+        if last_value != values_column[values_keys[i]]:
+            output_dict[time_dictionary[time_keys[i]]] = values_column[values_keys[i]]
+
+            last_value = values_column[values_keys[i]]
+
+    return output_dict
+
+
+def __parse_backtest_trades(trades: list, limit_executed: list, limit_canceled: list, market_executed: list):
+    """
+    Determine the lifecycle of limit orders
+
+    :param trades: The list of trades from the backtest
+    :param limit_executed: The list of limit orders that were executed
+    :param limit_canceled: The list of limit orders that were canceled
+    :param market_executed: The list of executed market orders
+    :return: None
+    """
+
+    # Now just parse if there should be an executed time or a canceled time
+    for i in range(len(trades)):
+        try:
+            trades[i]['time'] = trades[i].pop('created_at')
+        except KeyError:
+            pass
+        if trades[i]['type'] == 'limit':
+            # TODO this wastes a few CPU cycles at the moment so it could be cleaned up
+            for j in limit_executed:
+                if trades[i]['id'] == j['id']:
+                    trades[i]['time'] = j['executed_time']
+                    break
+
+            for j in limit_canceled:
+                if trades[i]['id'] == j['id']:
+                    trades[i]['canceledTime'] = j['canceled_time']
+                    break
+        elif trades[i]['type'] == 'market':
+            # This adds in the execution price for the market orders
+            trades[i]['type'] = 'spot-market'
+            for j in market_executed:
+                if trades[i]['id'] == j['id']:
+                    trades[i]['price'] = j['executed_price']
+                    break
+
+    return trades
+
+
+def format_metric(metrics: dict, display_name, type_: str):
+    return {
+        "value": metrics[display_name],
+        "display_name": display_name,
+        "type": type_
+    }
+
+
+def format_platform_result(backtest_result):
+    """
+    Export the finished backtest result
+
+    Args:
+        backtest_result: A BacktestResult object to export
+    """
+    # We're given a dictionary of dataframes and that has to be transformed to a dictionary of dictionaries
+    history_and_returns = __dict_from_df_dict(backtest_result.history_and_returns)
+
+    # Grab a list of the traded assets
+    # The paper trade interface format may change in the future to be more optimized
+    traded_symbols = []
+    for i in backtest_result.trades['created']:
+        if i['symbol'] not in traded_symbols:
+            traded_symbols.append(i['symbol'])
+
+    # Set the account values to None
+    raw_or_resampled_account_values = None
+
+    # Now grab the account value dictionary itself
+    # Now just replicate the format of the resampled version
+    # This was the annoying backtest glitch that almost cost us an investor meeting so its important
+    history = history_and_returns['history']
+    account_value_name = 'Account Value (' + backtest_result.quote_currency + ')'
+    raw_or_resampled_account_values = {
+        'value': history[account_value_name],
+        'time': history['time']
+    }
+
+    # Now grab the raw account values
+    compressed_asset_column = __compress_dict_series(raw_or_resampled_account_values['value'],
+                                                     raw_or_resampled_account_values['time'])
+
+    compressed_array_conversion = []
+    for i in compressed_asset_column:
+        compressed_array_conversion.append({
+            'time': i,
+            'value': compressed_asset_column[i]
+        })
+
+    compressed_asset_keys = list(compressed_asset_column.keys())
+    first_account_value = compressed_asset_column[compressed_asset_keys[0]]
+    last_account_value = compressed_asset_column[compressed_asset_keys[-1]]
+
+    trades = __parse_backtest_trades(backtest_result.trades['created'],
+                                     backtest_result.trades['limits_executed'],
+                                     backtest_result.trades['limits_canceled'],
+                                     backtest_result.trades['executed_market_orders'])
+
+    # Now change the metrics keys to be nicer
+    metrics = backtest_result.metrics
+    refined_metrics = {
+        'calmar': format_metric(metrics, 'Calmar Ratio', 'number'),
+        'cagr': format_metric(metrics, 'Compound Annual Growth Rate (%)', 'number'),
+        'cavr': format_metric(metrics, 'Conditional Value-at-Risk', 'number'),
+
+        'cum_returns': format_metric(metrics, 'Cumulative Returns (%)', 'number'),
+        'max_drawdown': format_metric(metrics, 'Max Drawdown (%)', 'number'),
+        'resampled_time': format_metric(metrics, 'Resampled Time', 'number'),
+
+        'risk_free_rate': format_metric(metrics, 'Risk Free Return Rate', 'number'),
+        'sharpe': format_metric(metrics, 'Sharpe Ratio', 'number'),
+        'sortino': format_metric(metrics, 'Sortino Ratio', 'number'),
+
+        'value_at_risk': format_metric(metrics, 'Value-at-Risk', 'number'),
+        'variance': format_metric(metrics, 'Variance (%)', 'number'),
+        'volatility': format_metric(metrics, 'Volatility', 'number')
+    }
+
+    backtest_result.metrics = refined_metrics
+    return {
+        'exchange': backtest_result.exchange,
+        'symbols': traded_symbols,
+        'quote_asset': backtest_result.quote_currency,
+        'start_time': backtest_result.start_time,
+        'stop_time': backtest_result.stop_time,
+        'account_values': compressed_array_conversion,
+        'trades': trades,
+        'metrics': refined_metrics,
+        'indicators': {},  # Add to this array when we support
+        # strategy indicators
+        'user_callbacks': backtest_result.user_callbacks,
+        'initial_account_value': first_account_value,
+        'final_account_value': last_account_value,
+        'backtest_id': str(uuid.uuid4())
+        # Everything above here is put into the firebase root
+    }
diff --git a/blankly/exchanges/interfaces/paper_trade/backtest_controller.py b/blankly/exchanges/interfaces/paper_trade/backtest_controller.py
index 665e0ec9..c97723c6 100644
--- a/blankly/exchanges/interfaces/paper_trade/backtest_controller.py
+++ b/blankly/exchanges/interfaces/paper_trade/backtest_controller.py
@@ -15,15 +15,19 @@
     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 os
+import time
 import traceback
 import typing
 from datetime import datetime as dt
 import copy
+import enum
+import blankly
 
 import numpy as np
 import pandas as pd
+import requests
 from bokeh.layouts import column as bokeh_columns
 from bokeh.models import HoverTool
 from bokeh.palettes import Category10_10
@@ -31,11 +35,16 @@
 
 import blankly.exchanges.interfaces.paper_trade.metrics as metrics
 from blankly.exchanges.interfaces.paper_trade.backtest_result import BacktestResult
-from blankly.exchanges.interfaces.paper_trade.paper_trade import PaperTrade
 from blankly.exchanges.interfaces.paper_trade.paper_trade_interface import PaperTradeInterface
 from blankly.utils.time_builder import time_interval_to_seconds
-from blankly.utils.utils import load_backtest_preferences, update_progress, write_backtest_preferences, \
-    get_base_asset, get_quote_asset, info_print, trunc
+from blankly.utils.utils import load_backtest_preferences, write_backtest_preferences, info_print, update_progress, \
+    get_base_asset, get_quote_asset
+from blankly.exchanges.interfaces.paper_trade.backtest.format_platform_result import \
+    format_platform_result
+
+from blankly.exchanges.interfaces.paper_trade.abc_backtest_controller import ABCBacktestController
+from blankly.exchanges.exchange import ABCExchange
+from blankly.data.data_reader import PriceReader, EventReader, TickReader
 
 
 def to_string_key(separated_list):
@@ -80,6 +89,7 @@ def intersection(as_, ae, bs, be):
             os_ = max(as_, bs)
             oe = min(ae, be)
             return [os_, oe]
+
     for i in local_segments:
         intersection_range = intersection(base_range[0], base_range[1], i[0], i[1])
         if intersection_range is None:
@@ -99,7 +109,7 @@ def intersection(as_, ae, bs, be):
     aggregate_indexes = []
     for i in range(len(positive_ranges) - 1):
         intersection_range = intersection(positive_ranges[i][0], positive_ranges[i][1],
-                                          positive_ranges[i + 1][0],     positive_ranges[i + 1][1])
+                                          positive_ranges[i + 1][0], positive_ranges[i + 1][1])
         if intersection_range is not None:
             aggregate_indexes.append([i, i + 1])
 
@@ -118,7 +128,7 @@ def intersection(as_, ae, bs, be):
 
     # Now just try to find the gaps in the positive ranges and those are our negative ranges
     for i in range(len(positive_ranges) - 1):
-        negative_ranges.append([positive_ranges[i][1], positive_ranges[i+1][0]])
+        negative_ranges.append([positive_ranges[i][1], positive_ranges[i + 1][0]])
 
     try:
         # Now we just have to make sure to check the bounds
@@ -137,59 +147,128 @@ def intersection(as_, ae, bs, be):
     return used_ranges, negative_ranges
 
 
-class BackTestController:
-    def __init__(self, paper_trade_exchange: PaperTrade, backtest_settings_path: str = None, callbacks: list = None):
-        self.preferences = load_backtest_preferences(backtest_settings_path)
-        self.backtest_settings_path = backtest_settings_path
-        if not paper_trade_exchange.get_type() == "paper_trade":
-            raise ValueError("Backtest controller was not constructed with a paper trade exchange object.")
-        self.interface: PaperTradeInterface = paper_trade_exchange.get_interface()
-
-        self.price_events = []
+class BackTestController(ABCBacktestController):  # circular import to type model
+    def __init__(self, model):
+        self.backtesting = False
+        self.preferences = None
+        self.backtest_settings_path = None
 
-        if callbacks is None:
-            callbacks = []
-        self.callbacks = callbacks
+        self.interface = None
 
-        self.current_time = None
         self.initial_time = None
+        self.model = model
 
-        self.prices = []  # [epoch, "BTC-USD", price]
-
-        self.price_dictionary = {}
+        self.traded_account_values = []
+        self.no_trade_account_values = []
 
-        self.pd_prices = None
+        # Prices sorted by symbol and then records of prices
+        self.prices = {}
+        # A list of events sorted by time. All events are put into this single list
+        self.events = []
 
         # User added times
         self.__user_added_times = []
 
+        # Open, high, low, close or volume
         self.use_price = None
 
+        # Should we write to the preferences?
         self.queue_backtest_write = False
 
+        # The quote currency to use
         self.quote_currency = None
 
         # Create a global generator because a second yield function gets really nasty
+        # This is used for the colors of the graphs
         self.__color_generator = Category10_10.__iter__()
 
+        # Some initial account value to store globally
         self.initial_account = None
 
-        self.__exchange_type = self.interface.get_exchange_type()
-
         # 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
-        self.__current_search_index = 0
-
         # Export a time for use in other classes
         self.time = None
 
+        # The smallest amount of time to save
+        self.min_resolution = 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
+        self.show_progress = False
+        self.sleep_count = 0
+
+        # Use this global to retain where we are in the prices dictionary by index
+        self.price_indexes = {}
+        # Use this to keep trace globally of the event index we're using
+        self.event_index = 0
+
+        # Custom injected price readers and events readers
+        self.__price_readers = []
+        self.__event_readers = []
+        self.__tick_readers = []
+
+    class PriceIdentifiers(enum.Enum):
+        exchange: str = 0
+        sandbox: bool = 1
+        symbol: str = 2
+        epoch_start: int = 3
+        epoch_stop: int = 4
+        resolution: int = 5
+
+    def parse_events(self):
+        """
+        Transform to:
+        [
+            {
+                "type": "news event",
+                "data": "gotem",
+                "time": 2
+            }
+        ]
+        """
+        # TODO some code duplication here for the different events
+        for reader in self.__event_readers:
+            # Get the data as dict of dataframes
+            data = reader.data
+            for event_type in data:
+                self.__check_user_time_bounds(reader.data[event_type]['time'].iloc[0],
+                                              reader.data[event_type]['time'].iloc[-1],
+                                              60)
+                # Get the dataframe in each one
+                event_df: pd.DataFrame = data[event_type]
+                # Now turn it into a set of records
+                records = event_df.to_dict(orient='records')
+                # And make sure to add on the event type to each one
+                for record_index in range(len(records)):
+                    records[record_index]['type'] = event_type
+
+                self.events += records
+
+        for tick_reader in self.__tick_readers:
+            data = tick_reader.data
+            for symbol in data:
+                self.__check_user_time_bounds(tick_reader.data[symbol]['time'].iloc[0],
+                                              tick_reader.data[symbol]['time'].iloc[-1],
+                                              60)
+                symbol_df: pd.DataFrame = data[symbol]
+
+                records = symbol_df.to_dict(orient='records')
+                data_formatted_records = []
+                for record_index in range(len(records)):
+                    data_formatted_records.append({
+                        'type': '__blankly__tick',
+                        'data': records[record_index],
+                        'time': records[record_index]['time']
+                    })
+
+                self.events += data_formatted_records
+
+        # Now we just need to sort by time
+        self.events = sorted(self.events, key=lambda d: d['time'])
 
     def sync_prices(self) -> dict:
         """
@@ -201,150 +280,161 @@ def sync_prices(self) -> dict:
         returns:
             dictionary with keys for each 'symbol'
         """
-        
-        cache_folder = self.preferences['settings']["cache_location"]
+
         # Make sure the cache folder exists and read files
-        try:
-            files = os.listdir(cache_folder)
-        except FileNotFoundError:
-            files = []
-            os.mkdir(cache_folder)
-
-        available_files = []
-        for i in range(len(files)):
-            # example file name: 'BTC-USD.1622400000.1622510793.60.csv'
-            # Remove the .csv from each of the files: BTC-USD.1622400000.1622510793.60
-            identifier = files[i][:-4].split(",")
-            # Cast to float first before
-            try:
-                identifier[1] = int(float(identifier[1]))
-                identifier[2] = int(float(identifier[2]))
-                identifier[3] = int(float(identifier[3]))
-                available_files.append(identifier)
-            except IndexError:
-                raise IndexError(f"Please remove file {files[i]} in your price caches or "
-                                 f"match the blankly cache format.")
-
-        # This is only the downloaded data
-        local_history_blocks = {}
-        for i in available_files:
-            # Sort these into resolutions
-            asset = i[0]
-
-            # Make sure to add the asset as a key
-            if asset not in list(local_history_blocks.keys()):
-                local_history_blocks[asset] = {}
-
-            # Add each resolution to its own internal list
-            if i[3] not in list(local_history_blocks[asset].keys()):
-                local_history_blocks[asset][i[3]] = [i]
+        cache_folder = self.preferences['settings']["cache_location"]
+
+        def sort_prices_by_resolution(price_dict):
+            for symbol_ in price_dict:
+                for resolution_ in price_dict[symbol_]:
+                    price_dict[symbol_][resolution_] = price_dict[symbol_][resolution_].sort_values(by=['time'],
+                                                                                                    ignore_index=True)
+
+            return price_dict
+
+        def aggregate_prices_by_resolution(price_dict, symbol_, resolution_, data_) -> dict:
+            if symbol_ not in price_dict:
+                price_dict[symbol_] = {}
+            # Concat after the resolution check here
+            if resolution_ not in price_dict[symbol_]:
+                price_dict[symbol_][resolution_] = data_
             else:
-                local_history_blocks[asset][i[3]].append(i)
-
-        # def segment(conjoined_ranges: list) -> list:
-        #     """
-        #     Split a list with sets of even intervals into multiple lists. The list must be single increments in all
-        #     places except for the splits
-        #     """
-        #     ranges = []
-        #     segments = 0
-        #     for i in range(len(conjoined_ranges)-1):
-        #         i += 1
-        #         if conjoined_ranges[i] - conjoined_ranges[i-1] != 1:
-        #             segmented_list = conjoined_ranges[segments:i]
-        #
-        #             ranges.append((segmented_list[0], segmented_list[-1]))
-        #             segments = i + 1
-        #
-        #     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(items)):
-            if items[i] is None:
+                price_dict[symbol_][resolution_] = pd.concat([price_dict[symbol_][resolution_],
+                                                              data_])
+            return price_dict
+
+        def parse_identifiers() -> list:
+            try:
+                files = os.listdir(cache_folder)
+            except FileNotFoundError:
+                files = []
+                os.mkdir(cache_folder)
+
+            identifiers_ = []
+            for file in range(len(files)):
+                # example file name: 'coinbase_pro.sandbox.BTC-USD.1622400000.1622510793.60.csv'
+                # Remove the .csv from each of the files: BTC-USD.1622400000.1622510793.60
+                identifier = files[file][:-4].split(",")
+                # Cast to float first before
+                try:
+                    identifiers_.append({
+                        self.PriceIdentifiers.exchange: identifier[0],
+                        self.PriceIdentifiers.sandbox: identifier[1] == 'True',
+                        self.PriceIdentifiers.symbol: identifier[2],
+                        self.PriceIdentifiers.epoch_start: int(float(identifier[3])),
+                        self.PriceIdentifiers.epoch_stop: int(float(identifier[4])),
+                        self.PriceIdentifiers.resolution: int(float(identifier[5]))
+                    })
+                except IndexError:
+                    # Remove each of the failed cache objects
+                    os.remove(os.path.join(cache_folder, files[file]))
+
+            return identifiers_
+
+        def sort_identifiers(identifiers_: list) -> dict:
+            """
+            Create the hierarchy based on the identifiers
+            {
+                'coinbase_pro': {
+                    True: {
+                        'BTC-USD': [60, 3600, 86400]
+                    }
+                    False: {
+
+                    }
+                }
+            }
+            """
+            local_history_blocks_ = {}
+
+            def sort_identifier(identifier_: dict):
+                exchange_ = identifier_[self.PriceIdentifiers.exchange]
+                sandbox_ = identifier_[self.PriceIdentifiers.sandbox]
+                symbol_ = identifier_[self.PriceIdentifiers.symbol]
+                resolution_ = identifier_[self.PriceIdentifiers.resolution]
+
+                if exchange_ not in local_history_blocks_:
+                    local_history_blocks_[exchange_] = {}
+
+                if sandbox_ not in local_history_blocks_[exchange_]:
+                    local_history_blocks_[exchange_][sandbox_] = {}
+
+                if symbol_ not in local_history_blocks_[exchange_][sandbox_]:
+                    local_history_blocks_[exchange_][sandbox_][symbol_] = {}
+
+                # Now we're checking the resolution array
+                if resolution_ not in local_history_blocks_[exchange_][sandbox_][symbol_]:
+                    local_history_blocks_[exchange_][sandbox_][symbol_][resolution_] = []
+
+                # Add these in as a dictionary
+                local_history_blocks_[exchange_][sandbox_][symbol_][resolution_].append({
+                    self.PriceIdentifiers.epoch_start: identifier_[self.PriceIdentifiers.epoch_start],
+                    self.PriceIdentifiers.epoch_stop: identifier_[self.PriceIdentifiers.epoch_stop]
+                })
+
+            # This is only the downloaded data
+            for identifier in identifiers_:
+                sort_identifier(identifier)
+
+            return local_history_blocks_
+
+        identifiers = parse_identifiers()
+
+        local_history_blocks = sort_identifiers(identifiers)
+
+        final_prices: dict = {}
+        prices_by_resolution: dict = {}
+        for i in range(len(self.__user_added_times)):
+            if self.__user_added_times[i] is None:
                 continue
-            asset = items[i][0]
-            resolution = items[i][3]
-            start_time = items[i][1]
-            end_time = items[i][2] - resolution
+            symbol = self.__user_added_times[i][self.PriceIdentifiers.symbol]
+            resolution = self.__user_added_times[i][self.PriceIdentifiers.resolution]
+            start_time = self.__user_added_times[i][self.PriceIdentifiers.epoch_start]
+            end_time = self.__user_added_times[i][self.PriceIdentifiers.epoch_stop] - resolution
+            exchange = self.interface.get_exchange_type()
+            sandbox = True
 
             if end_time < start_time:
                 raise RuntimeError("Must specify a longer timeframe to run the backtest.")
 
-            download_ranges = []
+            downloaded_ranges = []
 
             # Attempt to find the same symbol/asset possibilities in the backtest blocks
             try:
                 # 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])
+                available_prices = copy.deepcopy(local_history_blocks[exchange][sandbox][symbol][resolution])
+                # Pull the epoch start and epoch stop for this particular symbol / resolution
+                for price_set in available_prices:
+                    downloaded_ranges.append([
+                        price_set[self.PriceIdentifiers.epoch_start],
+                        price_set[self.PriceIdentifiers.epoch_stop]
+                    ])
             except KeyError:
                 pass
 
-            for j in range(len(download_ranges)):
-                download_ranges[j] = download_ranges[j][1:3]
-
-            used_ranges, negative_ranges = split([start_time, end_time], download_ranges)
-
-            # for j in download_ranges:
-            #     download_start_time = j[1]
-            #     download_end_time = j[2] - resolution
-            #
-            #     contains_start = False
-            #     contains_end = False
-            #     if download_start_time <= start_time <= download_end_time:
-            #         contains_start = True
-            #     elif download_start_time <= end_time <= download_end_time:
-            #         contains_end = True
-            #
-            #     if contains_start or contains_end:
-            #         # Read in the whole thing if it has any sort of relevance on the data
-            #         relevant_df = pd.read_csv(os.path.join(cache_folder,
-            #                                                to_string_key(j) + ".csv"))
-            #
-            #         # If it only contains the start then it must be offset too high or is too long
-            #         if contains_start and not contains_end:
-            #             relevant_df = relevant_df[relevant_df['time'] >= start_time]
-            #
-            #             # Set the end time to match the first time in the relevant dataframe
-            #             self.__user_added_times[i][1] = relevant_df['time'].iloc[-1]
-            #
-            #         # If it only contains the end then it must be offset too low or is too short
-            #         if contains_end and not contains_start:
-            #             relevant_df = relevant_df[relevant_df['time'] <= end_time]
-            #
-            #             # Set the start time to match the very last time of the relevant dataframe
-            #             self.__user_added_times[i][2] = relevant_df['time'].iloc[0]
-            #
-            #         # If it contains start and end then it's just right
-            #         if contains_end and contains_start:
-            #             relevant_df = relevant_df[relevant_df['time'] >= start_time]
-            #             relevant_df = relevant_df[relevant_df['time'] <= end_time]
-            #
-            #             # This dataset has been completed so there is no need to continue
-            #             self.__user_added_times[i] = None
-            #
-            #         # Write these into the data array
-            #         if asset not in list(final_prices.keys()):
-            #             final_prices[asset] = relevant_df
-            #         else:
-            #             final_prices[asset] = pd.concat([final_prices[asset], relevant_df])
+            used_ranges, negative_ranges = split([start_time, end_time], downloaded_ranges)
 
             relevant_data = []
             for j in used_ranges:
-                relevant_data.append(pd.read_csv(os.path.join(cache_folder, to_string_key([asset, j[0], j[1],
+                relevant_data.append(pd.read_csv(os.path.join(cache_folder, to_string_key([exchange,
+                                                                                           True,
+                                                                                           symbol,
+                                                                                           j[0],
+                                                                                           j[1],
                                                                                            resolution]) + ".csv")))
 
             if len(relevant_data) > 0:
-                final_prices[asset] = pd.concat(relevant_data)
+                final_prices[symbol] = pd.concat(relevant_data)
+                for dataset in relevant_data:
+                    prices_by_resolution = aggregate_prices_by_resolution(prices_by_resolution, symbol, resolution,
+                                                                          dataset)
 
             # If there is any data left to download do it here
             for j in negative_ranges:
-                print("No cached data found for " + asset + " from: " + str(j[0]) + " to " +
+                print("No cached data found for " + symbol + " from: " + str(j[0]) + " to " +
                       str(j[1]) + " at a resolution of " + str(resolution) + " seconds.")
-                download = self.interface.get_product_history(asset,
+                download = self.interface.get_product_history(symbol,
                                                               j[0],
                                                               j[1],
                                                               resolution)
@@ -352,117 +442,149 @@ 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']:
                     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
+                        download.to_csv(os.path.join(cache_folder, f'{exchange},'
+                                                                   f'{True},'
+                                                                   f'{symbol},'
+                                                                   f'{int(j[0])},'
+                                                                   f'{int(j[1]) + resolution},'  # This adds resolution 
+                                                                                                 # back to the exported
+                                                                                                 # time series
                                                                    f'{resolution}.csv'),
                                         index=False)
 
+                prices_by_resolution = aggregate_prices_by_resolution(prices_by_resolution, symbol, resolution,
+                                                                      download)
                 # Write these into the data array
-                if asset not in list(final_prices.keys()):
-                    final_prices[asset] = download
+                if symbol not in final_prices:
+                    final_prices[symbol] = download
                 else:
-                    final_prices[asset] = pd.concat([final_prices[asset], download])
+                    final_prices[symbol] = pd.concat([final_prices[symbol], download])
 
             # After all the negative ranges are appended, we need to sort & trim
-            final_prices[asset] = final_prices[asset].sort_values(by=['time'], ignore_index=True)
+            final_prices[symbol] = final_prices[symbol].sort_values(by=['time'], ignore_index=True)
 
             # Now make sure to just trim our times to hit the start and end times
-            final_prices[asset] = final_prices[asset][final_prices[asset]['time'] >= start_time]
-            final_prices[asset] = final_prices[asset][final_prices[asset]['time'] <= end_time + resolution]  # Add back
+            final_prices[symbol] = final_prices[symbol][final_prices[symbol]['time'] >= start_time]
+            final_prices[symbol] = final_prices[symbol][
+                final_prices[symbol]['time'] <= end_time + resolution]  # Add back
+
+        # Now add any custom prices
+        for price_reader in self.__price_readers:
+            data = price_reader.data
+            for symbol in data:
+                symbol_info = price_reader.prices_info[symbol]
+                resolution = symbol_info['resolution']
+                start_time = symbol_info['start_time']
+                stop_time = symbol_info['stop_time']
+
+                # Add each symbol to the final prices without doing any processing
+                if symbol in final_prices:
+                    final_prices[symbol] = pd.concat([final_prices[symbol], data[symbol]])
+                else:
+                    final_prices[symbol] = data[symbol]
+
+                prices_by_resolution = aggregate_prices_by_resolution(prices_by_resolution, symbol, resolution,
+                                                                      data[symbol])
+
+                self.__check_user_time_bounds(start_time,
+                                              stop_time,
+                                              resolution)
+
+        # Send the prices by resolution to the interface
+        self.interface.receive_price_cache(sort_prices_by_resolution(prices_by_resolution))
+
+        # Finally, convert back into records
+        for symbol in final_prices:
+            final_prices[symbol] = final_prices[symbol].to_records()
 
         return final_prices
 
-        # cache_folder = self.preferences['settings']["cache_location"]
-        # # Make sure the cache folder exists and read files
-        # try:
-        #     files = os.listdir(cache_folder)
-        # except FileNotFoundError:
-        #     files = []
-        #     os.mkdir(cache_folder)
-        #
-        # available_files = []
-        # for i in range(len(files)):
-        #     # example file name: 'BTC-USD.1622400000.1622510793.60.csv'
-        #     # Remove the .csv from each of the files: BTC-USD.1622400000.1622510793.60
-        #     identifier = files[i][:-4].split(",")
-        #     identifier[1] = float(identifier[1])
-        #     identifier[2] = float(identifier[2])
-        #     identifier[3] = float(identifier[3])
-        #     available_files.append(identifier)
-        #
-        # assets = self.preferences['price_data']['assets']  # type: dict
-        # for i in assets:
-        #     identifier = [i[0], i[1], i[2], i[3]]
-        #     string_identifier = to_string_key(identifier)
-        #     if identifier in available_files:
-        #         # Read the csv here
-        #         if tuple(identifier) not in self.price_dictionary.keys():
-        #             print("Including: " + string_identifier + ".csv in backtest.")
-        #             self.price_dictionary[tuple(identifier)] = pd.read_csv(os.path.join(cache_folder,
-        #                                                                                 string_identifier + ".csv")
-        #                                                                    )
-        #     else:
-        #         if tuple(identifier) not in self.price_dictionary.keys():
-        #             print("No exact cache exists for " + str(identifier[0]) + " from " + str(identifier[1]) + " to " +
-        #                   str(identifier[2]) + " at " + str(identifier[3]) + "s resolution. Downloading...")
-        #             download = self.interface.get_product_history(identifier[0], identifier[1], identifier[2],
-        #                                                           identifier[3])
-        #             self.price_dictionary[tuple(identifier)] = download
-        #             if save:
-        #                 download.to_csv(os.path.join(cache_folder, string_identifier + ".csv"), index=False)
-        #
-        # # Merge all the same asset ids into the same dictionary spots
-        # unique_assets = {}
-        # for k, v in self.price_dictionary.items():
-        #     if k[0] in unique_assets:
-        #         unique_assets[k[0]] = pd.concat([unique_assets[k[0]], v], ignore_index=True)
-        #     else:
-        #         unique_assets[k[0]] = v
-        #
-        # return unique_assets
-
-    def add_prices(self, asset_id, start_time, end_time, resolution, save=False):
-        # Create its unique identifier
-        identifier = [asset_id, int(start_time), int(end_time), int(resolution)]
+    def add_prices(self,
+                   symbol: str,
+                   resolution: [str, int, float],
+                   to: str = None,
+                   start_date: typing.Union[str, float, int] = None,
+                   stop_date: typing.Union[str, float, int] = None):
+        """
+        This is the user facing function for adding prices to the engine
+        """
+        start = None
+        end = None
 
-        # If it's not loaded then write it to the file
-        if tuple(identifier) not in self.price_dictionary.keys():
-            # self.preferences['price_data']['assets'].append(identifier)
-            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
+        resolution = int(time_interval_to_seconds(resolution))
 
-            if self.user_stop is None:
-                self.user_stop = end_time
+        # Even if they specified start/end unevenly it will be overwritten with any to argument
+        if to is not None:
+            start = time.time() - time_interval_to_seconds(to)
+            end = time.time()
+
+        if start_date is not None:
+            if isinstance(stop_date, (int, float)):
+                start = start_date
             else:
-                if end_time > self.user_stop:
-                    self.user_stop = end_time
+                start_date = pd.to_datetime(start_date)
+                epoch = dt.utcfromtimestamp(0)
+                start = (start_date - epoch).total_seconds()
 
-            # Now keep track of the smallest price event added
-            if self.min_resolution is None:
-                self.min_resolution = resolution
+        if stop_date is not None:
+            if isinstance(stop_date, (int, float)):
+                end = stop_date
             else:
-                if resolution < self.min_resolution:
-                    self.min_resolution = resolution
+                end_date = pd.to_datetime(stop_date)
+                epoch = dt.utcfromtimestamp(0)
+                end = (end_date - epoch).total_seconds()
+
+        # If start/ends are specified unevenly
+        if (start_date is None and stop_date is not None) or (start_date is not None and stop_date is None):
+            raise ValueError("Both start and end dates must be set or use the 'to' argument.")
+
+        self.__add_prices(symbol, start, end, resolution)
+
+    def add_custom_prices(self, price_reader: PriceReader):
+        self.__price_readers.append(price_reader)
+
+    def add_custom_events(self, event_reader: EventReader):
+        self.__event_readers.append(event_reader)
+
+    def add_tick_events(self, tick_reader: TickReader):
+        self.__tick_readers.append(tick_reader)
+
+    def __add_prices(self, symbol, start_time, end_time, resolution, save=False):
+        # If it's not loaded then write it to the file
+        # Add it as a new price
+        self.__user_added_times.append({
+            self.PriceIdentifiers.sandbox: True,
+            self.PriceIdentifiers.symbol: symbol,
+            self.PriceIdentifiers.resolution: resolution,
+            self.PriceIdentifiers.epoch_start: start_time,
+            self.PriceIdentifiers.epoch_stop: end_time
+        })
+        if save:
+            self.queue_backtest_write = True
+
+        self.__check_user_time_bounds(start_time, end_time, resolution)
+
+    def __check_user_time_bounds(self, start_time, end_time, resolution):
+        # 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:
-            print("already identified")
+            if start_time < self.user_start:
+                self.user_start = start_time
 
-    def append_backtest_price_event(self, callback: typing.Callable, asset_id, time_interval, state_object, ohlc, init,
-                                    teardown):
-        if isinstance(time_interval, str):
-            time_interval = time_interval_to_seconds(time_interval)
-        self.price_events.append([callback, asset_id, time_interval, state_object, ohlc, init, teardown])
+        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
 
     def write_setting(self, key, value, save=False):
         """
@@ -477,24 +599,30 @@ def write_setting(self, key, value, save=False):
         if save:
             self.queue_backtest_write = True
 
-    def write_initial_price_values(self, account_dictionary):
+    def __write_initial_price_values(self, account_dictionary):
         """
         Write in a new price dictionary for the paper trade exchange.
         """
         self.interface.override_local_account(account_dictionary)
 
-    def format_account_data(self, local_time) -> typing.Tuple[typing.Dict[typing.Union[str, typing.Any],
-                                                                          typing.Union[int, typing.Any]],
-                                                              typing.Dict[typing.Union[str, typing.Any],
-                                                                          typing.Union[int, typing.Any]]]:
+    def format_account_data(self, interface: PaperTradeInterface, local_time) -> \
+            typing.Tuple[typing.Dict[
+                             typing.Union[
+                                 str, typing.Any],
+                             typing.Union[
+                                 int, typing.Any]],
+                         typing.Dict[
+                             typing.Union[str,
+                                          typing.Any],
+                             typing.Union[
+                                 int, typing.Any]]]:
 
         # This is done so that only traded assets are evaluated.
         true_available = {}
-        assets = self.__traded_assets
         true_account = {}
-        for i in assets:
+        for i in interface.traded_assets:
             # Grab the account status
-            true_account[i] = self.interface.get_account(i)
+            true_account[i] = interface.get_account(i)
 
         # Create an account total value
         value_total = 0
@@ -517,13 +645,13 @@ def format_account_data(self, local_time) -> typing.Tuple[typing.Dict[typing.Uni
             currency_pair = i
 
             # Convert to quote (this could be optimized a bit)
-            if self.__exchange_type != 'alpaca':
+            if interface.get_exchange_type() != 'alpaca':
                 currency_pair += '-'
                 currency_pair += self.quote_currency
 
             # Get price at time
             try:
-                price = self.interface.get_price(currency_pair)
+                price = interface.get_price(currency_pair)
             except KeyError:
                 # Must be a currency we have no data for
                 price = 0
@@ -537,8 +665,8 @@ def format_account_data(self, local_time) -> typing.Tuple[typing.Dict[typing.Uni
         value_total += quote_value
         true_available[self.quote_currency] = quote_value
 
-        no_trade_value += self.initial_account[self.quote_currency]['available'] + \
-                          self.initial_account[self.quote_currency]['hold']
+        no_trade_value += self.initial_account[self.quote_currency][
+                              'available'] + self.initial_account[self.quote_currency]['hold']
 
         true_available['Account Value (' + self.quote_currency + ')'] = value_total
 
@@ -550,8 +678,7 @@ def __account_was_used(self, column) -> bool:
         show_zero_delta = self.preferences['settings']['show_tickers_with_zero_delta']
 
         # 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
+        is_used = column in self.interface.traded_assets or 'Account Value (' + self.quote_currency + ')' == column
 
         # Return true if they are not the same or the setting is set to true
         output = is_used or show_zero_delta
@@ -565,13 +692,134 @@ def __next_color(self):
             self.__color_generator = Category10_10.__iter__()
             return next(self.__color_generator)
 
-    def run(self) -> BacktestResult:
+    def advance_time_and_price_index(self):
+        def handle_blankly_tick(type_: str, data):
+            if type_ == 'tick':
+                self.model.websocket_update(data)
+
+        def run_events():
+            # Ensure that we don't index error here
+            events_length = len(self.events)
+            # Make sure we don't crash at first
+            if self.event_index >= events_length:
+                return
+
+            # Store the time because we need accurate time for the async stuff
+            time_backup = self.time
+            while self.events[self.event_index]['time'] < time_backup:
+                # Set time to something different here
+                event = self.events[self.event_index]
+                self.time = event['time']
+                if event['type'][0:11] != '__blankly__':
+                    self.model.event(event['type'], event['data'])
+                else:
+                    handle_blankly_tick(event['type'][11:], event['data'])
+                # Fired some event, go to the next one
+                self.event_index += 1
+
+                # Just check after doing that if we went outside the index
+                if self.event_index >= events_length:
+                    return
+
+            self.time = time_backup
+
+        # Now update the time to match
+        self.interface.receive_time(self.time)
+
+        for symbol in self.prices:
+            # Make sure that each price column is at least at the current time
+
+            # This just incrementing the price indexes until it's less than time and ensuring that
+            #  it's less than the length of the price
+            price_length = len(self.prices[symbol]) - 2
+            while self.prices[symbol][self.price_indexes[symbol]]['time'] < self.time:
+                if price_length >= self.price_indexes[symbol]:
+                    self.price_indexes[symbol] += 1
+                else:
+                    self.model.has_data = False
+                    break
+
+            # Write this new price into the interface
+            self.interface.receive_price(symbol, new_price=self.prices[symbol][
+                self.price_indexes[symbol]][self.use_price])
+
+        # Check has_data here also
+        if self.time > self.user_stop:
+            self.model.has_data = False
+
+        run_events()
+
+    def sleep(self, seconds: [int, float]):
+        # Always evaluate limits
+        self.interface.evaluate_limits()
+        self.sleep_count += 1
+
+        if self.show_progress:
+            if self.sleep_count % 300 == 0:
+                # Update the progress occasionally
+                update_progress((self.time - self.user_start) / (self.user_stop - self.user_start))
+
+        # Advance the time
+        self.time += seconds
+        # Refresh all the prices and times
+        self.advance_time_and_price_index()
+
+    def value_account(self) -> None:
+        """
+        Store the valuation for the account
+
+        This is accessible by the user
+        """
+        # Don't do anything when live
+        if not self.backtesting:
+            return
+
+        available_dict, no_trade_dict = self.format_account_data(self.interface, self.time)
+
+        self.traded_account_values.append(available_dict)
+        self.no_trade_account_values.append(no_trade_dict)
+
+    # TODO this class should be constructed with a BacktestConfiguration object
+    def run(self,
+            args,
+            exchange: ABCExchange,
+            initial_account_values,
+            backtest_settings_path: str = None,
+            **kwargs) -> BacktestResult:
         """
         Setup
         """
+        self.backtesting = True
+        self.preferences = load_backtest_preferences(backtest_settings_path)
+        # Write any dynamic arguments back into the backtest preferences
+        for setting in kwargs:
+            self.preferences['settings'][setting] = kwargs[setting]
 
-        # Create this initial so that we can compare how our strategy performs
-        self.initial_account = self.interface.get_account()
+        self.backtest_settings_path = backtest_settings_path
+        self.show_progress = self.preferences['settings']['show_progress_during_backtest']
+
+        if not exchange.get_type() == "paper_trade":
+            raise ValueError("Backtest controller was not constructed with a paper trade exchange object.")
+        # Define the interface on run
+        self.interface: PaperTradeInterface = exchange.get_interface()
+        # This is where we begin logging the backtest time
+        start_clock = time.time()
+
+        # Figure out our traded assets here
+        self.prices = self.sync_prices()
+        # Now ensure all events are processed
+        self.parse_events()
+        for i in self.prices:
+            base = get_base_asset(i)
+            quote = get_quote_asset(i)
+            if base not in self.interface.traded_assets:
+                self.interface.traded_assets.append(base)
+            if quote not in self.interface.traded_assets:
+                self.interface.traded_assets.append(quote)
+
+        # Write them in
+        if initial_account_values is not None:
+            self.__write_initial_price_values(initial_account_values)
 
         # Write our queued edits to the file
         if self.queue_backtest_write:
@@ -586,93 +834,66 @@ def run(self) -> BacktestResult:
 
         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)
+            self.add_prices(benchmark_symbol, start_date=self.user_start, stop_date=self.user_stop,
+                            resolution=self.min_resolution)
 
-        prices = self.sync_prices()
-
-        # Organize each price into this structure: [epoch, "BTC-USD", price, open, high, low, close, volume]
         use_price = self.preferences['settings']['use_price']
         self.use_price = use_price
 
-        self.pd_prices = {**prices}
-
-        for k, v in prices.items():
-            frame = v  # type: pd.DataFrame
+        for frame_symbol, price_list in self.prices.items():
+            # This is a list of dictionaries
+            frame = price_list  # type: list
 
             # Be sure to push these initial prices to the strategy
             try:
-                self.interface.receive_price(k, v[use_price].iloc[0])
+                self.interface.receive_price(frame_symbol, frame[0][use_price])
             except IndexError:
                 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:
+                        # This handles the weird types
+                        if len(price_dict[j]) == 0:
+                            return False
                         if not price_dict[j].empty:
                             return True
                     return False
 
-                if not check_if_any_column_has_prices(prices):
+                if not check_if_any_column_has_prices(self.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 "
+                    raise IndexError(f"Data for symbol {frame_symbol} is empty. Are you using a symbol that "
+                                     f"is incompatible "
                                      f"with this exchange?")
 
             # Be sure to send in the initial time
-            self.interface.receive_time(v['time'].iloc[0])
-
-            for index, row in frame.iterrows():
-                # TODO iterrows() is allegedly pretty slow
-                self.prices.append([row.time, k, row[use_price],
-                                    row['open'],  # (index) 3
-                                    row['high'],  # 4
-                                    row['low'],  # 5
-                                    row['close'],  # 6
-                                    row['volume']])  # 7
-
-        # 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. "
+            first_time = price_list['time'][0]
+            self.interface.receive_time(first_time)
+            self.price_indexes[frame_symbol] = 0
+
+            # Find the first time in the list
+            self.initial_time = copy.copy(self.user_start)
+
+        if self.prices == {} and self.events == []:
+            raise ValueError("No data 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
-
-        # Add a section to the price events which controls the next time they run & change to array of dicts
-        for i in range(len(self.price_events)):
-            self.price_events[i] = {
-                'function': self.price_events[i][0],
-                'asset_id': self.price_events[i][1],
-                'interval': self.price_events[i][2],
-                'state_object': self.price_events[i][3],
-                'next_run': self.initial_time,
-                'ohlc': self.price_events[i][4],
-                'init': self.price_events[i][5],
-                'teardown': self.price_events[i][6]
-            }
+        """
+        Begin backtesting
+        """
+        # Create this initial so that we can compare how our strategy performs
+        self.initial_account = self.interface.get_account()
 
-        # Initialize this before the callbacks so it works in the initialization functions
-        self.time = self.initial_time
+        # Initialize this before the callbacks, so it works in the initialization functions
+        self.time = copy.copy(self.user_start)
 
         # Turn on backtesting immediately after setting the time
         self.interface.set_backtesting(True)
 
-        # Run the initialization functions for the price events
-        print("\nInitializing...")
-        for i in self.price_events:
-            if i['init'] is not None:
-                i['init'](i['asset_id'], i['state_object'])
-
-        """
-        Begin backtesting
-        """
-
         # Re-evaluate the traded assets account
         # This is mainly used if the user has an account with some value that gets added in at the backtest point
         # This occurs after initialization so there has to be a function to test & re-evaluate that
@@ -683,216 +904,41 @@ def check_if_any_column_has_prices(price_dict: dict) -> bool:
         if self.quote_currency not in column_keys:
             column_keys.append(self.quote_currency)
         # If they start a price event on something they don't own, this should also be included
-        for i in self.price_events:
-            base_asset = get_base_asset(i['asset_id'])
-            quote_asset = get_quote_asset(i['asset_id'])
-            if base_asset not in column_keys:
-                column_keys.append(base_asset)
-            if quote_asset not in column_keys:
-                column_keys.append(quote_asset)
-        self.__traded_assets = list.copy(column_keys)
         column_keys.append('time')
 
-        # column_keys = ['time']
-        # for i in account.keys():
-        #     column_keys.append(i['currency'])
-
         cycle_status = pd.DataFrame(columns=column_keys)
 
         no_trade_cycle_status = pd.DataFrame(columns=column_keys)
 
-        # Append dictionaries to this to make the pandas dataframe
-        price_data = []
-
-        # Append dictionaries to this to make the no trade dataframe
-        no_trade = []
-
         # Add an initial account row here
         if self.preferences['settings']['save_initial_account_value']:
-            available_dict, no_trade_dict = self.format_account_data(self.initial_time)
-            price_data.append(available_dict)
-            no_trade.append(no_trade_dict)
-
-        show_progress = self.preferences['settings']['show_progress_during_backtest']
-
-        ignore_exceptions = self.preferences['settings']['ignore_user_exceptions']
+            available_dict, no_trade_dict = self.format_account_data(self.interface, self.user_start)
+            self.traded_account_values.append(available_dict)
+            self.no_trade_account_values.append(no_trade_dict)
 
         print("\nBacktesting...")
-        price_number = len(self.prices)
-
-        # This dictionary will hold price arrays below sorted by symbol which will allow us to grab most
-        #  recent ohlcv data when necessary
-        ohlcv_tracker = {}
 
+        # Start the model here
         try:
-            for i in range(price_number):
-                #                 row.time,      k,  use_price, 'open', 'high', 'low','close','volume'
-                # Formatted like [1609146000.0, 'AAPL', 132.99, 132.72, 133.0, 132.6, 132.99, 32603.0]
-                price_array = self.prices[i]
-                if show_progress:
-                    if i % 100 == 0:
-                        update_progress(i / price_number)
-                self.interface.receive_price(asset_id=price_array[1], new_price=price_array[2])
-                self.current_time = price_array[0]
-                self.interface.evaluate_limits()
-
-                # This will keep the most recent price event data organized by symbol
-                ohlcv_tracker[price_array[1]] = price_array
-
-                while True:
-                    # Need to go through and establish an order for each of the price events
-                    self.price_events = sorted(self.price_events, key=lambda sort_key: sort_key['next_run'])
-
-                    # Now the lowest one has to go past the current time to be invalid
-                    if self.price_events[0]['next_run'] > self.current_time:
-                        break
-
-                    local_time = self.price_events[0]['next_run']
-
-                    # Export the time for strategy
-                    self.time = local_time
-
-                    self.interface.receive_time(local_time)
-
-                    # This is the actual callback to the user space
-                    try:
-                        if self.price_events[0]['ohlc']:
-                            # This pulls all the price data out of the price array defined on line 260
-                            ohlcv_array = ohlcv_tracker[self.price_events[0]['asset_id']]
-                            self.price_events[0]['function']({'open': ohlcv_array[3],
-                                                              'high': ohlcv_array[4],
-                                                              'low': ohlcv_array[5],
-                                                              'close': ohlcv_array[6],
-                                                              'volume': ohlcv_array[7]},
-
-                                                             self.price_events[0]['asset_id'],
-                                                             self.price_events[0]['state_object'])
-                        else:
-                            self.price_events[0]['function'](self.interface.get_price(self.price_events[0]['asset_id']),
-                                                             self.price_events[0]['asset_id'], self.price_events[0][
-                                                                 'state_object'])
-                    except Exception as e:
-                        if ignore_exceptions:
-                            traceback.print_exc()
-                        else:
-                            raise e
-
-                    # Delay the next run until after the interval
-                    self.price_events[0]['next_run'] += self.price_events[0]['interval']
-
-                    available_dict, no_trade_dict = self.format_account_data(local_time)
-
-                    price_data.append(available_dict)
-
-                    no_trade.append(no_trade_dict)
-
-            # Finish filling the progress bar
-            if show_progress:
+            self.model.main(args)
+            if self.show_progress:
+                # If it finishes give it 100%
                 update_progress(1)
-
-            # Finally, run the teardown functions
-            for i in self.price_events:
-                # Pull the teardown and pass the state object
-                if callable(i['teardown']):
-                    i['teardown'](i['state_object'])
         except Exception:
             traceback.print_exc()
 
-        # Reset time to be None to indicate we're no longer in a backtest
+        # Reset time to indicate we are no longer in a backtest
         self.time = None
 
         # Push the accounts to the dataframe
-        cycle_status = pd.concat([cycle_status, pd.DataFrame(price_data)], ignore_index=True).sort_values(by=['time'])
+        cycle_status = pd.concat([cycle_status, pd.DataFrame(self.traded_account_values)],
+                                 ignore_index=True).sort_values(by=['time'])
 
         if len(cycle_status) == 0:
             raise RuntimeError("Empty result - no valid backtesting events occurred. Was there an error?.")
 
-        no_trade_cycle_status = pd.concat([no_trade_cycle_status, pd.DataFrame(no_trade)], ignore_index=True)\
-            .sort_values(by=['time'])
-
-        figures = []
-        # for i in self.prices:
-        #     result_index = cycle_status['time'].sub(i[0]).abs().idxmin()
-        #     for i in cycle_status.iloc[result_index]:
-
-        hover = HoverTool(
-            tooltips=[
-                ('value', '@value')
-            ],
-
-            # formatters={
-            #     'time': 'datetime',  # use 'datetime' formatter for 'date' field
-            #     '@{value}': 'printf',   # use 'printf' formatter for '@{adj close}' field
-            # },
-
-            # display a tooltip whenever the cursor is vertically in line with a glyph
-            mode='vline'
-        )
-        
-        # 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
-
-            time = [dt.fromtimestamp(ts) for ts in cycle_status['time']]
-
-            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')
-                    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
-                    p.legend.location = "top_left"
-                    p.legend.title = column
-                    p.legend.title_text_font_style = "bold"
-                    p.legend.title_text_font_size = "20px"
-                    if global_x_range is None:
-                        global_x_range = p.x_range
-                    else:
-                        p.x_range = global_x_range
-
-                    figures.append(p)
-
-            show(bokeh_columns(figures))
+        no_trade_cycle_status = pd.concat([no_trade_cycle_status, pd.DataFrame(self.no_trade_account_values)],
+                                          ignore_index=True).sort_values(by=['time'])
 
         def is_number(s):
             try:
@@ -902,7 +948,7 @@ def is_number(s):
             except ValueError:
                 return False
 
-        history_and_returns = {
+        history_and_returns: dict = {
             'history': cycle_status
         }
         metrics_indicators = {}
@@ -913,7 +959,7 @@ def is_number(s):
             '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, figures)
+        }, self.prices, self.initial_time, self.interface.time(), self.quote_currency, [])
 
         # If they set resampling we use resampling for everything
         resample_setting = self.preferences['settings']['resample_account_value_for_metrics']
@@ -973,7 +1019,7 @@ def attempt(math_callable: typing.Callable, dict_of_dataframes: dict, kwargs: di
                                                    {'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)
@@ -988,26 +1034,19 @@ def attempt(math_callable: typing.Callable, dict_of_dataframes: dict, kwargs: di
                                                                        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()
-            
+            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, 
+            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(history_and_returns, metrics_indicators)
-
         # Remove NaN values here
-        history_and_returns['resampled_account_value'] = history_and_returns['resampled_account_value'].\
+        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
@@ -1022,6 +1061,147 @@ def attempt(math_callable: typing.Callable, dict_of_dataframes: dict, kwargs: di
         result_object.history_and_returns = history_and_returns
         result_object.metrics = metrics_indicators
         result_object.user_callbacks = user_callbacks
+        result_object.exchange = self.interface.get_exchange_type()
+
+        figures = []
+        # This modifies the platform result in place
+        platform_result = format_platform_result(result_object)
+        if self.preferences['settings']['GUI_output']:
+            def internal_backtest_viewer():
+                # for i in self.prices:
+                #     result_index = cycle_status['time'].sub(i[0]).abs().idxmin()
+                #     for i in cycle_status.iloc[result_index]:
+
+                hover = HoverTool(
+                    tooltips=[
+                        ('value', '@value')
+                    ],
+
+                    # formatters={
+                    #     'time': 'datetime',  # use 'datetime' formatter for 'date' field
+                    #     '@{value}': 'printf',   # use 'printf' formatter for '@{adj close}' field
+                    # },
+
+                    # display a tooltip whenever the cursor is vertically in line with a glyph
+                    mode='vline'
+                )
+
+                # 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")
+
+                global_x_range = None
+
+                time_ = [dt.fromtimestamp(ts) for ts in cycle_status['time']]
+
+                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')
+                        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]
+                                benchmark_series = pd.Series(self.prices[benchmark_symbol][use_price])
+                                initial_benchmark_value = benchmark_series.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 = benchmark_series.multiply(multiplier)
+                                normalized_compare_time_series = self.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
+                        p.legend.location = "top_left"
+                        p.legend.title = column
+                        p.legend.title_text_font_style = "bold"
+                        p.legend.title_text_font_size = "20px"
+                        if global_x_range is None:
+                            global_x_range = p.x_range
+                        else:
+                            p.x_range = global_x_range
+
+                        figures.append(p)
+
+                show(bokeh_columns(figures))
+                info_print(f'Make an account to take advantage of the platform backtest viewer: '
+                           f'https://app.blankly.finance/RETIe0J8EPSQz7wizoJX0OAFb8y1/EZkgTZMLJVaZK6kNy0mv/'
+                           f'2b2ff92c-ee41-42b3-9afb-387de9e4f894/backtest')
+
+            # This is where we end the backtesting time
+            stop_clock = time.time()
+
+            try:
+                json_file = json.loads(open('./blankly.json').read())
+                api_key = json_file['api_key']
+                api_pass = json_file['api_pass']
+                # Need this to generate the URL
+                # Need this to know where to post to
+                model_id = json_file['model_id']
+
+                requests.post(f'https://events.blankly.finance/v1/backtest/result', json=platform_result, headers={
+                    'api_key': api_key,
+                    'api_pass': api_pass,
+                    'model_id': model_id
+                })
+
+                requests.post(f'https://events.blankly.finance/v1/backtest/status', json={
+                    'successful': True,
+                    'status_summary': 'Completed',
+                    'status_details': '',
+                    'time_elapsed': stop_clock-start_clock,
+                    'backtest_id': platform_result['backtest_id']
+                }, headers={
+                    'api_key': api_key,
+                    'api_pass': api_pass,
+                    'model_id': model_id
+                })
+
+                import webbrowser
+
+                link = f'https://app.blankly.finance/{api_key}/{model_id}/{platform_result["backtest_id"]}' \
+                       f'/backtest'
+                webbrowser.open(
+                    link
+                )
+                info_print(f'View your backtest here: {link}')
+            except (FileNotFoundError, KeyError):
+                internal_backtest_viewer()
+
+        # Finally, write the figures in
+        result_object.figures = figures
 
         self.interface.set_backtesting(False)
+        self.backtesting = False
+
+        # Export to the platform here
+        blankly.reporter.export_backtest_result(platform_result)
+
         return result_object
diff --git a/blankly/exchanges/interfaces/paper_trade/backtest_result.py b/blankly/exchanges/interfaces/paper_trade/backtest_result.py
index 064d0da8..0a5198d9 100644
--- a/blankly/exchanges/interfaces/paper_trade/backtest_result.py
+++ b/blankly/exchanges/interfaces/paper_trade/backtest_result.py
@@ -22,16 +22,16 @@
 
 class BacktestResult:
     def __init__(self, history_and_returns: dict, trades: dict, history: dict,
-                 start_time: float, stop_time: float, quote_currency: str, price_events: list, figures: list):
+                 start_time: float, stop_time: float, quote_currency: str, figures: 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.exchange = 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
@@ -79,11 +79,14 @@ def search(arr, size, x):
                         # 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
+                    if len(arr) > 1:
+                        if arr[search_index] <= x <= arr[search_index + 1]:
+                            # Found it in this range
+                            return search_index
+                        else:
+                            search_index += 1
                     else:
-                        search_index += 1
+                        return 0
             try:
                 # Iterate and find the correct quote price
                 index_ = search(times, len(times), epoch)
@@ -108,15 +111,18 @@ def search(arr, size, x):
         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
+        try:
+            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
+        except TypeError:
+            raise TypeError("No valid account data found, make sure to create valid account value datapoints.")
 
         # Turn that resample into a dataframe
         return DataFrame(resampled_array, columns=['time', 'value'])
@@ -152,9 +158,11 @@ def __str__(self):
 
         return_string += "Blankly Metrics: \n"
         for i in self.metrics.keys():
-            spaces_needed = 33 - len(i)
-            user_metrics_line = i + ": " + (' ' * spaces_needed) + str(self.metrics[i])
-            if i[-3:] == "(%)":
+            display_name = self.metrics[i]['display_name']
+            value = self.metrics[i]['value']
+            spaces_needed = 33 - len(display_name)
+            user_metrics_line = display_name + ": " + (' ' * spaces_needed) + str(value)
+            if display_name[-3:] == "(%)":
                 user_metrics_line += "%"
             user_metrics_line += "\n"
             return_string += user_metrics_line
diff --git a/blankly/exchanges/interfaces/paper_trade/backtesting_wrapper.py b/blankly/exchanges/interfaces/paper_trade/backtesting_wrapper.py
index 94f418e6..9059f99d 100644
--- a/blankly/exchanges/interfaces/paper_trade/backtesting_wrapper.py
+++ b/blankly/exchanges/interfaces/paper_trade/backtesting_wrapper.py
@@ -26,6 +26,8 @@ def __init__(self):
             'time': 0
         }
 
+        self.full_prices = {}
+
     def set_backtesting(self, status: bool):
         self.backtesting = status
 
@@ -35,6 +37,9 @@ def receive_time(self, new_time):
     def receive_price(self, asset_id, new_price):
         self.frame['prices'][asset_id] = new_price
 
+    def receive_price_cache(self, prices: dict):
+        self.full_prices = prices
+
     """
     Override functions for manipulating backtesting
     """
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/config/settings.json b/tests/config/settings.json
index d964f5f3..18d75567 100644
--- a/tests/config/settings.json
+++ b/tests/config/settings.json
@@ -10,16 +10,26 @@
     "kucoin": {
       "cash": "USDT"
     },
+    "okex": {
+      "cash": "USDT"
+    },
     "binance": {
       "cash": "USDT",
       "binance_tld": "us"
     },
+    "ftx": {
+      "ftx_tld": "us"
+    },
     "alpaca": {
       "websocket_stream": "iex",
       "cash": "USD"
     },
     "oanda": {
       "cash": "USD"
+    },
+    "ftx_futures": {
+      "cash": "USD",
+      "ftx_tld": "com"
     }
   }
 }
\ No newline at end of file
diff --git a/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py b/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py
index 0bb22219..e6d6b10a 100644
--- a/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py
+++ b/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py
@@ -251,6 +251,6 @@ def test_get_cash(alpaca_mock_interface) -> None:
 
 
 def test_get_fees(alpaca_mock_interface) -> None:
-    fee_response = alpaca_mock_interface.get_fees()
+    fee_response = alpaca_mock_interface.get_fees('AAPL')
     assert fee_response['maker_fee_rate'] == 0
     assert fee_response['taker_fee_rate'] == 0
diff --git a/tests/exchanges/interfaces/oanda/test_oanda_interface.py b/tests/exchanges/interfaces/oanda/test_oanda_interface.py
index fd3927d0..9315484f 100644
--- a/tests/exchanges/interfaces/oanda/test_oanda_interface.py
+++ b/tests/exchanges/interfaces/oanda/test_oanda_interface.py
@@ -24,6 +24,7 @@
 import blankly
 from blankly.exchanges.interfaces.oanda.oanda_interface import OandaInterface
 from tests.helpers.comparisons import validate_response
+from tests.testing_utils import forex_market_open
 
 
 @pytest.fixture
@@ -55,6 +56,9 @@ def test_get_cash(oanda_interface: OandaInterface) -> None:
 
 def test_marketorder_comprehensive(oanda_interface: OandaInterface) -> None:
     # query for the unique ID of EUR_USD
+    if not forex_market_open():
+        return
+
     products = oanda_interface.get_products()
 
     found_eur_usd = False
@@ -67,7 +71,7 @@ def test_marketorder_comprehensive(oanda_interface: OandaInterface) -> None:
     eur_usd = 'EUR-USD'
     market_buy_order = oanda_interface.market_order(eur_usd, 'buy', 200)
 
-    time.sleep(1)
+    time.sleep(2)
     resp = oanda_interface.get_order(eur_usd, market_buy_order.get_id())
 
     # verify resp is correct
@@ -76,7 +80,7 @@ def test_marketorder_comprehensive(oanda_interface: OandaInterface) -> None:
     # Todo: validate market sell order object can query its info correctly
     oanda_interface.market_order(eur_usd, 'sell', 200)
 
-    time.sleep(1)
+    time.sleep(2)
     resp = oanda_interface.get_order(eur_usd, market_buy_order.get_id())
 
     # verify resp is correct
diff --git a/tests/exchanges/test_interface_homogeneity.py b/tests/exchanges/test_interface_homogeneity.py
index ab10ca0f..5498b7ee 100644
--- a/tests/exchanges/test_interface_homogeneity.py
+++ b/tests/exchanges/test_interface_homogeneity.py
@@ -15,6 +15,7 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
+
 import time
 import unittest
 from datetime import datetime as dt
@@ -27,8 +28,9 @@
 from blankly.exchanges.orders.limit_order import LimitOrder
 from blankly.exchanges.orders.market_order import MarketOrder
 from blankly.utils.time_builder import build_day
-from blankly.utils.utils import compare_dictionaries
-from tests.testing_utils import get_valid_symbol
+from blankly.utils.utils import compare_dictionaries, get_base_asset, get_quote_asset
+from tests.testing_utils import get_valid_symbol, forex_market_open
+from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface
 
 
 def compare_responses(response_list, force_exchange_specific=True):
@@ -61,9 +63,13 @@ class InterfaceHomogeneity(unittest.TestCase):
     Binance_Interface_data = None
     Kucoin_data = None
     Kucoin_Interface = None
+    Okx_data = None
+    Okx_Interface = None
+    Okx_Interface_Data = None
     Coinbase_Pro_Interface = None
     Kucoin = None
     Coinbase_Pro = None
+    Okx = None
     data_interfaces = None
     interfaces = None
     FTX = None
@@ -81,12 +87,26 @@ def setUpClass(cls) -> None:
         cls.interfaces.append(cls.Coinbase_Pro_Interface)
         cls.data_interfaces.append(cls.Coinbase_Pro_Interface)
 
+        # Okex definition and appending
+        cls.Okx = blankly.Okx(portfolio_name="okx sandbox portfolio",
+                              keys_path='./tests/config/keys.json',
+                              settings_path="./tests/config/settings.json")
+        cls.Okx_Interface = cls.Okx.get_interface()
+        cls.interfaces.append(cls.Okx_Interface)
+
+        cls.Okx_data = blankly.Okx(portfolio_name="okx data portfolio",
+                                   keys_path='./tests/config/keys.json',
+                                   settings_path="./tests/config/settings.json")
+        cls.Okx_Interface_data = cls.Okx_data.get_interface()
+        cls.data_interfaces.append(cls.Okx_Interface_data)
+
         # 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.data_interfaces.append(cls.Kucoin_Interface)
 
         cls.Kucoin_data = blankly.Kucoin(portfolio_name="KC Data Keys",
                                          keys_path='./tests/config/keys.json',
@@ -123,7 +143,7 @@ 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")
@@ -185,19 +205,31 @@ def test_get_account(self):
 
         self.assertTrue(compare_responses(responses, force_exchange_specific=False))
 
-    def check_market_order(self, order1: MarketOrder, side, size):
+    def check_market_order(self, order: MarketOrder, side, size):
         """
         Test if a market order passes these checks.
         Args:
-            order1 (dict): The market order to test - has to be type MarketOrder
+            order (dict): The market order to test - has to be type MarketOrder
             side (str): Market side (buy/sell)
             size (float): Amount of base currency used in purchase (pre-fees)
         """
-        self.assertEqual(order1.get_side(), side)
-        self.assertEqual(order1.get_size(), size)
-        self.assertEqual(order1.get_type(), 'market')
+        self.assertEqual(order.get_side(), side)
+        self.assertEqual(order.get_size(), size)
+        self.assertEqual(order.get_type(), 'market')
 
     def test_market_order(self):
+        def check_account_delta(before: dict, after: dict, order: MarketOrder) -> None:
+            # A market order should not have changed the funds on hold
+            self.assertAlmostEqual(before['hold'], after['hold'], places=1)
+
+            # The symbol should have gained less than the size on the buy if there were fees
+            # Before + requested size >= the filled size
+            before['available'] = int(float(before['available']))  # added this and line below
+            after['available'] = int(float(after['available']))
+
+            self.assertGreaterEqual(blankly.trunc(before['available'], 2) + order.get_size(),
+                                    blankly.trunc(after['available'], 2))
+
         # Make sure to buy back the funds we're loosing from fees - minimum balance of .1 bitcoin
         btc_account = self.Binance_Interface.get_account(symbol="BTC")['available']
         if btc_account < .1:
@@ -209,23 +241,38 @@ def test_market_order(self):
         status_responses = []
         # These are the immediate exchange responses that we aggregate & check
         exchange_responses = []
-        for i in self.interfaces:
 
+        for i in self.interfaces:
             type_ = i.get_exchange_type()
             if type_ == "ftx":
                 continue
-            if not (type_ == 'alpaca' or type_ == 'oanda'):
-                size = .01
-            else:
+            if type_ == 'oanda':
                 # Non fractional exchanges have to be sent here
                 size = 1
 
+                if not forex_market_open():
+                    continue
+            elif type_ == 'okx':
+                size = 1
+            else:
+                size = .01
+
+            # Grab the account values before
+            initial_value = i.get_account(get_base_asset(get_valid_symbol(type_)))
+
             order_responses.append({
                 'order': i.market_order(get_valid_symbol(type_), 'buy', size),
                 'side': 'buy',
                 'size': size
             })
 
+            # Give it half a second to execute and settle
+            time.sleep(.5)
+
+            # Grab them after
+            after_value = i.get_account(get_base_asset(get_valid_symbol(type_)))
+            check_account_delta(initial_value, after_value, order_responses[-1]['order'])
+
             order_responses.append({
                 'order': i.market_order(get_valid_symbol(type_), 'sell', size),
                 'side': 'sell',
@@ -259,7 +306,7 @@ def check_limit_order(self, limit_order: LimitOrder, expected_side: str, size, p
         # TODO fix status homogeneity
         # self.assertEqual(limit_order.get_status(), {'status': 'new'})
         self.assertEqual(limit_order.get_size(), size)
-        self.assertEqual(limit_order.get_asset_id(), product_id)
+        self.assertEqual(limit_order.get_symbol(), product_id)
 
     def test_limit_order(self):
         """
@@ -269,43 +316,66 @@ def test_limit_order(self):
         - Comparing with open orders
         - Canceling orders
         """
-        binance_limits = self.Binance_Interface.get_order_filter('BTC-USDT')["limit_order"]
+        limits = []
+        sorted_orders = {}
+
+        def evaluate_limit_order(interface: ABCExchangeInterface, symbol: str, buy_price: [float, int],
+                                 sell_price: [float, int], size: [float, int]):
+            def check_account_delta(before: dict, after: dict, order: LimitOrder) -> None:
+                # On a buy the quote asset should get moved to hold
 
-        binance_buy = self.Binance_Interface.limit_order('BTC-USDT', 'buy', int(binance_limits['min_price']+100), .01)
-        binance_sell = self.Binance_Interface.limit_order('BTC-USDT', 'sell', int(binance_limits['max_price']-100), .01)
-        self.check_limit_order(binance_sell, 'sell', .01, 'BTC-USDT')
-        self.check_limit_order(binance_buy, 'buy', .01, 'BTC-USDT')
-        time.sleep(3)
+                before['available'] = float(before['available'])  # added this and line below
+                after['available'] = float(after['available'])
 
-        coinbase_buy = self.Coinbase_Pro_Interface.limit_order('BTC-USD', 'buy', .01, 1)
-        self.check_limit_order(coinbase_buy, 'buy', 1, 'BTC-USD')
+                before['hold'] = float(before['hold'])  # added this and line below
+                after['hold'] = float(after['hold'])
 
-        coinbase_sell = self.Coinbase_Pro_Interface.limit_order('BTC-USD', 'sell', 100000, 1)
-        self.check_limit_order(coinbase_sell, 'sell', 1, 'BTC-USD')
+                self.assertAlmostEqual(before['available'], after['available'] + (order.get_price() * order.get_size()),
+                                       places=1)
 
-        kucoin_buy = self.Kucoin_Interface.limit_order('ETH-USDT', 'buy', .01, 1)
-        self.check_limit_order(kucoin_buy, 'buy', 1, 'ETH-USDT')
+                # The symbol should have gained less than the size on the buy if there were fees
+                # Before + requested size >= the filled size
+                self.assertAlmostEqual(before['hold'], after['hold'] - (order.get_price() * order.get_size()),
+                                       places=1)
 
-        kucoin_sell = self.Kucoin_Interface.limit_order('ETH-USDT', 'sell', 100000, 1)
-        self.check_limit_order(kucoin_sell, 'sell', 1, 'ETH-USDT')
+            initial_account = interface.get_account(get_quote_asset(symbol))
+            buy = interface.limit_order(symbol, 'buy', buy_price, size)
+            after_buy_account = interface.get_account(get_quote_asset(symbol))
+            # Buying power is always moving on alpaca, so it can't really be compared in this way
+            # need a larger range
+            if buy.exchange != 'alpaca':
+                check_account_delta(initial_account, after_buy_account, buy)
 
-        alpaca_buy = self.Alpaca_Interface.limit_order('AAPL', 'buy', 10, 1)
-        self.check_limit_order(alpaca_buy, 'buy', 1, 'AAPL')
+            sell = interface.limit_order(symbol, 'sell', sell_price, size)
+            self.check_limit_order(sell, 'sell', size, symbol)
+            self.check_limit_order(buy, 'buy', size, symbol)
 
-        alpaca_sell = self.Alpaca_Interface.limit_order('AAPL', 'sell', 100000, 1)
-        self.check_limit_order(alpaca_sell, 'sell', 1, 'AAPL')
+            if buy.exchange not in sorted_orders:
+                sorted_orders[buy.exchange] = {}
 
-        oanda_buy = self.Oanda_Interface.limit_order('EUR-USD', 'buy', .01, 1)
-        self.check_limit_order(oanda_buy, 'buy', 1, 'EUR-USD')
+            sorted_orders[buy.exchange] = {
+                'buy': buy,
+                'sell': sell
+            }
 
-        oanda_sell = self.Oanda_Interface.limit_order('EUR-USD', 'sell', 100000, 1)
-        self.check_limit_order(oanda_sell, 'sell', 1, 'EUR-USD')
+            return [buy, sell]
+
+        limits += evaluate_limit_order(self.Alpaca_Interface, 'AAPL', 10, 100000, 1)
+
+        binance_limits = self.Binance_Interface.get_order_filter('BTC-USDT')["limit_order"]
+        limits += evaluate_limit_order(self.Binance_Interface, 'BTC-USDT', int(binance_limits['min_price'] + 100),
+                                       int(binance_limits['max_price'] - 100), .01)
+
+        limits += evaluate_limit_order(self.Coinbase_Pro_Interface, 'BTC-USD', .01, 100000, 1)
+
+        limits += evaluate_limit_order(self.Kucoin_Interface, 'ETH-USDT', .01, 100000, 1)
+
+        limits += evaluate_limit_order(self.Oanda_Interface, 'EUR-USD', .01, 100000, 1)
+
+        limits += evaluate_limit_order(self.Okx_Interface, 'BTC-USDT', 0.50, 100000, .01)
 
-        limits = [binance_buy, binance_sell, coinbase_buy, coinbase_sell, kucoin_buy, kucoin_sell,
-                  alpaca_sell, alpaca_buy, oanda_buy, oanda_sell]
         responses = []
         status = []
-
         cancels = []
 
         open_orders = {
@@ -313,7 +383,8 @@ def test_limit_order(self):
             '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')
+            'oanda': self.Oanda_Interface.get_open_orders('EUR-USD'),
+            'okx': self.Okx_Interface.get_open_orders('BTC-USDT')
         }
 
         # Simple test to ensure that some degree of orders have been placed
@@ -323,20 +394,23 @@ 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['okx']
         all_orders = all_orders + open_orders['kucoin']
         all_orders = all_orders + open_orders['alpaca']
         all_orders = all_orders + open_orders['oanda']
 
         # Filter for limit orders
         open_orders = []
+
         for i in all_orders:
             if i['type'] == 'limit':
                 open_orders.append(i)
 
         self.assertTrue(compare_responses(open_orders))
+
         for i in limits:
             found = False
-            for j in open_orders:
+            for j in all_orders:
                 if i.get_id() == j['id']:
                     found = True
                     self.assertTrue(compare_dictionaries(i.get_response(), j))
@@ -350,40 +424,44 @@ def test_limit_order(self):
         self.assertTrue(compare_responses(responses))
         self.assertTrue(compare_responses(status))
 
-        cancels.append(self.Binance_Interface.cancel_order('BTC-USDT', binance_buy.get_id()))
-        cancels.append(self.Binance_Interface.cancel_order('BTC-USDT', binance_sell.get_id()))
+        cancels.append(self.Binance_Interface.cancel_order('BTC-USDT', sorted_orders['binance']['buy'].get_id()))
+        cancels.append(self.Binance_Interface.cancel_order('BTC-USDT', sorted_orders['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.Kucoin_Interface.cancel_order('ETH-USDT', sorted_orders['kucoin']['buy'].get_id()))
+        cancels.append(self.Kucoin_Interface.cancel_order('ETH-USDT', sorted_orders['kucoin']['sell'].get_id()))
+        cancels.append(self.Okx_Interface.cancel_order('BTC-USDT', sorted_orders['okx']['buy'].get_id()))
+        cancels.append(self.Okx_Interface.cancel_order('BTC-USDT', sorted_orders['okx']['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()))
+        cancels.append(self.Coinbase_Pro_Interface.cancel_order('BTC-USD',
+                                                                sorted_orders['coinbase_pro']['buy'].get_id()))
+        cancels.append(self.Coinbase_Pro_Interface.cancel_order('BTC-USD',
+                                                                sorted_orders['coinbase_pro']['sell'].get_id()))
 
-        cancels.append(self.Alpaca_Interface.cancel_order('AAPL', alpaca_buy.get_id()))
-        cancels.append(self.Alpaca_Interface.cancel_order('AAPL', alpaca_sell.get_id()))
+        cancels.append(self.Alpaca_Interface.cancel_order('AAPL', sorted_orders['alpaca']['buy'].get_id()))
+        cancels.append(self.Alpaca_Interface.cancel_order('AAPL', sorted_orders['alpaca']['sell'].get_id()))
 
-        cancels.append(self.Oanda_Interface.cancel_order('EUR-USD', oanda_buy.get_id()))
-        cancels.append(self.Oanda_Interface.cancel_order('EUR-USD', oanda_sell.get_id()))
+        cancels.append(self.Oanda_Interface.cancel_order('EUR-USD', sorted_orders['oanda']['buy'].get_id()))
+        cancels.append(self.Oanda_Interface.cancel_order('EUR-USD', sorted_orders['oanda']['sell'].get_id()))
 
         self.assertTrue(compare_responses(cancels, force_exchange_specific=False))
 
     def test_get_keys(self):
         responses = []
         for i in self.interfaces:
-            responses.append(i.get_fees())
+            responses.append(i.get_fees(get_valid_symbol(i.get_exchange_type())))
 
         self.assertTrue(compare_responses(responses, force_exchange_specific=False))
 
     def check_product_history_types(self, df: pd.DataFrame):
         # This is caused by casting on windows in pandas - I believe it is a bug
-        self.assertTrue(isinstance(df['time'][0], int) or
-                        isinstance(df['time'][0], numpy.int64) or
-                        isinstance(df['time'][0], numpy.int32))
-        self.assertTrue(isinstance(df['low'][0], float))
-        self.assertTrue(isinstance(df['high'][0], float))
-        self.assertTrue(isinstance(df['open'][0], float))
-        self.assertTrue(isinstance(df['close'][0], float))
-        self.assertTrue(isinstance(df['volume'][0], float))
+        self.assertTrue(isinstance(df['time'].iloc[0], int) or
+                        isinstance(df['time'].iloc[0], numpy.int64) or
+                        isinstance(df['time'].iloc[0], numpy.int32))
+        self.assertTrue(isinstance(df['low'].iloc[0], float))
+        self.assertTrue(isinstance(df['high'].iloc[0], float))
+        self.assertTrue(isinstance(df['open'].iloc[0], float))
+        self.assertTrue(isinstance(df['close'].iloc[0], float))
+        self.assertTrue(isinstance(df['volume'].iloc[0], float))
 
     def check_product_history_columns(self, df: pd.DataFrame):
         self.assertTrue(isinstance(df['time'], pd.Series))
@@ -392,31 +470,34 @@ def check_product_history_columns(self, df: pd.DataFrame):
         self.assertTrue(isinstance(df['open'], pd.Series))
         self.assertTrue(isinstance(df['close'], pd.Series))
         self.assertTrue(isinstance(df['volume'], pd.Series))
-    
+
     def test_single_point_history(self):
-        responses = []
         for i in self.data_interfaces:
+            print(f'Checking {i.get_exchange_type()}')
             valid_symbol = get_valid_symbol(i.get_exchange_type())
-            responses.append(i.history(valid_symbol, 1))
+            response = i.history(valid_symbol, 1)
 
-        for i in responses:
-            self.check_product_history_columns(i)
+            self.check_product_history_columns(response)
 
-            self.assertEqual(len(i), 1)
+            self.assertEqual(len(response), 1)
 
-            self.check_product_history_types(i)
+            self.check_product_history_types(response)
 
     def test_point_based_history(self):
-        responses = []
         for i in self.data_interfaces:
-            valid_symbol = get_valid_symbol(i.get_exchange_type())
-            responses.append(i.history(valid_symbol, 150, resolution='1d'))
-        for i in responses:
-            self.check_product_history_columns(i)
+            type_ = i.get_exchange_type()
+
+            if type_ == 'okx' or type_ == 'kucoin':
+                continue
+            valid_symbol = get_valid_symbol(type_)
+            response = i.history(valid_symbol, 150, resolution='1d')
 
-            self.assertEqual(150, len(i))
+            print(f'Checking {i.get_exchange_type()}')
+            self.check_product_history_columns(response)
 
-            self.check_product_history_types(i)
+            self.assertEqual(150, len(response))
+
+            self.check_product_history_types(response)
 
     def test_point_with_end_history(self):
         responses = []
@@ -424,10 +505,10 @@ def test_point_with_end_history(self):
         arbitrary_date: dt = dateparser.parse("8/23/21")
 
         # This won't work at the start of the
-        end_date = arbitrary_date.replace(day=arbitrary_date.day-1)
-        close_stop = str(arbitrary_date.replace(day=arbitrary_date.day-2).date())
+        end_date = arbitrary_date.replace(day=arbitrary_date.day - 1)
+        close_stop = str(arbitrary_date.replace(day=arbitrary_date.day - 2).date())
 
-        expected_hours = end_date.day * 24 - (24*2)
+        expected_hours = end_date.day * 24 - (24 * 2)
 
         end_date_str = str(end_date.date())
 
@@ -457,10 +538,13 @@ def test_start_with_end_history(self):
         stop = str(stop_dt.date())
 
         # The dates are offset by one because the time is the open time
-        close_stop = str(stop_dt.replace(day=stop_dt.day-1).date())
+        close_stop = str(stop_dt.replace(day=stop_dt.day - 1).date())
 
         for i in self.data_interfaces:
             valid_symbol = get_valid_symbol(i.get_exchange_type())
+            # skip okx and kucoin because bad
+            if i.get_exchange_type() == 'okx' or i.get_exchange_type() == 'kucoin':
+                continue
             responses.append(i.history(valid_symbol, resolution='1h', start_date=start, end_date=stop))
 
         for idx, resp in enumerate(responses):
@@ -483,6 +567,9 @@ def test_get_product_history(self):
         for i in self.data_interfaces:
             type_ = i.get_exchange_type()
 
+            if type_ == 'okx':
+                continue
+
             # Exclude alpaca currently because the trading hours make it unreliable
             if not (type_ == "alpaca" or type_ == 'oanda'):
                 responses.append(i.get_product_history(get_valid_symbol(type_),
@@ -501,18 +588,18 @@ def test_get_product_history(self):
         for i in responses:
             self.check_product_history_columns(i)
 
-            self.assertEqual(len(i), test_intervals)
+            self.assertEqual(test_intervals, len(i))
             start_time = i['time'][0]
             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,
-                            f"\ncurrent_time-(build_day()): {current_time-(build_day())}\nend_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
-            self.assertTrue(intervals_ago < start_time < intervals_ago+(build_day()))
+            self.assertTrue(intervals_ago < start_time < intervals_ago + (build_day()))
 
             self.check_product_history_types(i)
 
diff --git a/tests/new_interface_tests/__init__.py b/tests/new_interface_tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/new_interface_tests/conftest.py b/tests/new_interface_tests/conftest.py
new file mode 100644
index 00000000..4c6bea91
--- /dev/null
+++ b/tests/new_interface_tests/conftest.py
@@ -0,0 +1,26 @@
+from _pytest.python import Metafunc
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+
+from blankly.exchanges.futures.futures_exchange import FuturesExchange
+
+from tests.new_interface_tests.test_utils import FUTURES_EXCHANGES, SYMBOLS
+
+
+def gen_id(arg):
+    if isinstance(arg, FuturesExchange):
+        return arg.get_type()
+    elif isinstance(arg, FuturesExchangeInterface):
+        return arg.get_exchange_type()
+    return arg
+
+
+def pytest_generate_tests(metafunc: Metafunc):
+    if 'futures_interface' in metafunc.fixturenames:
+        interfaces = [ex.interface for ex in FUTURES_EXCHANGES]
+        if 'symbol' in metafunc.fixturenames:
+            vals = [(interface, symbol) for interface in interfaces
+                    for symbol in SYMBOLS[interface.get_exchange_type()]]
+            metafunc.parametrize('futures_interface,symbol', vals, ids=gen_id)
+        else:
+            metafunc.parametrize('futures_interface', interfaces, ids=gen_id)
+
diff --git a/tests/new_interface_tests/test_interfaces.py b/tests/new_interface_tests/test_interfaces.py
new file mode 100644
index 00000000..add0439d
--- /dev/null
+++ b/tests/new_interface_tests/test_interfaces.py
@@ -0,0 +1,315 @@
+import sys
+import time
+from datetime import datetime
+from operator import itemgetter
+
+import pytest
+from _pytest.python_api import approx
+
+from blankly.enums import Side, OrderType, ContractType, OrderStatus, HedgeMode, MarginType, PositionMode, TimeInForce
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+from blankly.utils import utils
+from tests.new_interface_tests.test_utils import wait_till_filled, homogeneity_testing, sell, buy, cancelling_order, close_position, \
+    close_all
+
+# TODO auto truncate
+# TODO min size/min notional api
+
+
+def valid_product_helper(futures_interface: FuturesExchangeInterface, product):
+    base = product['base_asset']
+    quote = product['quote_asset']
+    symbol = product['symbol']
+    assert symbol == base + '-' + quote
+    exc = futures_interface.to_exchange_symbol(symbol)
+    assert futures_interface.to_blankly_symbol(exc) == symbol
+
+
+def test_valid_symbols(futures_interface: FuturesExchangeInterface):
+    for product in futures_interface.get_products().values():
+        valid_product_helper(futures_interface, product)
+
+
+def test_valid_symbol_from_position(
+        futures_interface: FuturesExchangeInterface, symbol: str):
+    with cancelling_order(futures_interface, symbol):
+        products = futures_interface.get_positions().values()
+    for product in products:
+        valid_product_helper(futures_interface, product)
+
+
+@homogeneity_testing
+def test_get_products(futures_interface: FuturesExchangeInterface):
+    return list(futures_interface.get_products().values())
+
+
+@homogeneity_testing
+def test_get_products_symbol(futures_interface: FuturesExchangeInterface,
+                             symbol: str):
+    return futures_interface.get_products(symbol)
+
+
+@homogeneity_testing
+def test_get_account(futures_interface: FuturesExchangeInterface):
+    return list(futures_interface.get_account().values())
+
+
+@homogeneity_testing
+def test_get_positions(futures_interface: FuturesExchangeInterface):
+    return list(futures_interface.get_positions().values())
+
+
+@homogeneity_testing
+@pytest.mark.parametrize('side', [Side.BUY, Side.SELL])
+def test_get_positions(futures_interface: FuturesExchangeInterface,
+                       symbol: str, side: Side):
+    if side == Side.BUY:
+        func = buy
+        m = 1
+    elif side == Side.SELL:
+        func = sell
+        m = -1
+
+    close_position(futures_interface, symbol)
+
+    order = func(futures_interface, symbol)
+    position = futures_interface.get_positions(symbol)
+    assert position.symbol == symbol
+    assert m * position.size == order.size
+    assert 0 < position.size * m
+    assert position.leverage == futures_interface.get_leverage(symbol)
+    # assert position.margin_type == futures_interface.get_margin_type(symbol)
+
+    close_position(futures_interface, symbol)
+
+    return position
+
+
+def test_simple_open_close(futures_interface: FuturesExchangeInterface,
+                           symbol: str) -> None:
+    close_position(futures_interface, symbol)
+
+    # buy
+    size = buy(futures_interface, symbol).size
+    assert futures_interface.get_positions(symbol).size == approx(size)
+
+    close_position(futures_interface, symbol)
+
+    # sell
+    size = sell(futures_interface, symbol).size
+    assert futures_interface.get_positions(symbol).size == approx(-size)
+
+    close_position(futures_interface, symbol)
+
+
+@homogeneity_testing
+@pytest.mark.parametrize('side', [Side.BUY, Side.SELL])
+def test_order(futures_interface: FuturesExchangeInterface, symbol: str,
+               side: Side):
+    close_position(futures_interface, symbol)
+
+    price = futures_interface.get_price(symbol)
+    product = futures_interface.get_products(symbol)
+    size = utils.trunc(13 / price, product.size_precision)
+    order = futures_interface.market_order(symbol, side, size)
+
+    assert order.symbol == symbol
+    assert 0 <= order.id
+    assert order.size == approx(size, rel=0.01)
+    assert order.status in (OrderStatus.OPEN, OrderStatus.FILLED)
+    assert order.type == OrderType.MARKET
+    assert order.contract_type == ContractType.PERPETUAL
+    assert order.side == side
+    assert order.limit_price == 0
+    assert order.position == PositionMode.BOTH
+    assert order.time_in_force == TimeInForce.GTC
+
+    res = wait_till_filled(futures_interface,
+                           order)  # for market orders, just wait for fil
+
+    # make sure we paid what we expect
+    assert res.price == approx(size * price, rel=0.01)
+
+    # check our stake is increased/decreased
+    position_size = futures_interface.get_positions(symbol).size
+    if side == Side.BUY:
+        assert order.size == position_size
+    else:
+        assert order.size == -position_size
+
+    # close position
+    close_position(futures_interface, symbol)
+
+    # set price, status and make sure order/res are otherwise equal
+    order.price = res.price
+    order.status = res.status
+    assert order == res
+
+    return order
+
+
+@homogeneity_testing
+@pytest.mark.parametrize(
+    'type', [OrderType.LIMIT, OrderType.TAKE_PROFIT, OrderType.STOP])
+@pytest.mark.parametrize('side', [Side.BUY, Side.SELL])
+def test_priced_orders(futures_interface: FuturesExchangeInterface,
+                       symbol: str, side: Side, type: OrderType):
+    close_position(futures_interface, symbol)
+
+    price = futures_interface.get_price(symbol)
+    product = futures_interface.get_products(symbol)
+    size = utils.trunc(20 / price, product.size_precision)
+    m = 0.3
+    if side == Side.BUY:
+        m = -m
+    if type == OrderType.STOP:
+        m = -m
+    limit_price = utils.trunc(price * (1 + m), product.price_precision)
+    if type == OrderType.LIMIT:
+        order = futures_interface.limit_order(symbol, side, limit_price, size)
+    elif type == OrderType.TAKE_PROFIT:
+        order = futures_interface.take_profit(symbol, side, limit_price, size)
+    elif type == OrderType.STOP:
+        order = futures_interface.stop_loss(symbol, side, limit_price, size)
+
+    assert order.symbol == symbol
+    assert 0 <= order.id
+    assert order.size == approx(size, rel=0.01)
+    assert order.status == OrderStatus.OPEN
+    assert order.type == type
+    assert order.contract_type == ContractType.PERPETUAL
+    assert order.side == side
+    assert order.limit_price == limit_price
+    assert order.position == PositionMode.BOTH
+    assert order.time_in_force == TimeInForce.GTC
+
+    res = futures_interface.cancel_order(symbol, order.id)
+
+    order.status = OrderStatus.CANCELED
+    assert order == res
+
+    return order
+
+
+def test_set_hedge_mode(futures_interface: FuturesExchangeInterface):
+    if futures_interface.get_exchange_type() == 'ftx_futures':
+        pytest.xfail('FTX Futures does not support hedge mode')
+    close_all(futures_interface)
+    futures_interface.set_hedge_mode(HedgeMode.HEDGE)
+    assert futures_interface.get_hedge_mode() == HedgeMode.HEDGE
+
+
+def test_set_oneway_mode(futures_interface: FuturesExchangeInterface):
+    close_all(futures_interface)
+    futures_interface.set_hedge_mode(HedgeMode.ONEWAY)
+    assert futures_interface.get_hedge_mode() == HedgeMode.ONEWAY
+
+
+def test_account_leverage(futures_interface: FuturesExchangeInterface):
+    close_all(futures_interface)
+    if futures_interface.get_exchange_type() == 'binance_futures':
+        pytest.xfail(
+            'Binance Futures does not support setting account leverage')
+    futures_interface.set_leverage(3)
+    assert futures_interface.get_leverage() == 3
+
+
+def test_symbol_leverage(futures_interface: FuturesExchangeInterface,
+                         symbol: str):
+    if futures_interface.get_exchange_type() == 'ftx_futures':
+        close_all(futures_interface)
+        futures_interface.set_leverage(3)  # set globally for ftx
+        with pytest.raises(
+                Exception):  # changing for symbol should raise an exception
+            futures_interface.set_leverage(3, symbol)
+    else:
+        # for other exchanges we can proceed as normal
+        close_position(futures_interface, symbol)
+        futures_interface.set_leverage(3, symbol)
+    assert futures_interface.get_leverage(symbol) == 3
+
+
+def test_set_cross_margin(futures_interface: FuturesExchangeInterface,
+                          symbol: str):
+    if futures_interface.get_exchange_type() == 'binance_futures':
+        pytest.xfail('Binance doesn\'t have an API for this')
+    close_all(futures_interface)
+    futures_interface.set_margin_type(symbol, MarginType.CROSSED)
+    assert futures_interface.get_margin_type(symbol) == MarginType.CROSSED
+
+
+def test_set_isolated_margin(futures_interface: FuturesExchangeInterface,
+                             symbol: str):
+    if futures_interface.get_exchange_type() == 'ftx_futures':
+        pytest.xfail('FTX Futures does not support isolated margin')
+    futures_interface.set_margin_type(symbol, MarginType.ISOLATED)
+
+    if futures_interface.get_exchange_type() == 'binance_futures':
+        pytest.xfail('Binance doesn\'t have an API for this')
+    assert futures_interface.get_margin_type(symbol) == MarginType.ISOLATED
+
+
+@homogeneity_testing
+def test_get_open_orders(futures_interface: FuturesExchangeInterface,
+                         symbol: str):
+    with cancelling_order(futures_interface, symbol) as order:
+        open_orders = futures_interface.get_open_orders(symbol)
+    assert order in open_orders
+    assert all(o.symbol == symbol for o in open_orders)
+    return open_orders
+
+
+@homogeneity_testing
+def test_get_order(futures_interface: FuturesExchangeInterface, symbol: str):
+    with cancelling_order(futures_interface, symbol) as order:
+        assert futures_interface.get_order(symbol, order.id) == order
+
+
+@homogeneity_testing
+def test_price(futures_interface: FuturesExchangeInterface, symbol: str):
+    price = futures_interface.get_price(symbol)
+    assert 0 < price
+    return price
+
+
+@homogeneity_testing
+def test_funding_rate_history(futures_interface: FuturesExchangeInterface,
+                              symbol: str):
+    day = 60 * 60 * 24
+    now = int(datetime.now().timestamp())
+    start = now - day * 365
+    end = now - day * 7
+    history = futures_interface.get_funding_rate_history(symbol=symbol,
+                                                         epoch_start=start,
+                                                         epoch_stop=end)
+
+    # non-perp contracts don't have funding rates
+    if futures_interface.get_products(
+    )[symbol].contract_type != ContractType.PERPETUAL:
+        assert len(history) == 0
+        return
+
+    # test start and end times
+    assert start <= history[0]['time'] < start + day
+    assert end - day < history[-1]['time'] <= end
+
+    # test ascending order
+    assert sorted(history, key=itemgetter('time')) == history
+
+    # test resolution
+    errors = []
+    resolution = futures_interface.get_funding_rate_resolution()
+    for i in range(1, len(history)):
+        prev = history[i - 1]
+        current = history[i]
+        real_res = current['time'] - prev['time']
+        if real_res != resolution:
+            errors.append(current)
+            print(
+                f'wrong resolution in funding rate data: {real_res}, should be {resolution}',
+                file=sys.stderr)
+            if len(errors) > 20:
+                pytest.fail(f'too many incorrect resolutions: errors={errors}')
+                # pytest.fail(f'too many incorrect resolutions: {errors=}')
+
+    return history
diff --git a/tests/new_interface_tests/test_utils.py b/tests/new_interface_tests/test_utils.py
new file mode 100644
index 00000000..c7fc7a3d
--- /dev/null
+++ b/tests/new_interface_tests/test_utils.py
@@ -0,0 +1,210 @@
+from blankly.enums import OrderStatus, Side, ContractType, OrderType, PositionMode, TimeInForce
+from blankly.exchanges.interfaces.binance_futures.binance_futures import BinanceFutures
+from blankly.exchanges.interfaces.ftx_futures.ftx_futures import FTXFutures
+from blankly.exchanges.futures.futures_exchange import FuturesExchange
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+from blankly.exchanges.orders.futures.futures_order import FuturesOrder
+from blankly.utils import utils
+
+import time
+import functools
+import os
+
+import pytest
+from _pytest.python import Metafunc
+from _pytest.python_api import approx
+
+from contextlib import contextmanager
+
+FUTURES_EXCHANGES = [
+    BinanceFutures(keys_path="./tests/config/keys.json",
+                   preferences_path="./tests/config/settings.json",
+                   portfolio_name="Futures Test Key"),
+    # FTXFutures(keys_path="./tests/config/keys.json",
+    #            preferences_path="./tests/config/settings.json",
+    #            portfolio_name="Dotcom Test Account"),
+]
+SYMBOLS = {
+    'ftx_futures': ['SOL-USD', 'ETH-USD'],
+    'binance_futures': ['BCH-USDT', 'ETH-USDT']
+}
+
+
+def homogeneity_testing(func=None, check_values: bool = False):
+    # allow using without arguments
+    # this trick brought to you by Python Cookbook
+    if func is None:
+        return functools.partial(homogeneity_testing, values=check_values)
+
+    results = {}
+
+    @functools.wraps(func)
+    def wrapper(*args, **kwargs):
+        test_name = os.environ.get('PYTEST_CURRENT_TEST')
+        test_name = test_name[test_name.find('[') + 1:test_name.find(']')]
+        result = func(*args, **kwargs)
+        compare_results(results, result, check_values)
+        results[test_name] = result
+        return result
+
+    return wrapper
+
+
+def compare_results(previous_outputs: dict, result, check_values: bool):
+    for prev_name, prev_results in previous_outputs.items():
+        compare_values(prev_name, prev_results, result, check_values)
+
+
+def compare_values(other_name, other, this, check_values: bool):
+    assert type(this) == type(
+        other
+    ), f'comparing to {other_name}: types do not match. this={this} other={other}'
+    # gapped by python 3.7
+    # ), f'comparing to {other_name}: types do not match. {this=} {other=}'
+    if isinstance(this, list):
+        if this and other:
+            for val in this[1:]:  # check for self-homogenity
+                compare_values('self', val, this[0], check_values)
+            for val in other:
+                compare_values(other_name, val, this[0], check_values)
+    if isinstance(other, dict):
+        other.pop('exchange_specific', None)
+        this.pop('exchange_specific', None)
+        assert this.keys() == other.keys(
+        ), f'comparing to {other_name}: dict keys do not match. ' \
+           f'symmetric difference: {set(this.keys()).symmetric_difference(other.keys())}'
+        for key in other:
+            compare_values(other_name, other[key], this[key], check_values)
+    elif check_values:
+        assert this == other, f'comparing to {other_name}: values are not equal.'
+
+
+def wait_till_filled(interface: FuturesExchangeInterface, order: FuturesOrder):
+    retries = 0
+    res = interface.get_order(order.symbol, order.id)
+    while res.status != OrderStatus.FILLED:
+        if retries > 2:
+            pytest.fail(f"order was not filled in time. status: {res.status}")
+        time.sleep(1 << retries)
+        retries += 1
+        res = interface.get_order(res.symbol, order.id)
+    return res
+
+
+def place_order(interface: FuturesExchangeInterface, symbol: str, side: Side, funds: int, reduce_only: bool = False):
+    product = interface.get_products(symbol)
+    price = interface.get_price(symbol)
+    order_size = utils.trunc(funds / price, product.size_precision)
+
+    order = interface.market_order(symbol, side, order_size, reduce_only=reduce_only)
+    assert order.symbol == symbol
+    assert 0 <= order.id
+    assert order.status in (OrderStatus.OPEN, OrderStatus.FILLED)
+    assert order.type == OrderType.MARKET
+    assert order.contract_type == ContractType.PERPETUAL
+    assert order.side == side
+    assert order.limit_price == 0
+    assert order.position == PositionMode.BOTH
+    assert order.time_in_force == TimeInForce.GTC
+
+    res = wait_till_filled(interface, order)
+    assert res.status == OrderStatus.FILLED
+
+    if reduce_only:
+        # 1% tolerance
+        assert res.price <= price * order_size * 1.01
+        assert res.size <= order_size * 1.01
+
+        # set size equal so we can compare
+        order.size = res.size
+    else:
+        assert res.price == approx(price * order_size, rel=0.01)
+        assert res.size == approx(order_size)
+
+    # set price, status equal so we can compare
+    order.price = res.price
+    order.status = res.status
+    assert order == res
+
+    return order
+
+
+def sell(interface: FuturesExchangeInterface, symbol: str, funds: int = 20, reduce_only: bool = False):
+    return place_order(interface, symbol, Side.SELL, funds, reduce_only)
+
+
+def buy(interface: FuturesExchangeInterface, symbol: str, funds: int = 20, reduce_only: bool = False):
+    return place_order(interface, symbol, Side.BUY, funds, reduce_only)
+
+def close_all(futures_interface: FuturesExchangeInterface):
+    for symbol in futures_interface.get_positions():
+        close_position(futures_interface, symbol)
+
+    for order in futures_interface.get_open_orders():
+        futures_interface.cancel_order(order.symbol, order.id)
+    assert len(futures_interface.get_positions()) == 0
+
+
+def close_position(interface: FuturesExchangeInterface, symbol: str):
+    """
+    Exit position
+    Args:
+        interface: the interface to sell on
+        symbol: the symbol to sell
+    """
+    position = interface.get_positions(symbol)
+    if not position:
+        return
+    if position.size < 0:
+        order = interface.market_order(symbol,
+                                       Side.BUY,
+                                       position.size * -2,
+                                       reduce_only=True)
+    elif position.size > 0:
+        order = interface.market_order(symbol,
+                                       Side.SELL,
+                                       position.size * 2,
+                                       reduce_only=True)
+    else:
+        pytest.fail('position size is zero')
+    wait_till_filled(interface, order)
+    assert interface.get_positions(symbol) is None
+
+
+@contextmanager
+def cancelling_order(interface: FuturesExchangeInterface, symbol: str):
+    """
+    Context manager for placing and cancelling a limit order from within a test.
+    Useful for testing paths that require active orders.
+    Args:
+        interface: The interface to place an order on
+        symbol: The symbol to buy
+
+    Returns: The order object as returned from the interface
+    """
+    product = interface.get_products(symbol)
+    price = interface.get_price(symbol)
+
+    # place our limit order
+    order_size = utils.trunc(100 / price, product.size_precision)  # $1 order
+    limit_price = utils.trunc(price * 0.95, product.price_precision)
+    order = interface.limit_order(symbol, Side.BUY, limit_price, order_size)
+    assert order.symbol == symbol
+    assert 0 <= order.id
+    assert order.size == approx(order_size)
+    assert order.status == OrderStatus.OPEN
+    assert order.type == OrderType.LIMIT
+    assert order.contract_type == ContractType.PERPETUAL
+    assert order.side == Side.BUY
+    assert order.position == PositionMode.BOTH
+    assert order.limit_price == approx(limit_price)
+    assert order.time_in_force == TimeInForce.GTC
+
+    yield order
+
+    res = interface.cancel_order(symbol, order.id)
+    assert res.status == OrderStatus.CANCELED
+
+    assert order != res
+    res.status = OrderStatus.OPEN
+    assert order == res
diff --git a/tests/strategy/test_strategy.py b/tests/strategy/test_strategy.py
index 44dff7c7..0e20a134 100644
--- a/tests/strategy/test_strategy.py
+++ b/tests/strategy/test_strategy.py
@@ -52,7 +52,7 @@ def price_event(self, price, symbol, state: StrategyState):
     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.assertTrue(set(price.keys()) == {'open', 'high', 'low', 'close', 'volume', 'time'})
         self.ran_bar_event = True
 
     def test_backtest(self):
@@ -63,7 +63,7 @@ def test_backtest(self):
             blankly.Kucoin(**kwargs),
             blankly.FTX(**kwargs),
             blankly.CoinbasePro(**kwargs),
-            blankly.Binance(**kwargs),
+            blankly.Binance(**kwargs, portfolio_name='Data Key'),
             blankly.Oanda(**kwargs),
             blankly.Alpaca(**kwargs)
         ]
diff --git a/tests/testing_utils.py b/tests/testing_utils.py
index 9965140c..5d424e39 100644
--- a/tests/testing_utils.py
+++ b/tests/testing_utils.py
@@ -15,11 +15,14 @@
     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
 
 
 def get_valid_symbol(exchange: str):
     if exchange == 'binance':
         return 'BTC-USDT'
+    elif exchange == 'okx':
+        return 'ADA-USDT'
     elif exchange == 'coinbase_pro':
         return 'BTC-USD'
     elif exchange == 'alpaca':
@@ -32,3 +35,16 @@ def get_valid_symbol(exchange: str):
         return 'ETH-USDT'
     else:
         raise LookupError("Specified exchange not found.")
+
+
+def forex_market_open():
+    now = datetime.datetime.now()
+    # friday == 4
+    # saturday == 5
+    # TODO Technically it resumes at 5pm sunday but I don't handle that
+    if now.weekday() >= 5:
+        return False
+    elif now.weekday() >= 4 and now.hour >= 17:
+        return False
+    else:
+        return True
diff --git a/tests/websockets/test_crypto_websockets.py b/tests/websockets/test_crypto_websockets.py
index 8bdc109d..74d46925 100644
--- a/tests/websockets/test_crypto_websockets.py
+++ b/tests/websockets/test_crypto_websockets.py
@@ -27,17 +27,19 @@ def test_crypto_websocket():
 
 
 class CryptoWebsockets:
-
     def __init__(self):
         self.validated_responses = {
             'orderbook': {
                 'coinbase_pro': False,
-                'binance': False
+                'binance': False,
+                'ftx': False,
+                'kucoin': False
             },
             'prices': {
                 'coinbase_pro': False,
                 'binance': False,
                 'ftx':  False,
+                'kucoin': False
             }
         }
 
@@ -61,16 +63,26 @@ def test_websockets(self):
         # 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.price_manager.create_ticker(self.kucoin_price, override_symbol='BTC-USDT', override_exchange='kucoin')
+        self.price_manager.create_ticker(self.okx_price, override_symbol='BTC-USDT', override_exchange='okx')
 
         self.orderbook_manager.create_orderbook(self.coinbase_orderbook, override_symbol='BTC-USD',
                                                 override_exchange='coinbase_pro')
 
+        self.orderbook_manager.create_orderbook(self.kucoin_orderbook, override_symbol='BTC-USDT',
+                                                override_exchange='kucoin')
+
         self.orderbook_manager.create_orderbook(self.binance_orderbook, override_symbol='BTC-USDT',
                                                 override_exchange='binance')
 
-        if self.timeout(180):
+        self.orderbook_manager.create_orderbook(self.ftx_orderbook, override_symbol='BTC-USD',
+                                                override_exchange='ftx')
+
+        self.orderbook_manager.create_orderbook(self.okx_orderbook, override_symbol='BTC-USDT',
+                                                override_exchange='okx')
+
+        if self.timeout(60):
             self.passed = True
 
     def timeout(self, start_at: int):
@@ -143,6 +155,16 @@ def ftx_price(self, message):
             self.validated_responses['prices']['ftx'] = True
             self.price_manager.close_websocket('BTC-USD', 'ftx')
 
+    def kucoin_price(self, message):
+        if self.validate_price_event(message):
+            self.validated_responses['prices']['kucoin'] = True
+            self.price_manager.close_websocket('BTC-USDT', 'kucoin')
+
+    def okx_price(self, message):
+        if self.validate_price_event(message):
+            self.validated_responses['prices']['okx'] = True
+            self.price_manager.close_websocket('BTC-USDT', 'okx')
+
     """
     These can async validate each response
     """
@@ -155,3 +177,18 @@ 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')
+
+    def ftx_orderbook(self, message):
+        if self.validate_orderbook_event(message):
+            self.validated_responses['orderbook']['ftx'] = True
+            self.orderbook_manager.close_websocket('BTC-USD', 'ftx')
+
+    def kucoin_orderbook(self, message):
+        if self.validate_orderbook_event(message):
+            self.validated_responses['orderbook']['kucoin'] = True
+            self.orderbook_manager.close_websocket('BTC-USDT', 'kucoin')
+
+    def okx_orderbook(self, message):
+        if self.validate_orderbook_event(message):
+            self.validated_responses['orderbook']['okx'] = True
+            self.orderbook_manager.close_websocket('BTC-USDT', 'okx')