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

Issue:
fix more bugs & continue deduplication efforts

error when using history with start/end dates 
## Description
When I use start/end dates in the `state.interface.history( ... )` function, I get an error saying `an integer is required`.

## Code to reproduce

```python
exchange = blankly.CoinbasePro()

hist = exchange.interface.history(
    'ETH-USD',
    start_date='2021-04-25',
    end_date='2021-05-01',
    resolution='1d',
    return_as='deque'
)
```

## Error trace

```
    hist = exchange.interface.history('ETH-USD',
  File "/Users/pchalasani/Git/mlcrypt/.venv/lib/python3.8/site-packages/blankly/exchanges/interfaces/abc_base_exchange_interface.py", line 98, in history
    return self.cast_type(response, return_as, point_count)
  File "/Users/pchalasani/Git/mlcrypt/.venv/lib/python3.8/site-packages/blankly/exchanges/interfaces/abc_base_exchange_interface.py", line 161, in cast_type
    response[i] = deque(response[i], point_count)
TypeError: an integer is required
```

## Source of error

Stepping thru the debugger in PyCharm shows where the error is coming from, see this screenshot. Seems like `point_count` needs to be cast to an integer before the return statement.

<img width="715" alt="image" src="https://user-images.githubusercontent.com/554347/166125846-7e5a4d24-1c60-44c4-b7df-de457520fa5a.png">



Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 43c0f91b..2aba11b2 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,7 @@
    <img style="margin: 0 auto; padding-bottom: 15px; padding-top: 30px" width=70%" src="https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/blankly-github-logo.png?alt=media&token=8f436cd2-3d28-432c-867a-afef780f4260">
 </div>
 <br />
+
 <div align="center">
   <b>💨  Rapidly build and deploy quantitative models for stocks, crypto, and forex  🚀</b>
 </div>
@@ -36,29 +37,29 @@
 
 ## Why Blankly? 
 
-​	Blankly is a live trading engine, backtest runner and development framework wrapped into one powerful open source package. Models can be instantly backtested, paper traded, sandbox tested and run live by simply changing a single line. We built blankly for every type of quant including training & running ML models in the same environment, cross-exchange/cross-symbol arbitrage, and even long/short positions on stocks (all with built-in websockets).
-
-​	Convert your existing model or build a new one - unlock the ability to run & optimize across all of our supported exchanges. Getting started is easy - just `pip install blankly` and `blankly init`.
+Blankly is an ecosystem for algotraders enabling anyone to build, monetize and scale their trading algorithms for stocks, crypto, futures or forex. The same code can be backtested, paper traded, sandbox tested and run live by simply changing a single line. Develop locally then deploy, iterate and share using the blankly platform.
 
-## Sponsored Promotion
-<a target="_blank" href="https://tokenbot.com/?utm_source=github&utm_medium=blankly&utm_campaign=algodevs"><img src="https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/github%2FTokenBot-Blankly-banner.png?alt=media&token=843c16b0-da39-41a5-b34d-f7d5fdfdf088">
+Add simulated latency, social media & websocket backtests to existing models or build a new one using live data natively. Getting started is easy - just `pip install blankly` and `blankly init`.
 
 Check out our [website](https://blankly.finance) and our [docs](https://docs.blankly.finance).
 
 [YouTube - Under 25 Lines Build an Alpaca RSI Trading Bot](https://youtu.be/pcm0h63rhUU)
 <a target="_blank" href="https://youtu.be/pcm0h63rhUU"><img src="https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/github%2Fbuild_a_bot_readme_thumbnail.jpg?alt=media&token=a9dd030a-805c-447f-a970-2bc8e1815662" style="border-radius:10px"></a>
 
-### Trade Stocks, Crypto, and Forex Seamlessly
+### Trade Stocks, Crypto, Futures, and Forex
 
 ```python
 from blankly import Alpaca, CoinbasePro
 
 stocks = Alpaca()
 crypto = CoinbasePro()
+futures = BinanceFutures()
 
 # Easily perform the same actions across exchanges & asset types
 stocks.interface.market_order('AAPL', 'buy', 1)
 crypto.interface.market_order('BTC-USD', 'buy', 1)
+# Full futures featureset
+futures.interface.get_hedge_mode()
 ```
 
 ### Backtest your trades, events, websockets, and custom data
@@ -87,7 +88,7 @@ if __name__ == "__main__":
     model = TwitterBot(exchange)
 
     # Add the tweets json here
-    model.backtester.add_custom_events(blankly.data.EventReader('./tweets.json'))
+    model.backtester.add_custom_events(blankly.data.JsonEventReader('./tweets.json'))
     # Now add some underlying prices at 1 month
     model.backtester.add_prices('TSLA', '1h', start_date='3/20/22', stop_date='4/15/22')
 
@@ -158,6 +159,20 @@ If you don't want to use our `init` command, you can find the same files in the
 
 More information can be found on our [docs](https://docs.blankly.finance)
 
+### Docker
+
+1. Pull a python docker container
+
+```bash
+$ docker pull python
+```
+
+2. Move to a directory to initialize and run
+
+```bash
+$ blankly init
+```
+
 ### Directory format
 
 The working directory format should have *at least* these files:
@@ -186,9 +201,11 @@ For more info, and ways to do more advanced things, check out our [getting start
 | Coinbase Pro        | 🟢           | 🟢          | 🟢           | 🟢           |
 | Binance             | 🟢           | 🟢          | 🟢           | 🟢           |
 | Alpaca              | 🟢           | 🟢          | 🟢           | 🟢           |
-| OANDA               | 🟢           |         | 🟢           | 🟢           |
+| OANDA               | 🟢           |  | 🟢           | 🟢           |
 | FTX                 | 🟢           | 🟢          | 🟢           | 🟢           |
 | KuCoin              | 🟢           | 🟢        | 🟢           | 🟢           |
+| Binance Futures | 🟢 | 🟢 | 🟢 | 🟢 |
+| FTX Futures | 🟡 | 🟡 | 🟢 | 🟢 |
 | Okx | 🟢 | 🟢 | 🟢 | 🟢 |
 | Kraken              | 🟡           | 🟡          | 🟡           | 🟡           |
 | Keyless Backtesting |              |            |              | 🟢           |
diff --git a/blankly/PENDING.md b/blankly/PENDING.md
index 40013aa6..50678063 100644
--- a/blankly/PENDING.md
+++ b/blankly/PENDING.md
@@ -2,14 +2,7 @@
 ### Incorrect
 - None
 ### Needed
-- Variables parameter for the price events to initialize the variables defaults. This is across all event types
-- Add `count_decimals` to the utils
-- get_price_derivative has been removed due to sklearn dependency
-- `fit_parabola` has been removed due to just being old 
-- Pass epoch to start_date end_date in .backtest()
-- Modified __init__ headers in strategy to remove the symbol arg
-- Add that ACCOUNT_ID for oanda ca only by obtained by doing `curl -H "Authorization: Bearer AUTH_TOKEN" https://api-fxpractice.oanda.com/v3/accounts`
-- More info about using the keyless exchange
+- None
 
 
 
diff --git a/blankly/__init__.py b/blankly/__init__.py
index ca25f881..fb68f1d2 100644
--- a/blankly/__init__.py
+++ b/blankly/__init__.py
@@ -44,16 +44,20 @@
 from blankly.utils import time_builder
 
 from blankly.enums import Side, OrderType, OrderStatus, TimeInForce
+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.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
+
     reporter = __Reporter
     is_deployed = True
 except ImportError:
diff --git a/blankly/__main__.py b/blankly/__main__.py
new file mode 100644
index 00000000..3cd73785
--- /dev/null
+++ b/blankly/__main__.py
@@ -0,0 +1,4 @@
+import blankly.deployment.new_cli
+
+if __name__ == '__main__':
+    blankly.deployment.new_cli.main()
diff --git a/blankly/data/__init__.py b/blankly/data/__init__.py
index cd0773ad..a9b5d632 100644
--- a/blankly/data/__init__.py
+++ b/blankly/data/__init__.py
@@ -16,19 +16,7 @@
     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
-
+from blankly.data.data_reader import PriceReader, JsonEventReader, TickReader, DataTypes
 
 """
 Some datatype examples
diff --git a/blankly/data/data_reader.py b/blankly/data/data_reader.py
index 3311ce44..de8b616f 100644
--- a/blankly/data/data_reader.py
+++ b/blankly/data/data_reader.py
@@ -20,7 +20,15 @@
 import pandas as pd
 from enum import Enum
 
-from . import DataTypes
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+
+
+class DataTypes(Enum):
+    ohlcv_csv = 0
+    tick_csv = 1
+    ohlcv_json = 2
+    event_json = 3
+
 
 """
 This class seeks to solve these problems:
@@ -35,7 +43,7 @@ class FileTypes(Enum):
     json = 'json'
 
 
-class __DataReader:
+class DataReader:
     @staticmethod
     def _check_length(df: pd.DataFrame, identifier: str):
         try:
@@ -65,7 +73,7 @@ def __init__(self, data_type: [DataTypes, str]):
         self.price_data: bool = self.__is_price_data(data_type)
 
 
-class __FormatReader(__DataReader):
+class __FormatReader(DataReader):
     def __init__(self, data_type):
         super().__init__(data_type)
 
@@ -178,7 +186,7 @@ def __init__(self, file_path: [str, list], symbol: [str, list]):
         self.prices_info = {}
 
         try:
-            assert(len(symbols) == len(set(symbols)))
+            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.")
@@ -197,7 +205,14 @@ def __init__(self, file_path: [str, list], symbol: [str, list]):
         self._guess_resolutions()
 
 
-class EventReader(__DataReader):
+class EventReader(DataReader):
+    def __init__(self, event_type: str, events: dict):
+        super().__init__(DataTypes.event_json)
+        time, data = zip(*events.items())
+        self._write_dataset({'time': time, 'data': data}, event_type, ('time', 'data'))
+
+
+class JsonEventReader(DataReader):
     def __parse_json_events(self, file_path):
         contents = json.loads(open(file_path).read())
 
@@ -214,6 +229,13 @@ def __init__(self, file_path):
         self.__parse_json_events(file_path)
 
 
+class FundingRateEventReader(EventReader):
+    def __init__(self, symbol: str, start: int, stop: int, interface: FuturesExchangeInterface):
+        history = interface.get_funding_rate_history(symbol, start, stop)
+        history = {ev['time']: {'symbol': symbol, 'rate': ev['rate']} for ev in history}
+        super().__init__('__blankly__funding_rate', history)
+
+
 class TickReader(__FormatReader):
     def __init__(self, file_path: [str, list], symbol: [str, list] = None):
         super().__init__(DataTypes.tick_csv)
diff --git a/blankly/data/templates/keyless.py b/blankly/data/templates/keyless.py
index cdbed15f..b4fb5ac0 100644
--- a/blankly/data/templates/keyless.py
+++ b/blankly/data/templates/keyless.py
@@ -1,39 +1,37 @@
 import requests
 import blankly
+from blankly.data import PriceReader
 
 
 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'])
-    if rsi[-1] < 30 and not state.variables['owns_position']:
+    curr_value = state.interface.account[state.base_asset].available
+    if rsi[-1] < 30 and not curr_value:
         # Dollar cost average buy
-        buy = int(state.interface.cash / price)
+        buy = blankly.trunc(state.interface.cash / price, 5)
         state.interface.market_order(symbol, side='buy', size=buy)
-        state.variables['owns_position'] = True
-    elif rsi[-1] > 70 and state.variables['owns_position']:
+    elif rsi[-1] > 70 and curr_value:
         # Dollar cost average sell
-        curr_value = state.interface.account[state.base_asset].available
-        state.interface.market_order(symbol, side='sell', size=int(curr_value))
-        state.variables['owns_position'] = False
+        state.interface.market_order(symbol, side='sell', size=curr_value)
 
 
 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']
-    state.variables['owns_position'] = False
 
 
 if __name__ == "__main__":
     # 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
+    data = requests.get(
+        'https://firebasestorage.googleapis.com/v0/b/blankly-6ada5.appspot.com/o/demo_data.csv?alt=media&token=acfa5c39-8f08-45dc-8be3-2033dc2b7b28').text
     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})
+    exchange = blankly.KeylessExchange(price_reader=PriceReader('./price_examples.csv', 'BTC-USD'))
 
     # Use our strategy helper
     strategy = blankly.Strategy(exchange)
@@ -42,5 +40,5 @@ def init(symbol, state: blankly.StrategyState):
     strategy.add_price_event(price_event, symbol='BTC-USD', resolution='1d', init=init)
 
     # Backtest the strategy
-    results = strategy.backtest(start_date=1568547200, end_date=1642982400)
+    results = strategy.backtest(start_date=1598377600, end_date=1650067200, initial_values={'USD': 10000})
     print(results)
diff --git a/blankly/data/templates/none.py b/blankly/data/templates/none.py
index 92954c96..17668b1b 100644
--- a/blankly/data/templates/none.py
+++ b/blankly/data/templates/none.py
@@ -7,4 +7,4 @@
     if blankly.is_deployed:
         strategy.start()
     else:
-        strategy.backtest(to='1y', initial_values={'USD': 10000})
+        strategy.backtest(to='1y', initial_values={'QUOTE_ASSET': 10000})
diff --git a/blankly/data/templates/none_screener.py b/blankly/data/templates/none_screener.py
index d4bde883..2cd8f440 100644
--- a/blankly/data/templates/none_screener.py
+++ b/blankly/data/templates/none_screener.py
@@ -1,4 +1,4 @@
-from blankly import Screener, ScreenerState
+from blankly import Screener, EXCHANGE_CLASS, ScreenerState
 
 
 def evaluator(symbol, state: ScreenerState):
diff --git a/blankly/data/templates/rsi_bot.py b/blankly/data/templates/rsi_bot.py
index b51fd71b..8f8a03e4 100644
--- a/blankly/data/templates/rsi_bot.py
+++ b/blankly/data/templates/rsi_bot.py
@@ -44,4 +44,4 @@ def init(symbol, state: blankly.StrategyState):
     if blankly.is_deployed:
         strategy.start()
     else:
-        strategy.backtest(to='1y', initial_values={'USD': 10000})
+        strategy.backtest(to='1y', initial_values={'QUOTE_ASSET': 10000})
diff --git a/blankly/data/templates/rsi_screener.py b/blankly/data/templates/rsi_screener.py
index ba0deefb..b01e4ebe 100644
--- a/blankly/data/templates/rsi_screener.py
+++ b/blankly/data/templates/rsi_screener.py
@@ -1,4 +1,5 @@
-from blankly import Screener, Alpaca, ScreenerState
+import blankly
+from blankly import Screener, EXCHANGE_CLASS, ScreenerState
 from blankly.indicators import rsi
 
 tickers = SYMBOL_LIST  # any stocks that you may want
diff --git a/blankly/deployment/api.py b/blankly/deployment/api.py
index dd40cc7f..d0ee72d7 100644
--- a/blankly/deployment/api.py
+++ b/blankly/deployment/api.py
@@ -24,7 +24,7 @@
 blankly_deployment_url = 'https://deploy.blankly.finance'
 
 
-# blankly_deployment_url = 'http://localhost:8080'
+# blankly_deployment_url = 'http://localhost:80'
 
 
 class API:
@@ -171,7 +171,7 @@ def create_model(self, project_id: str, type_: str, name: str, description: str)
         model['projectId'] = project_id or self.user_id
         return model
 
-    def list_models(self, project_id: str):
+    def list_models(self, project_id: str = None):
         models = self.__request('post', 'model/list',
                                 data={
                                     'projectId': project_id or self.user_id
@@ -189,12 +189,17 @@ def list_all_models(self):
                        for model in self.list_models(team['id'])]
         return models
 
+    def get_starter_models(self):
+        return self.__request('get', 'model/starter-models')
+
     def list_teams(self):
         return self.__request('get', 'project/teams')
 
-    def generate_keys(self, project_id: str):
+    def generate_keys(self, project_id: str, name: str, autogenerated: bool = True):
         return self.__request('post', 'project/generate-project-token',
-                              data={'projectId': project_id})
+                              data={'projectId': project_id,
+                                    'name': name,
+                                    'autogenerated': autogenerated})
 
 
 if __name__ == '__main__':
diff --git a/blankly/deployment/cli.py b/blankly/deployment/cli.py
index a7167879..0e196d0e 100644
--- a/blankly/deployment/cli.py
+++ b/blankly/deployment/cli.py
@@ -170,11 +170,11 @@ def print_selection_index(index_):
               TermColors.UNDERLINE + "(Input the index of your selection)" + TermColors.ENDC)
 
         index = int(input(TermColors.UNDERLINE + TermColors.OKCYAN +
-                    "You have chosen:" + TermColors.ENDC + " "))
+                          "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}")
+                              f"{len(options) - 1}")
 
         print('\n' + TermColors.BOLD + TermColors.WARNING + f"Chose {choice}:" + TermColors.ENDC + " " +
               TermColors.BOLD + TermColors.OKBLUE + options[index] + TermColors.ENDC)
@@ -221,7 +221,7 @@ def get_project_model_and_name(args, api: API):
                                        "\t" + TermColors.BOLD + TermColors.WARNING +
                                        'A screener is a model uses blankly.Screener' + TermColors.ENDC])
 
-                model_id = api.create_model(type_, model_name, general_description)['modelId']
+                model_id = api.create_model(None, type_, model_name, general_description)['modelId']
             else:
                 models = api.list_models()
                 ids = []
@@ -375,7 +375,6 @@ def create_and_write_file(filename: str, default_contents: str = None):
 backtest_parser.set_defaults(which='backtest')
 add_path_arg(backtest_parser, required=False)
 
-
 run_parser = subparsers.add_parser('run', help='Mimic the run mechanism used in blankly deployment.')
 run_parser.add_argument('--monitor',
                         action='store_true',
@@ -429,7 +428,7 @@ def is_logged_in():
             # 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):
diff --git a/blankly/deployment/exchange_data.py b/blankly/deployment/exchange_data.py
index 17f52ddf..324b1161 100644
--- a/blankly/deployment/exchange_data.py
+++ b/blankly/deployment/exchange_data.py
@@ -67,10 +67,12 @@ def kucoin_test_func(auth, tld):
                                                      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'],
@@ -81,10 +83,12 @@ def kucoin_test_func(auth, tld):
     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'),
 ]
diff --git a/blankly/deployment/login.py b/blankly/deployment/login.py
index 46adc47b..6cb7ed2f 100644
--- a/blankly/deployment/login.py
+++ b/blankly/deployment/login.py
@@ -30,7 +30,6 @@
 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)
@@ -67,8 +66,6 @@ def do_GET(self):
         return api
 
 
-
-
 def get_token() -> Optional[str]:
     token_file = get_token_file()
     if token_file.exists():
diff --git a/blankly/deployment/new_cli.py b/blankly/deployment/new_cli.py
index 9e731c31..2a9f7fa2 100644
--- a/blankly/deployment/new_cli.py
+++ b/blankly/deployment/new_cli.py
@@ -19,7 +19,10 @@
 import argparse
 import json
 import os.path
+import shutil
+import subprocess
 import sys
+import tempfile
 import traceback
 import webbrowser
 from pathlib import Path
@@ -28,14 +31,14 @@
 
 from questionary import Choice
 
-from blankly.deployment.api import API, blankly_deployment_url
+from blankly.deployment.api import API
 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.ui import text, confirm, print_work, print_failure, print_success, select, show_spinner, path
 from blankly.deployment.exchange_data import EXCHANGES, Exchange, EXCHANGE_CHOICES, exc_display_name, \
     EXCHANGE_CHOICES_NO_KEYLESS
-
+from blankly.utils.utils import load_deployment_settings, load_user_preferences, load_backtest_preferences
 TEMPLATES = {'strategy': {'none': 'none.py',
                           'rsi_bot': 'rsi_bot.py'},
              'screener': {'none': 'none_screener.py',
@@ -124,13 +127,70 @@ def add_key_interactive(exchange: Exchange):
     return False
 
 
+def init_starter_model(model):
+    path = model.get('path', '.')
+    url = model.get('url', 'https://github.com/blankly-finance/examples')
+    exchange_name = model.get('exchange', 'alpaca')
+    model_type = model.get('modelType', 'strategy')
+
+    with tempfile.TemporaryDirectory() as dir:
+        with show_spinner('Downloading files') as spinner:
+            ret = subprocess.run(['git', 'clone', url, dir],
+                                 stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode
+            if ret != 0:
+                spinner.fail('Failed to download starter model. Make sure you have `git` installed.')
+                return
+            for file in os.listdir(os.path.join(dir, path)):
+                try:
+                    shutil.move(os.path.join(dir, path, file), './')
+                except OSError as e:
+                    spinner.fail(f'Failed while copying files: {e}. Try again in an empty directory.')
+                    return
+            spinner.ok('Downloaded files')
+
+    clean_blankly_json()
+
+    exchange = next((e for e in EXCHANGES if e.name == exchange_name), None)
+
+    if exchange and confirm(f'Would you like to add {exchange.display_name} keys to your model?').unsafe_ask():
+        add_key_interactive(exchange)
+
+    if confirm('Would you like to connect this model to the Blankly Platform?').unsafe_ask():
+        api = ensure_login()
+        ensure_model(api)
+
+    print_success('Done!')
+
+
+def clean_blankly_json():
+    with open('blankly.json', 'r') as file:
+        data = json.load(file)
+    del data['model_id']
+    del data['project_id']
+    del data['api_key']
+    del data['api_pass']
+    with open('blankly.json', 'w') as file:
+        json.dump(data, file, indent=4)
+
+
 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 \
+    files = [f for f in os.listdir() if not f.startswith('.')]
+    if files \
             and not confirm('This directory is not empty. Would you like to continue anyways?').unsafe_ask():
         return
 
+    api = None
+    if args.model:
+        api = ensure_login()
+        starters = api.get_starter_models() or []
+        model = next((m for m in starters if m['shortName'] == args.model), None)
+        if not model:
+            print_failure('That starter model doesn\'t exist. Make sure you are typing the name properly.')
+            return
+        init_starter_model(model)
+        return
+
     exchange = select('What exchange would you like to connect to?', EXCHANGE_CHOICES).unsafe_ask()
 
     model_type = 'strategy'
@@ -151,7 +211,8 @@ def blankly_init(args):
     api = None
     model = None
     if args.prompt_login and confirm('Would you like to connect this model to the Blankly Platform?').unsafe_ask():
-        api = ensure_login()
+        if not api:
+            api = ensure_login()
         model = get_model_interactive(api, model_type)
 
     with show_spinner('Generating files') as spinner:
@@ -176,6 +237,7 @@ def blankly_init(args):
 
     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()
@@ -195,6 +257,10 @@ def get_model_interactive(api, model_type):
         models = api.list_all_models()
         spinner.ok('Loaded')
 
+    if not models:
+        print_failure('You have no models on the platform. Are you logged into the right account?')
+        sys.exit()
+
     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:
@@ -211,23 +277,11 @@ def get_model_repr(model: dict) -> str:
 
 
 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"}}}
+    data = load_user_preferences(override_allow_nonexistent=True)
+
+    data['settings']['binance']['binance_tld'] = tld
+    data['settings']['ftx']['ftx_tld'] = tld
+    data['settings']['ftx_futures']['ftx_tld'] = tld
     return json.dumps(data, indent=4)
 
 
@@ -246,25 +300,20 @@ def generate_keys_json():
             portfolio['sandbox'] = portfolio.get('sandbox', False)
     return json.dumps(keys, indent=4)
 
-
-def generate_blankly_json(api: Optional[API], 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'}
-            }
-
+  
+def generate_blankly_json(api: Optional[API], model: Optional[dict], model_type: str, main_script: str = 'bot.py'):
+    data = load_deployment_settings()
+    data['main_script'] = main_script
+    data['type'] = model_type
+    data['python_version'] = get_python_version()
+    
     if model:
         data['model_id'] = model['id']
         data['project_id'] = model['projectId']
 
     if api:
         project_id = model['projectId'] if model else api.user_id
-        keys = api.generate_keys(project_id)
+        keys = api.generate_keys(project_id, f'Local keys for {data["model_id"]}')
         data['api_key'] = keys['apiKey']
         data['api_pass'] = keys['apiPass']
     return json.dumps(data, indent=4)
@@ -272,20 +321,9 @@ def generate_blankly_json(api: Optional[API], model: Optional[dict], model_type)
 
 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}}
+    data = load_backtest_preferences(override_allow_nonexistent=True)
+
+    data['settings']['quote_account_value_in'] = currency
     return json.dumps(data, indent=4)
 
 
@@ -298,7 +336,8 @@ def generate_bot_py(exchange: Optional[Exchange], template: str) -> str:
         ('EXCHANGE_NAME', exchange.display_name,),
         ('EXCHANGE_CLASS', exchange.python_class,),
         ('SYMBOL_LIST', "['" + "', '".join(exchange.symbols) + "']",),
-        ('SYMBOL', exchange.symbols[0],)
+        ('SYMBOL', exchange.symbols[0],),
+        ('QUOTE_ASSET', exchange.currency)
     ]:
         bot_py = bot_py.replace(pattern, replacement)
     return bot_py
@@ -336,15 +375,28 @@ def ensure_model(api: API):
                                                  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'])
+        model = get_model_interactive(api, data.get('type', 'strategy'))
         data['model_id'] = model['modelId']
         data['project_id'] = model['projectId']
 
     if 'api_key' not in data or 'api_pass' not in data:
-        keys = api.generate_keys(data['project_id'])
+        keys = api.generate_keys(data['project_id'], f'Local keys for {data["model_id"]}')
         data['api_key'] = keys['apiKey']
         data['api_pass'] = keys['apiPass']
 
+    files = [f for f in os.listdir() if not f.startswith('.')]
+    if 'main_script' not in data or data['main_script'].lstrip('./') not in files:
+        if 'bot.py' in files:
+            data['main_script'] = 'bot.py'
+        else:
+            data['main_script'] = path(
+                'What is the path to your main script/entry point? (Usually bot.py)').unsafe_ask()
+
+    if data['main_script'].lstrip('./') not in files:
+        print_failure(
+            f'The file {data["main_script"]} could not be found. Please create it or set a different entry point.')
+        sys.exit()
+
     # save model_id and plan back into blankly.json
     with open('blankly.json', 'w') as file:
         json.dump(data, file, indent=4)
@@ -353,7 +405,7 @@ def ensure_model(api: API):
 
 
 def missing_deployment_files() -> list:
-    paths = ['bot.py', 'blankly.json', 'keys.json', 'backtest.json', 'requirements.txt', 'settings.json']
+    paths = ['blankly.json', 'keys.json', 'backtest.json', 'requirements.txt', 'settings.json']
     return [path for path in paths if not Path(path).is_file()]
 
 
@@ -433,10 +485,11 @@ def blankly_key(args):
 
 
 def main():
-    parser = argparse.ArgumentParser(description='Blankly CLI & deployment tool')
+    parser = argparse.ArgumentParser(prog='blankly', 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('model', nargs='?', help='select a starter model')
     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)
diff --git a/blankly/deployment/ui.py b/blankly/deployment/ui.py
index bc6b4937..85bdcb67 100644
--- a/blankly/deployment/ui.py
+++ b/blankly/deployment/ui.py
@@ -28,7 +28,7 @@
 )
 questionary.constants.DEFAULT_STYLE = STYLE
 
-from questionary import confirm, text, select
+from questionary import confirm, text, select, path
 
 
 def fprint(text, **kwargs):
@@ -56,7 +56,7 @@ def text_spinner(
         right=']',
         interval=80
 ):
-    fullstr = text + ' ' * (len(text)//2) + text[:-1]
+    fullstr = text + ' ' * (len(text) // 2) + text[:-1]
     win_size = len(text)
     return YaspinSpinner([
         left + fullstr[i:i + win_size] + right
diff --git a/blankly/exchanges/exchange.py b/blankly/exchanges/exchange.py
index 328dddf3..43fc61f6 100644
--- a/blankly/exchanges/exchange.py
+++ b/blankly/exchanges/exchange.py
@@ -31,7 +31,6 @@
 from blankly.exchanges.interfaces.okx.okx_interface import OkxInterface
 
 
-
 class Exchange(ABCExchange, abc.ABC):
     interface: ABCExchangeInterface
 
@@ -198,4 +197,4 @@ def get_asset_state(self, symbol):
 
     @abc.abstractmethod
     def get_direct_calls(self):
-        pass
+        pass
\ No newline at end of file
diff --git a/blankly/exchanges/interfaces/abc_base_exchange_interface.py b/blankly/exchanges/interfaces/abc_base_exchange_interface.py
index b678e137..8a1fbdaf 100644
--- a/blankly/exchanges/interfaces/abc_base_exchange_interface.py
+++ b/blankly/exchanges/interfaces/abc_base_exchange_interface.py
@@ -26,7 +26,6 @@
 import numpy
 import pandas
 import pandas as pd
-from dateutil import parser
 
 from blankly import utils
 from blankly.utils import time_interval_to_seconds
@@ -93,12 +92,12 @@ def history(self,
         if isinstance(to, int):
             point_count = to
         else:
-            point_count = (stop-start)/res_seconds + 1
+            point_count = int((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()
+        is_backtesting = self.backtesting_time()
         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
@@ -119,6 +118,7 @@ def calculate_epochs(self, start_date, end_date, resolution, to):
             count_from = most_recent_valid_resolution
         else:
             if isinstance(end_date, str):
+                from dateutil import parser
                 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):
@@ -147,7 +147,7 @@ def calculate_epochs(self, start_date, end_date, resolution, to):
     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):
+    def backtesting_time(self):
         return None
 
     @staticmethod
diff --git a/blankly/exchanges/interfaces/abc_exchange_interface.py b/blankly/exchanges/interfaces/abc_exchange_interface.py
index 5b52363c..4ba0125f 100644
--- a/blankly/exchanges/interfaces/abc_exchange_interface.py
+++ b/blankly/exchanges/interfaces/abc_exchange_interface.py
@@ -30,12 +30,12 @@
 
 class ABCExchangeInterface(ABCBaseExchangeInterface, abc.ABC):
     @abc.abstractmethod
-    def __init__(self, exchange_name, authenticated_API):
+    def __init__(self, exchange_name, authenticated_api):
         """
         Create an abstract exchange interface
         Args:
             exchange_name (str): Define exchange name ex: 'binance' or 'coinbase_pro'
-            authenticated_API (obj): Authenticated direct calls object
+            authenticated_api (obj): Authenticated direct calls object
         """
         pass
 
@@ -115,6 +115,34 @@ def limit_order(self,
         """
         pass
 
+    @abc.abstractmethod
+    def take_profit_order(self,
+                          symbol: str,
+                          price: float,
+                          size: float) -> LimitOrder:
+        """
+        Take profit order
+        Args:
+            symbol: asset to buy
+            price: price to sell at
+            size: amount of asset (like BTC)
+        """
+        pass
+
+    @abc.abstractmethod
+    def stop_loss_order(self,
+                        symbol: str,
+                        price: float,
+                        size: float) -> LimitOrder:
+        """
+        Stop loss order
+        Args:
+            symbol: asset to buy
+            price: price to sell at
+            size: amount of asset (like BTC)
+        """
+        pass
+
     @abc.abstractmethod
     def cancel_order(self,
                      symbol: str,
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_interface.py b/blankly/exchanges/interfaces/alpaca/alpaca_interface.py
index d0d01167..301f8135 100644
--- a/blankly/exchanges/interfaces/alpaca/alpaca_interface.py
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_interface.py
@@ -21,14 +21,14 @@
 from datetime import datetime as dt, timezone
 
 import alpaca_trade_api
-import dateparser
 import pandas as pd
 from alpaca_trade_api.rest import APIError as AlpacaAPIError, TimeFrame
-from dateutil import parser
 
 from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
 from blankly.exchanges.orders.limit_order import LimitOrder
 from blankly.exchanges.orders.market_order import MarketOrder
+from blankly.exchanges.orders.stop_loss import StopLossOrder
+from blankly.exchanges.orders.take_profit import TakeProfitOrder
 from blankly.utils import utils as utils
 from blankly.utils.exceptions import APIException
 from blankly.utils.time_builder import build_minute, time_interval_to_seconds, number_interval_to_string
@@ -37,8 +37,8 @@
 class AlpacaInterface(ExchangeInterface):
     def __init__(self, exchange_name, authenticated_api):
         self.__unique_assets = None
-        super().__init__(exchange_name, authenticated_api, valid_resolutions=[60, 60*5, 60*15,
-                                                                              60*60*24])
+        super().__init__(exchange_name, authenticated_api, valid_resolutions=[60, 60 * 5, 60 * 15,
+                                                                              60 * 60 * 24])
         assert isinstance(self.calls, alpaca_trade_api.REST)
 
     def init_exchange(self):
@@ -46,9 +46,9 @@ def init_exchange(self):
             account_info = self.calls.get_account()
         except alpaca_trade_api.rest.APIError as e:
             raise APIException(e.__str__() + ". 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 "
-                               "keys.json.")
+                                             "while in sandbox mode? \nTry toggling the \'sandbox\' setting "
+                                             "in your keys.json or check if the keys were input correctly into your "
+                                             "keys.json.")
         try:
             if account_info['account_blocked']:
                 warnings.warn('Your alpaca account is indicated as blocked for trading....')
@@ -200,6 +200,7 @@ def get_account(self, symbol=None):
 
     @staticmethod
     def __parse_iso(response):
+        from dateutil import parser
         try:
             response['created_at'] = parser.isoparse(response['created_at']).timestamp()
         except ValueError as e:
@@ -213,42 +214,36 @@ def __parse_iso(response):
     @utils.order_protection
     def market_order(self, symbol, side, size) -> MarketOrder:
         assert isinstance(self.calls, alpaca_trade_api.REST)
-        needed = self.needed['market_order']
 
-        renames = [
-            ['qty', 'size']
-        ]
+        needed = self.needed['market_order']
+        order = utils.build_order_info(0, side, size, symbol, 'market')
 
-        order = {
-            'size': size,
-            'side': side,
-            'symbol': symbol,
-            'type': 'market'
-        }
         response = self.calls.submit_order(symbol, side=side, type='market', time_in_force='day', qty=size)
 
-        response = self.__parse_iso(response)
-
-        response = utils.rename_to(renames, response)
-        response = utils.isolate_specific(needed, response)
+        response = self._fix_response(needed, response)
         return MarketOrder(order, response, self)
 
     @utils.order_protection
     def limit_order(self, symbol: str, side: str, price: float, size: float) -> LimitOrder:
         needed = self.needed['limit_order']
+        order = utils.build_order_info(price, side, size, symbol, 'limit')
 
-        renames = [
-            ['limit_price', 'price'],
-            ['qty', 'size']
-        ]
+        response = self.calls.submit_order(symbol,
+                                           side=side,
+                                           type='limit',
+                                           time_in_force='gtc',
+                                           qty=size,
+                                           limit_price=price)
+
+        response = self._fix_response(needed, response)
+        return LimitOrder(order, response, self)
+
+    @utils.order_protection
+    def take_profit_order(self, symbol: str, price: float, size: float) -> TakeProfitOrder:
+        side = 'sell'
+        needed = self.needed['take_profit']
+        order = utils.build_order_info(price, side, size, symbol, 'take_profit')
 
-        order = {
-            'quantity': size,
-            'side': side,
-            'price': price,
-            'symbol': symbol,
-            'type': 'limit'
-        }
         response = self.calls.submit_order(symbol,
                                            side=side,
                                            type='limit',
@@ -256,12 +251,35 @@ def limit_order(self, symbol: str, side: str, price: float, size: float) -> Limi
                                            qty=size,
                                            limit_price=price)
 
-        response = self.__parse_iso(response)
+        response = self._fix_response(needed, response)
+        return TakeProfitOrder(order, response, self)
 
-        response = utils.rename_to(renames, response)
+    @utils.order_protection
+    def stop_loss_order(self, symbol: str, price: float, size: float) -> StopLossOrder:
+        side = 'sell'
+        needed = self.needed['stop_loss']
+        order = utils.build_order_info(price, side, size, symbol, 'stop_loss')
+
+        response = self.calls.submit_order(symbol,
+                                           side=side,
+                                           type='stop',
+                                           time_in_force='gtc',
+                                           qty=size,
+                                           stop_price=price)
+
+        response = self._fix_response(needed, response)
+        return StopLossOrder(order, response, self)
+
+    def _fix_response(self, needed, response):
+        response = self.__parse_iso(response)
+        response = utils.rename_to([
+            ['limit_price', 'price'],
+            ['qty', 'size']
+        ], response)
         response = utils.isolate_specific(needed, response)
-        response['time_in_force'] = response['time_in_force'].upper()
-        return LimitOrder(order, response, self)
+        if 'time_in_force' in response:
+            response['time_in_force'] = response['time_in_force'].upper()
+        return response
 
     def cancel_order(self, symbol, order_id) -> dict:
         assert isinstance(self.calls, alpaca_trade_api.REST)
@@ -305,7 +323,18 @@ def homogenize_order(self, order):
                 ["limit_price", "price"]
             ]
             order = utils.rename_to(renames, order)
-
+        elif order['type'] == "stop_loss":
+            renames = [
+                ["qty", "size"],
+                ["limit_price", "price"]
+            ]
+            order = utils.rename_to(renames, order)
+        elif order['type'] == "take_profit":
+            renames = [
+                ["qty", "size"],
+                ["limit_price", "price"]
+            ]
+            order = utils.rename_to(renames, order)
         elif order['type'] == "market":
             if order['notional']:
                 renames = [
@@ -324,6 +353,8 @@ def homogenize_order(self, order):
             order = utils.rename_to(renames, order)
 
         order = self.__parse_iso(order)
+        if 'time_in_force' in order:
+            order['time_in_force'] = order['time_in_force'].upper()
 
         needed = self.choose_order_specificity(order['type'])
 
@@ -347,7 +378,7 @@ def get_product_history(self, symbol: str, epoch_start: float, epoch_stop: float
             if resolution not in supported_multiples:
                 utils.info_print("Granularity is not an accepted granularity...rounding to nearest valid value.")
                 resolution = supported_multiples[min(range(len(supported_multiples)),
-                                                 key=lambda i: abs(supported_multiples[i] - resolution))]
+                                                     key=lambda i: abs(supported_multiples[i] - resolution))]
 
             found_multiple, row_divisor = super().evaluate_multiples(supported_multiples, resolution)
 
@@ -484,6 +515,7 @@ def find_last_n_points(epoch_start_, epoch_stop_):
     def __convert_times(date):  # There aren't any usages of this
         # convert start_date to datetime object
         if isinstance(date, str):
+            import dateparser
             date = dateparser.parse(date)
         elif isinstance(date, float):
             date = dt.fromtimestamp(date)
diff --git a/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py b/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py
index 887805c6..99c1ef62 100644
--- a/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py
+++ b/blankly/exchanges/interfaces/alpaca/alpaca_websocket.py
@@ -174,12 +174,7 @@ def read_websocket(self):
                 else:
                     recent_time = time.time()
 
-                if self.__log:
-                    if counter % 100 == 0:
-                        self.__file.close()
-                        self.__file = open(self.__filePath, 'a')
-                    line = self.__logging_callback(received)
-                    self.__file.write(line)
+                self.log_response(self.__logging_callback, received)
 
                 # Manage price events and fire for each manager attached
                 interface_message = self.__interface_callback(received)
diff --git a/blankly/exchanges/interfaces/binance/binance_api.py b/blankly/exchanges/interfaces/binance/binance_api.py
index a69e15d0..f760b526 100644
--- a/blankly/exchanges/interfaces/binance/binance_api.py
+++ b/blankly/exchanges/interfaces/binance/binance_api.py
@@ -25,7 +25,6 @@
 from requests.auth import AuthBase
 
 # Create custom authentication for Exchange
-from blankly.exchanges.interfaces.binance.binance_auth import BinanceAuth
 
 
 class BinanceExchangeAuth(AuthBase):
@@ -49,7 +48,7 @@ class API:
     API_URL = 'https://api.binance.{}/api'
     API_TESTNET_URL = 'https://testnet.binance.vision/api'
 
-    def __init__(self, auth: BinanceAuth, tld: str = '.us', testnet: bool = False):
+    def __init__(self, auth, tld: str = '.us', testnet: bool = False):
         self.api_key = auth.keys['API_KEY']
         self.secret_key = auth.keys['API_SECRET']
 
diff --git a/blankly/exchanges/interfaces/binance/binance_interface.py b/blankly/exchanges/interfaces/binance/binance_interface.py
index 066196ff..a465f535 100644
--- a/blankly/exchanges/interfaces/binance/binance_interface.py
+++ b/blankly/exchanges/interfaces/binance/binance_interface.py
@@ -17,7 +17,6 @@
 """
 import time
 
-import binance.exceptions
 import pandas as pd
 
 import blankly.utils.exceptions as exceptions
@@ -26,10 +25,15 @@
 from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
 from blankly.exchanges.orders.limit_order import LimitOrder
 from blankly.exchanges.orders.market_order import MarketOrder
+from blankly.exchanges.orders.stop_loss import StopLossOrder
+from blankly.exchanges.orders.take_profit import TakeProfitOrder
 
 
 class BinanceInterface(ExchangeInterface):
+    _asset_precision: dict
+
     def __init__(self, exchange_name, authenticated_api):
+        self._asset_precision = {}
         # Initialize this as None so that it can be filled & cached when needed
         self.__available_currencies = None
         super().__init__(exchange_name, authenticated_api, valid_resolutions=[60, 180, 300, 900, 1800, 3600, 7200,
@@ -37,6 +41,7 @@ def __init__(self, exchange_name, authenticated_api):
                                                                               604800, 2592000])
 
     def init_exchange(self):
+        import binance.exceptions
         try:
             self.calls.get_account()
         except binance.exceptions.BinanceAPIException:
@@ -248,6 +253,23 @@ def get_account(self, symbol=None) -> utils.AttributeDict:
                 })
         return utils.AttributeDict(parsed_dictionary)
 
+    def _fix_response(self, needed, response):
+        response['side'] = response['side'].lower()
+        response['type'] = response['type'].lower()
+        response['status'] = super().homogenize_order_status('binance', response['status'].lower())
+        response['symbol'] = utils.to_blankly_symbol(response['symbol'], 'binance')
+        if 'transactTime' in response:
+            response["transactTime"] /= 1000
+        response = utils.rename_to([
+            ["orderId", "id"],
+            ["transactTime", "created_at"],
+            ["origQty", "size"],
+            ["timeInForce", "time_in_force"],
+            ["cummulativeQuoteQty", "funds"]
+        ], response)
+        response = utils.isolate_specific(needed, response)
+        return response
+
     @utils.order_protection
     def market_order(self, symbol, side, size) -> MarketOrder:
         """
@@ -303,13 +325,8 @@ def market_order(self, symbol, side, size) -> MarketOrder:
             ]
         }
         """
-        renames = [
-            ["orderId", "id"],
-            ["transactTime", "created_at"],
-            ["origQty", "size"],
-            ["timeInForce", "time_in_force"],
-            ["cummulativeQuoteQty", "funds"]
-        ]
+        if self.should_auto_trunc:
+            size = utils.trunc(size, self.get_asset_precision(symbol))
         order = {
             'size': size,
             'side': side,
@@ -320,14 +337,7 @@ def market_order(self, symbol, side, size) -> MarketOrder:
         # The interface here will be the query of order status from this object, because orders are dynamic
         # creatures
         response = self.calls.order_market(symbol=modified_symbol, side=side, quantity=size)
-        response['side'] = response['side'].lower()
-        response['type'] = response['type'].lower()
-        response['status'] = super().homogenize_order_status('binance', response['status'].lower())
-        response["transactTime"] = response["transactTime"] / 1000
-        response['symbol'] = utils.to_blankly_symbol(response['symbol'], 'binance',
-                                                     quote_guess=utils.get_quote_asset(symbol))
-        response = utils.rename_to(renames, response)
-        response = utils.isolate_specific(needed, response)
+        response = self._fix_response(needed, response)
         return MarketOrder(order, response, self)
 
     @utils.order_protection
@@ -373,6 +383,8 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
         BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException
 
         """
+        if self.should_auto_trunc:
+            size = utils.trunc(size, self.get_asset_precision(symbol))
         order = {
             'size': size,
             'side': side,
@@ -382,20 +394,121 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
         }
         modified_symbol = utils.to_exchange_symbol(symbol, 'binance')
         response = self.calls.order_limit(symbol=modified_symbol, side=side, price=price, quantity=size)
-        renames = [
-            ["orderId", "id"],
-            ["transactTime", "created_at"],
-            ["origQty", "size"],
-            ["timeInForce", "time_in_force"],
-        ]
-        response['side'] = response['side'].lower()
-        response['type'] = response['type'].lower()
-        response['status'] = super().homogenize_order_status('binance', response['status'].lower())
-        response['symbol'] = utils.to_blankly_symbol(response['symbol'], 'binance')
-        response = utils.rename_to(renames, response)
-        response = utils.isolate_specific(needed, response)
+        response = self._fix_response(needed, response)
         return LimitOrder(order, response, self)
 
+    @utils.order_protection
+    def take_profit_order(self, symbol, price, size) -> TakeProfitOrder:
+        """
+        Used for sending take profit orders
+        Args:
+            symbol: currency to sell
+            price: price to sell at
+            size: amount of currency (like BTC)
+        """
+        needed = self.needed['take_profit']
+        """Send in a new take-profit order
+
+        Any order with an icebergQty MUST have timeInForce set to GTC.
+
+        :param symbol: required
+        :type symbol: str
+        :param side: required
+        :type side: str
+        :param quantity: required
+        :type quantity: decimal
+        :param price: required
+        :type price: str
+        :param timeInForce: default Good till cancelled
+        :type timeInForce: str
+        :param newClientOrderId: A unique id for the order. Automatically generated if not sent.
+        :type newClientOrderId: str
+        :param icebergQty: Used with LIMIT, STOP_LOSS_LIMIT, and TAKE_PROFIT_LIMIT to create an iceberg order.
+        :type icebergQty: decimal
+        :param newOrderRespType: Set the response JSON. ACK, RESULT, or FULL; default: RESULT.
+        :type newOrderRespType: str
+        :param recvWindow: the number of milliseconds the request is valid for
+        :type recvWindow: int
+
+        :returns: API response
+
+        See order endpoint for full response options
+
+        :raises: BinanceRequestException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException,
+        BinanceOrderMinPriceException, BinanceOrderMinTotalException,
+        BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException
+
+        """
+        side = 'sell'
+        order = {
+            'size': size,
+            'side': side,
+            'price': price,
+            'symbol': symbol,
+            'type': 'take_profit'
+        }
+        modified_symbol = utils.to_exchange_symbol(symbol, 'binance')
+        response = self.calls.create_order(symbol=modified_symbol, side=side, stopPrice=price, quantity=size,
+                                           type='TAKE_PROFIT')
+        response = self._fix_response(needed, response)
+        return TakeProfitOrder(order, response, self)
+
+    @utils.order_protection
+    def stop_loss_order(self, symbol, price, size) -> StopLossOrder:
+        """
+        Used for sending stop loss orders
+        Args:
+            symbol: currency to sell
+            price: price to sell at
+            size: amount of currency (like BTC)
+        """
+        needed = self.needed['stop_loss']
+        """Send in a new stop-loss order
+
+        Any order with an icebergQty MUST have timeInForce set to GTC.
+
+        :param symbol: required
+        :type symbol: str
+        :param side: required
+        :type side: str
+        :param quantity: required
+        :type quantity: decimal
+        :param price: required
+        :type price: str
+        :param timeInForce: default Good till cancelled
+        :type timeInForce: str
+        :param newClientOrderId: A unique id for the order. Automatically generated if not sent.
+        :type newClientOrderId: str
+        :param icebergQty: Used with LIMIT, STOP_LOSS_LIMIT, and TAKE_PROFIT_LIMIT to create an iceberg order.
+        :type icebergQty: decimal
+        :param newOrderRespType: Set the response JSON. ACK, RESULT, or FULL; default: RESULT.
+        :type newOrderRespType: str
+        :param recvWindow: the number of milliseconds the request is valid for
+        :type recvWindow: int
+
+        :returns: API response
+
+        See order endpoint for full response options
+
+        :raises: BinanceRequestException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException,
+        BinanceOrderMinPriceException, BinanceOrderMinTotalException,
+        BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException
+
+        """
+        side = 'sell'
+        order = {
+            'size': size,
+            'side': side,
+            'price': price,
+            'symbol': symbol,
+            'type': 'stop_loss'
+        }
+        modified_symbol = utils.to_exchange_symbol(symbol, 'binance')
+        response = self.calls.create_order(symbol=modified_symbol, side=side, stopPrice=price, quantity=size,
+                                           type='STOP_LOSS')
+        response = self._fix_response(needed, response)
+        return StopLossOrder(order, response, self)
+
     def cancel_order(self, symbol, order_id) -> dict:
         """Cancel an active order. Either orderId or origClientOrderId must be sent.
 
@@ -586,7 +699,10 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
         Returns:
             Dataframe with *at least* 'time (epoch)', 'low', 'high', 'open', 'close', 'volume' as columns.
         """
+        return self._binance_get_product_history(self.calls, symbol, epoch_start, epoch_stop, resolution)
 
+    @staticmethod
+    def _binance_get_product_history(calls, symbol, epoch_start, epoch_stop, resolution):
         resolution = blankly.time_builder.time_interval_to_seconds(resolution)
 
         # epoch_start, epoch_stop = super().get_product_history(symbol, epoch_start, epoch_stop, resolution)
@@ -632,9 +748,9 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
         while need > 1000:
             # Close is always 300 points ahead
             window_close = int(window_open + 1000 * resolution)
-            history = history + self.calls.get_klines(symbol=symbol, startTime=window_open * 1000,
-                                                      endTime=window_close * 1000, interval=gran_string,
-                                                      limit=1000)
+            history = history + calls.get_klines(symbol=symbol, startTime=window_open * 1000,
+                                                 endTime=window_close * 1000, interval=gran_string,
+                                                 limit=1000)
 
             window_open = window_close
             need -= 1000
@@ -642,9 +758,9 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
             utils.update_progress((initial_need - need) / initial_need)
 
         # Fill the remainder
-        history_block = history + self.calls.get_klines(symbol=symbol, startTime=window_open * 1000,
-                                                        endTime=epoch_stop * 1000, interval=gran_string,
-                                                        limit=1000)
+        history_block = history + calls.get_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',
diff --git a/blankly/exchanges/interfaces/binance_futures/binance_futures_interface.py b/blankly/exchanges/interfaces/binance_futures/binance_futures_interface.py
index 0dfd6178..17139ff3 100644
--- a/blankly/exchanges/interfaces/binance_futures/binance_futures_interface.py
+++ b/blankly/exchanges/interfaces/binance_futures/binance_futures_interface.py
@@ -16,19 +16,22 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 import time
+from operator import itemgetter
 from typing import Optional
 
 import binance.exceptions
 
+from blankly.exchanges.interfaces.binance.binance_interface import BinanceInterface
+
 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
@@ -41,16 +44,16 @@ def cached_property(func):
 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),
+    (0.00020, 0.00040),
+    (0.00016, 0.00040),
+    (0.00014, 0.00035),
+    (0.00012, 0.00032),
+    (0.00010, 0.00030),
+    (0.00008, 0.00027),
+    (0.00006, 0.00025),
+    (0.00004, 0.00022),
+    (0.00002, 0.00020),
+    (0.00000, 0.00017),
 ]
 
 
@@ -87,8 +90,8 @@ def init_exchange(self):
         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"
+        except binance.exceptions.BinanceAPIException as e:
+            if e.code != -4059:  # re raise anything other than "already set"
                 raise e
 
     def set_hedge_mode(self, hedge_mode: HedgeMode):
@@ -113,7 +116,7 @@ def get_leverage(self, symbol: str = None) -> float:
                 symbol=symbol)[0]['leverage'])
 
     @utils.order_protection
-    def set_leverage(self, leverage: int, symbol: str = None):
+    def set_leverage(self, leverage: float, symbol: str = None):
         if not symbol:
             raise Exception(
                 'Binance Futures does not support account wide leverage. Use interface.set_leverage(leverage, '
@@ -168,7 +171,7 @@ def get_products(self, filter: str = None) -> dict:
             if '_' in prod['symbol']:
                 continue  # don't support expiring contracts
             symbol = prod['baseAsset'] + '-' + prod['quoteAsset']
-            products[symbol] = utils.AttributeDict({
+            products[symbol] = {
                 'symbol': symbol,
                 'base_asset': prod['baseAsset'],
                 'quote_asset': prod['quoteAsset'],
@@ -176,28 +179,29 @@ def get_products(self, filter: str = None) -> dict:
                 '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:
+    def get_account(self, filter=None) -> dict:
         res = self.calls.futures_account()
 
-        accounts = utils.AttributeDict()
+        accounts = {}
         for asset in res['assets']:
             symbol = asset['asset']
-            accounts[symbol] = utils.AttributeDict({
+            accounts[symbol] = {
                 'available': float(asset['availableBalance']),
+                'hold': 0.0,  # TODO
                 'exchange_specific': asset,
-            })
+            }
 
         if filter:
             return accounts[filter]
         return accounts
 
-    def get_positions(self, filter: str = None) -> Optional[dict]:
+    def get_position(self, filter: str = None) -> Optional[dict]:
         account = self.calls.futures_account()
 
         positions = {}
@@ -209,21 +213,20 @@ def get_positions(self, filter: str = None) -> Optional[dict]:
             if size == 0:
                 continue  # don't show empty positions
             symbol = self.to_blankly_symbol(symbol)
+            base, quote = symbol.split('-')
             margin = MarginType.ISOLATED \
                 if position['isolated'] else MarginType.CROSSED
-            positions[symbol] = utils.AttributeDict({
+            positions[symbol] = {
                 'symbol': symbol,
-                'base_asset': utils.get_base_asset(symbol),
-                'quote_asset': utils.get_quote_asset(symbol),
+                'base_asset': base,
+                'quote_asset': quote,
                 'size': size,
+                'leverage': float(position['leverage']),
+                'margin_type': MarginType.ISOLATED if position.get('isolated', False) else MarginType.CROSSED,
                 '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)
@@ -304,7 +307,7 @@ def limit_order(
 
         return self.parse_order_response(response)
 
-    def take_profit(
+    def take_profit_order(
             self,
             symbol: str,
             side: Side,
@@ -328,7 +331,7 @@ def take_profit(
         return self.parse_order_response(response)
 
     @utils.order_protection
-    def stop_loss(
+    def stop_loss_order(
             self,
             symbol: str,
             side: Side,
@@ -375,14 +378,20 @@ 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:
+    def get_fees(self) -> dict:
         # 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})
+        return {'maker': maker, 'taker': taker}
+
+    def get_maker_fee(self) -> float:
+        return self.get_fees()['maker']
+
+    def get_taker_fee(self) -> float:
+        return self.get_fees()['taker']
 
     @property
-    def account(self) -> utils.AttributeDict:
+    def account(self) -> dict:
         return self.get_account()
 
     @property
@@ -391,139 +400,55 @@ def orders(self) -> list:
 
     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'])
+        # reuse impl from SPOT interface, it's the same thing
+        return BinanceInterface._binance_get_product_history(self.calls, symbol, epoch_start, epoch_stop, resolution)
 
     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
+        if self.calls.testnet:
+            # testnet api is incredibly stupid and has cost me several hours of my life
+            resolution = self.get_funding_rate_resolution()
+            start = int(epoch_start)
+            end = int(epoch_stop)
+            start = start - (start % resolution) + resolution
+            end = end - (end % resolution) + resolution
+            return [{'rate': 0.0001, 'time': t}
+                    for t in range(start, end + 1, resolution)]
 
-        # UNCOMMENT FOR WALRUS
-        # while response := self.calls.futures_funding_rate(
-        #         symbol=symbol,
-        #         startTime=window_start * 1000,
-        #         endTime=window_end * 1000,
-        #         limit=limit):
+        history = []
+        window = int(epoch_start)
 
-        # 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)
+                startTime=window,
+                limit=1000)
             # very stinky ^^
 
-            history.extend({
-                'rate': float(e['fundingRate']),
-                'time': e['fundingTime'] // 1000
-            } for e in response)
+            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)
-
+                # the (testnet) api is stupid and sometimes gives us times we don't want
+                if history[0]['time'] < window:
+                    break
+                window = history[-1]['time'] + 1
+                if epoch_stop < window:
+                    break
+        history.sort(key=itemgetter('time'))
+        while history and epoch_stop < history[-1]['time']:
+            history.pop()
+        while history and history[0]['time'] < epoch_start:
+            history.pop(0)
         return history
 
     def get_funding_rate_resolution(self) -> int:
         return time_builder.build_hour() * 8  # 8 hours
+
+    def get_funding_rate(self, symbol: str):
+        response = self.calls.futures_mark_price(symbol=symbol)
+        return response['nextFundingTime'], response['lastFundingRate']
+
+
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
index 015488cf..192ed400 100644
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro.py
@@ -36,7 +36,7 @@ def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=Non
             calls = CoinbaseProAPI(api_key=keys['API_KEY'],
                                    api_secret=keys['API_SECRET'],
                                    api_pass=keys['API_PASS'],
-                                   API_URL="https://api-public.sandbox.pro.coinbase.com/")
+                                   api_url="https://api-public.sandbox.pro.coinbase.com/")
         else:
             # Create the authenticated object
             calls = CoinbaseProAPI(api_key=keys['API_KEY'],
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_api.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_api.py
index 319a363d..2bc61a43 100644
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_api.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_api.py
@@ -59,9 +59,9 @@ def get_auth_headers(timestamp, message, api_key, secret_key, passphrase):
 
 
 class API:
-    def __init__(self, api_key: str, api_secret: str, api_pass: str, API_URL: str='https://api.pro.coinbase.com/'):
+    def __init__(self, api_key: str, api_secret: str, api_pass: str, api_url: str = 'https://api.pro.coinbase.com/'):
         self.__auth = CoinbaseExchangeAuth(api_key, api_secret, api_pass)
-        self.__api_url = API_URL
+        self.__api_url = api_url
         self.session = requests.Session()
 
     """
@@ -621,8 +621,10 @@ def place_stop_order(self, product_id, side, price, size=None, funds=None,
         """
         params = {'product_id': product_id,
                   'side': side,
+                  'stop_price': price,
                   'price': price,
-                  'order_type': 'stop',
+                  'order_type': 'limit',
+                  'stop': 'loss',
                   'size': size,
                   'funds': funds,
                   'client_oid': client_oid,
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
index 34a4d4d6..b88db10e 100644
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_interface.py
@@ -26,6 +26,8 @@
 from blankly.exchanges.orders.limit_order import LimitOrder
 from blankly.exchanges.orders.market_order import MarketOrder
 from blankly.exchanges.orders.stop_limit import StopLimit
+from blankly.exchanges.orders.stop_loss import StopLossOrder
+from blankly.exchanges.orders.take_profit import TakeProfitOrder
 from blankly.utils.exceptions import APIException, InvalidOrder
 
 
@@ -159,18 +161,11 @@ def market_order(self, symbol, side, size) -> MarketOrder:
             'settled': False
         }
         """
-        order = {
-            'size': size,
-            'side': side,
-            'symbol': symbol,
-            'type': 'market'
-        }
+        if self.should_auto_trunc:
+            size = utils.trunc(size, self.get_asset_precision(symbol))
+        order = utils.build_order_info(0, side, size, symbol, 'market')
         response = self.calls.place_market_order(symbol, side, size=size)
-        if "message" in response:
-            raise InvalidOrder("Invalid Order: " + response["message"])
-        response["created_at"] = utils.epoch_from_iso8601(response["created_at"])
-        response["symbol"] = response.pop('product_id')
-        response = utils.isolate_specific(needed, response)
+        response = self._fix_response(needed, response)
         return MarketOrder(order, response, self)
 
     @utils.order_protection
@@ -203,20 +198,91 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
             "settled": false
         }
         """
-        order = {
-            'size': size,
-            'side': side,
-            'price': price,
-            'symbol': symbol,
-            'type': 'limit'
-        }
+        if self.should_auto_trunc:
+            size = utils.trunc(size, self.get_asset_precision(symbol))
+        order = utils.build_order_info(price, side, size, symbol, 'limit')
         response = self.calls.place_limit_order(symbol, side, price, size=size)
+        response = self._fix_response(needed, response)
+        return LimitOrder(order, response, self)
+
+    @staticmethod
+    def _fix_response(needed, response):
         if "message" in response:
             raise InvalidOrder("Invalid Order: " + response["message"])
         response["created_at"] = utils.epoch_from_iso8601(response["created_at"])
         response["symbol"] = response.pop('product_id')
         response = utils.isolate_specific(needed, response)
-        return LimitOrder(order, response, self)
+        return response
+
+    @utils.order_protection
+    def take_profit_order(self, symbol, price, size) -> TakeProfitOrder:
+        """
+        Used for placing a take-profit orders
+        Args:
+            symbol: currency to sell
+            price: price to sell at
+            size: amount of currency (like BTC)
+        """
+        needed = self.needed['take_profit']
+        """
+        {
+            "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2",
+            "price": "0.10000000",
+            "size": "0.01000000",
+            "product_id": "BTC-USD",
+            "side": "buy",
+            "stp": "dc",
+            "type": "limit",
+            "time_in_force": "GTC",
+            "post_only": false,
+            "created_at": "2016-12-08T20:02:28.53864Z",
+            "fill_fees": "0.0000000000000000",
+            "filled_size": "0.00000000",
+            "executed_value": "0.0000000000000000",
+            "status": "pending",
+            "settled": false
+        }
+        """
+        side = 'sell'
+        order = utils.build_order_info(price, side, size, symbol, 'take_profit')
+        response = self.calls.place_limit_order(symbol, side, price, size=size)
+        response = self._fix_response(needed, response)
+        return TakeProfitOrder(order, response, self)
+
+    @utils.order_protection
+    def stop_loss_order(self, symbol, price, size) -> StopLossOrder:
+        """
+        Used for placing a stop-loss order
+        Args:
+            symbol: currency to sell
+            price: price to sell at
+            size: amount of currency (like BTC)
+        """
+        needed = self.needed['stop_loss']
+        """
+        {
+            "id": "d0c5340b-6d6c-49d9-b567-48c4bfca13d2",
+            "price": "0.10000000",
+            "size": "0.01000000",
+            "product_id": "BTC-USD",
+            "side": "buy",
+            "stp": "dc",
+            "type": "limit",
+            "time_in_force": "GTC",
+            "post_only": false,
+            "created_at": "2016-12-08T20:02:28.53864Z",
+            "fill_fees": "0.0000000000000000",
+            "filled_size": "0.00000000",
+            "executed_value": "0.0000000000000000",
+            "status": "pending",
+            "settled": false
+        }
+        """
+        side = 'sell'
+        order = utils.build_order_info(price, side, size, symbol, 'stop_loss')
+        response = self.calls.place_stop_order(symbol, side, price=price, size=size)
+        response = self._fix_response(needed, response)
+        return StopLossOrder(order, response, self)
 
     """
     Stop limit isn't added to the abstract class because the binance version is barely supported.
@@ -261,7 +327,7 @@ def stop_limit(self, symbol, side, stop_price, limit_price, size, stop='loss') -
         order = {
             'symbol': symbol,
             'side': side,
-            'type': 'stop',
+            'type': 'stop_loss',
             'stop': stop,
             'stop_price': stop_price,
             'size': size,
diff --git a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket_utils.py b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket_utils.py
index cff30614..e3ef6a6a 100644
--- a/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket_utils.py
+++ b/blankly/exchanges/interfaces/coinbase_pro/coinbase_pro_websocket_utils.py
@@ -40,11 +40,19 @@ def no_callback(message):
 
 
 def trade(received):
-    line = str(received["time"]) + "," + str(time.time()) + "," + received["price"] + "," + received[
-        "open_24h"] + "," + received["volume_24h"] + "," + received["low_24h"] + "," + received[
-               "high_24h"] + "," + received["volume_30d"] + "," + received["best_bid"] + "," + received[
-               "best_ask"] + "," + received["last_size"] + "\n"
-    return line
+    return ', '.join([
+        received['time'],
+        time.time(),
+        received['price'],
+        received['open_24h'],
+        received['volume_24h'],
+        received['low_24h'],
+        received['high_24h'],
+        received['volume_24h'],
+        received['best_bid'],
+        received['best_ask'],
+        received['last_size'],
+    ]) + '\n'
 
 
 def trade_interface(message):
diff --git a/blankly/exchanges/interfaces/exchange_interface.py b/blankly/exchanges/interfaces/exchange_interface.py
index 6761924e..84f8dd19 100644
--- a/blankly/exchanges/interfaces/exchange_interface.py
+++ b/blankly/exchanges/interfaces/exchange_interface.py
@@ -17,6 +17,7 @@
 """
 
 import abc
+from functools import lru_cache
 
 import blankly.utils.utils as utils
 from blankly.exchanges.interfaces.abc_exchange_interface import ABCExchangeInterface
@@ -72,6 +73,30 @@ def __init__(self, exchange_name, authenticated_api, preferences_path=None, vali
                 ["type", str],
                 ["side", str]
             ],
+            'take_profit': [
+                ["symbol", str],
+                ["id", str],
+                ["created_at", float],
+                ["stop_price", float],
+                ["stop", str],
+                ["size", float],
+                ["status", str],
+                ["time_in_force", str],
+                ["type", str],
+                ["side", str]
+            ],
+            'stop_loss': [
+                ["symbol", str],
+                ["id", str],
+                ["created_at", float],
+                ["stop_price", float],
+                ["stop", str],
+                ["size", float],
+                ["status", str],
+                ["time_in_force", str],
+                ["type", str],
+                ["side", str]
+            ],
             'stop_limit': [
                 ["symbol", str],
                 ["id", str],
@@ -161,7 +186,7 @@ def cash(self):
         using_setting = self.user_preferences['settings'][self.exchange_name]['cash']
         return self.get_account(using_setting)['available']
 
-    def is_backtesting(self):
+    def backtesting_time(self):
         """
         Overridden by interfaces which have a valid backtesting boolean value
         """
@@ -205,12 +230,11 @@ def get_account(self, symbol=None):
     def choose_order_specificity(self, order_type):
         # This lower should not be necessary if everything is truly homogeneous
         order_type = order_type.lower()
-        if order_type == 'market':
-            return self.needed['market_order']
-        elif order_type == 'limit':
-            return self.needed['limit_order']
-        else:
-            return self.needed['market_order']
+        if order_type == 'stop':
+            return self.needed['stop_loss']
+        elif order_type == 'take_profit':
+            return self.needed['take_profit']
+        return self.needed[f'{order_type}_order']
 
     """
     Order lifecycle should be:
@@ -229,3 +253,15 @@ def homogenize_order_status(exchange, status):
                 return 'new'
 
         return status
+
+    @property
+    def should_auto_trunc(self):
+        return self.user_preferences['settings'].get('auto_truncate', False)
+
+    @lru_cache(None)
+    def get_asset_precision(self, asset):
+        try:
+            product = next(p for p in self.get_products() if p['symbol'] == asset)
+            return utils.increment_to_precision(product['base_increment'])
+        except (KeyError, StopIteration):
+            return 8  # reasonable default for symbols that don't exist
diff --git a/blankly/exchanges/interfaces/ftx/ftx_interface.py b/blankly/exchanges/interfaces/ftx/ftx_interface.py
index 621e7227..69195137 100644
--- a/blankly/exchanges/interfaces/ftx/ftx_interface.py
+++ b/blankly/exchanges/interfaces/ftx/ftx_interface.py
@@ -16,7 +16,6 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-
 import pandas as pd
 import time
 from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
@@ -27,6 +26,9 @@
 import copy
 from typing import List
 
+from blankly.exchanges.orders.stop_loss import StopLossOrder
+from blankly.exchanges.orders.take_profit import TakeProfitOrder
+
 
 class FTXInterface(ExchangeInterface):
 
@@ -205,20 +207,15 @@ def market_order(self, symbol: str, side: str, size: float) -> MarketOrder:
             size: desired amount of base asset to use
         """
         needed = self.needed['market_order']
+        if self.should_auto_trunc:
+            size = utils.trunc(size, self.get_asset_precision(symbol))
 
+        # TODO pretty sure this needs to convert the symbol to an 'FTX' symbol before sending it off to the api
         response = self.get_calls().place_order(symbol, side, None, size, order_type="market")
 
-        response["symbol"] = utils.to_blankly_symbol(response.pop("market"), 'ftx')
-        response["created_at"] = utils.epoch_from_iso8601(response.pop("createdAt"))
-
-        response = utils.isolate_specific(needed, response)
+        response = self._fix_response(needed, response, False)
 
-        order = {
-            'size': size,
-            'side': side,
-            'symbol': symbol,
-            'type': 'market'
-        }
+        order = utils.build_order_info(0, side, size, symbol, 'market')
 
         return MarketOrder(order, response, self)
 
@@ -275,23 +272,70 @@ def limit_order(self,
             price: price to set limit order
             size: amount of asset (like BTC) for the limit to be valued
         """
-        needed = self.needed['limit_order']
+        needed = self.needed['take_profit']
         response = self.get_calls().place_order(symbol, side, price, size, order_type="limit")
+        if self.should_auto_trunc:
+            size = utils.trunc(size, self.get_asset_precision(symbol))
+        order = utils.build_order_info(price, side, size, symbol, 'take_profit')
 
-        order = {
-            'size': size,
-            'side': side,
-            'price': price,
-            'symbol': symbol,
-            'type': 'limit'
-        }
+        response = self._fix_response(needed, response)
+
+        return LimitOrder(order, response, self)
 
+    @staticmethod
+    def _fix_response(needed, response, set_tif=True):
         response["symbol"] = utils.to_blankly_symbol(response.pop("market"), 'ftx')
         response["created_at"] = utils.epoch_from_iso8601(response.pop("createdAt"))
-        response["time_in_force"] = "GTC"
+        if set_tif:
+            response["time_in_force"] = "GTC"
         response = utils.isolate_specific(needed, response)
+        return response
 
-        return LimitOrder(order, response, self)
+    @utils.order_protection
+    def stop_loss_order(self,
+                        symbol: str,
+                        price: float,
+                        size: float) -> StopLossOrder:
+        """
+        Used for placing a stop-loss order
+        Args:
+            symbol: asset to buy
+            price: price to sell at
+            size: amount of asset (like BTC)
+        """
+        needed = self.needed['stop_loss']
+        side = 'sell'
+        response = self.get_calls().place_conditional_order(symbol, side, size, order_type="stop",
+                                                            trigger_price=price)
+
+        order = utils.build_order_info(price, side, size, symbol, 'stop_loss')
+
+        response = self._fix_response(needed, response)
+
+        return StopLossOrder(order, response, self)
+
+    @utils.order_protection
+    def take_profit_order(self,
+                          symbol: str,
+                          price: float,
+                          size: float) -> TakeProfitOrder:
+        """
+        Used for placing a take-profit order
+        Args:
+            symbol: asset to buy
+            price: price to sell at
+            size: amount of asset (like BTC)
+        """
+        needed = self.needed['limit_order']
+        side = 'sell'
+        response = self.get_calls().place_conditional_order(symbol, side, size, order_type="takeProfit",
+                                                            trigger_price=price)
+
+        order = utils.build_order_info(price, side, size, symbol, 'take_profit')
+
+        response = self._fix_response(needed, response)
+
+        return TakeProfitOrder(order, response, self)
 
     def cancel_order(self, symbol: str, order_id: str) -> dict:
         """
@@ -534,7 +578,7 @@ def get_order_filter(self, symbol: str):
 
         return {
             "symbol": utils.to_blankly_symbol(market_info["name"], 'ftx'),  # make function in utils (or static)
-                                                                            # to switch slash to dash
+            # to switch slash to dash
             "base_asset": market_info["baseCurrency"],
             "quote_asset": market_info["quoteCurrency"],
             "max_orders": 99999999,
diff --git a/blankly/exchanges/interfaces/ftx/ftx_websocket_utils.py b/blankly/exchanges/interfaces/ftx/ftx_websocket_utils.py
index c6908297..1d54c168 100644
--- a/blankly/exchanges/interfaces/ftx/ftx_websocket_utils.py
+++ b/blankly/exchanges/interfaces/ftx/ftx_websocket_utils.py
@@ -20,6 +20,8 @@
 import blankly.utils.utils as utils
 from typing import List, Dict, Union, Any
 
+from blankly.exchanges.interfaces.coinbase_pro import coinbase_pro_websocket_utils
+
 
 def switch_type(stream):
     if stream == "trades":
@@ -46,19 +48,15 @@ def no_callback(message):
 
 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']
-              }
+        'trade_id': response.pop('id'),
+        'time': utils.epoch_from_iso8601(response['time']),
+        'size': response['size'],
+        'price': response['price']
+    }
 
     return output
 
 
-
 def trade(received):
-    line = str(received["time"]) + "," + str(time.time()) + "," + received["price"] + "," + received[
-        "open_24h"] + "," + received["volume_24h"] + "," + received["low_24h"] + "," + received[
-               "high_24h"] + "," + received["volume_30d"] + "," + received["best_bid"] + "," + received[
-               "best_ask"] + "," + received["last_size"] + "\n"
-    return line
+    # reuse impl from coinbase
+    return coinbase_pro_websocket_utils.trade(received)
diff --git a/blankly/exchanges/interfaces/ftx_futures/ftx_futures_interface.py b/blankly/exchanges/interfaces/ftx_futures/ftx_futures_interface.py
index 2192a276..2259dc2f 100644
--- a/blankly/exchanges/interfaces/ftx_futures/ftx_futures_interface.py
+++ b/blankly/exchanges/interfaces/ftx_futures/ftx_futures_interface.py
@@ -103,31 +103,32 @@ def get_products(self, filter: str = None) -> dict:
             if '-PERP' not in prod['name']:
                 continue  # only perpetual contracts for now
             symbol = prod['underlying'] + '-USD'
-            products[symbol] = utils.AttributeDict({
+            products[symbol] = {
                 'symbol': symbol,
                 'base_asset': prod['underlying'],
                 'quote_asset': 'USD',
                 'contract_type': ContractType.PERPETUAL,
-                'price_precision': self.increment_to_precision(
+                'price_precision': utils.increment_to_precision(
                     prod['priceIncrement']),
-                'size_precision': self.increment_to_precision(
+                'size_precision': utils.increment_to_precision(
                     prod['sizeIncrement']),
                 'exchange_specific': prod
-            })
+            }
         if filter:
             return products[filter]
         return products
 
-    def get_account(self, filter: str = None) -> utils.AttributeDict:
+    def get_account(self, filter: str = None) -> dict:
         balances = self.calls.get_balances()
         coins = self.calls.get_coins()
-        accounts = utils.AttributeDict({
-            coin['id']: utils.AttributeDict({
+        accounts = {
+            coin['id']: {
                 'available': 0.0,
+                'hold': 0.0,  # TODO
                 'exchange_specific': coin
-            })
+            }
             for coin in coins
-        })
+        }
 
         for bal in balances:
             coin = bal['coin']
@@ -142,7 +143,7 @@ def get_account(self, filter: str = None) -> utils.AttributeDict:
             return accounts[filter]
         return accounts
 
-    def get_positions(self, filter: str = None) -> Optional[dict]:
+    def get_position(self, filter: str = None) -> Optional[dict]:
         leverage = self.get_leverage()
 
         res = self.calls.get_positions()
@@ -152,17 +153,12 @@ def get_positions(self, filter: str = None) -> Optional[dict]:
             size = float(position['netSize'])
             if size == 0:
                 continue
-            positions[symbol] = utils.AttributeDict({
+            positions[symbol] = {
                 '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)
@@ -211,7 +207,7 @@ def limit_order(
                                      ioc=ioc)
         return self.parse_order_response(res)
 
-    def take_profit(
+    def take_profit_order(
             self,
             symbol: str,
             side: Side,
@@ -229,12 +225,12 @@ def take_profit(
                                                  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:
+    def stop_loss_order(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(
@@ -256,7 +252,7 @@ def get_hedge_mode(self):
         return HedgeMode.ONEWAY
 
     @utils.order_protection
-    def set_leverage(self, leverage: int, symbol: str = None):
+    def set_leverage(self, leverage: float, symbol: str = None):
         if symbol:
             raise Exception(
                 'FTX Futures does not allow setting leverage per symbol. Use interface.set_leverage(leverage) to set '
@@ -306,9 +302,9 @@ def get_funding_rate_history(self, symbol: str, epoch_start: int,
         # TODO dedup binance_futures_exchange maybe?
         history = []
         resolution = self.get_funding_rate_resolution()
-        LIMIT = 500
+        limit = 500
         window_start = epoch_start
-        window_end = epoch_start + LIMIT * resolution
+        window_end = epoch_start + limit * resolution
 
         response = True
         while response:
@@ -316,13 +312,13 @@ def get_funding_rate_history(self, symbol: str, epoch_start: int,
                                                     symbol)
 
             history.extend({
-                'rate': float(e['rate']),
-                'time': self.parse_timestamp(e['time'])
-            } for e in response)
+                               '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)
+                window_end = min(epoch_stop, window_start + limit * resolution)
 
         return sorted(history, key=operator.itemgetter('time'))
 
@@ -332,4 +328,11 @@ def get_funding_rate_resolution(self) -> int:
     def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
         raise NotImplementedError
 
+    def get_maker_fee(self) -> float:
+        raise NotImplementedError
 
+    def get_taker_fee(self) -> float:
+        raise NotImplementedError
+
+    def get_funding_rate(self, symbol: str) -> float:
+        raise NotImplementedError
diff --git a/blankly/exchanges/interfaces/futures_exchange_interface.py b/blankly/exchanges/interfaces/futures_exchange_interface.py
index 4c95806f..1f0e8d6f 100644
--- a/blankly/exchanges/interfaces/futures_exchange_interface.py
+++ b/blankly/exchanges/interfaces/futures_exchange_interface.py
@@ -20,13 +20,6 @@
 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
@@ -52,14 +45,12 @@ def __init__(self,
             self.init_exchange()
 
     @staticmethod
-    @abc.abstractmethod
     def to_blankly_symbol(symbol: str):
-        pass
+        return symbol
 
     @staticmethod
-    @abc.abstractmethod
     def to_exchange_symbol(symbol: str):
-        pass
+        return symbol
 
     def get_exchange_type(self) -> str:
         """Returns the exchange type (ex. 'binance', 'coinbase', 'alpaca')"""
@@ -76,13 +67,13 @@ def get_products(self, symbol: str = None) -> dict:
         pass
 
     @abc.abstractmethod
-    def get_account(self, symbol: str = None) -> utils.AttributeDict:
+    def get_account(self, symbol: str = None) -> dict:
         """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]:
+    def get_position(self, symbol: str = None) -> Optional[dict]:
         """Returns position information, or information for only one `symbol` if one is given"""
         pass
 
@@ -109,22 +100,22 @@ def limit_order(self,
         pass
 
     @abc.abstractmethod
-    def take_profit(self,
-                    symbol: str,
-                    side: Side,
-                    price: float,
-                    size: float,
-                    position: PositionMode = None) -> FuturesOrder:
+    def take_profit_order(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:
+    def stop_loss_order(self,
+                        symbol: str,
+                        side: Side,
+                        price: float,
+                        size: float,
+                        position: PositionMode = None) -> FuturesOrder:
         """Place a stop-loss order for a position"""
         pass
 
@@ -137,7 +128,7 @@ def get_hedge_mode(self):
         pass
 
     @abc.abstractmethod
-    def set_leverage(self, leverage: int, symbol: str = None):
+    def set_leverage(self, leverage: float, symbol: str = None):
         pass
 
     @abc.abstractmethod
@@ -173,14 +164,14 @@ def get_price(self, symbol: str) -> float:
         pass
 
     @property
-    def account(self) -> utils.AttributeDict:
+    def account(self) -> dict:
         """Account information"""
         return self.get_account()
 
     @property
-    def positions(self) -> utils.AttributeDict:
+    def positions(self) -> dict:
         """Position information"""
-        return self.get_positions()
+        return self.get_position()
 
     @property
     def orders(self) -> list:
@@ -203,6 +194,22 @@ def get_funding_rate_history(self, symbol: str, epoch_start: int,
         """
         pass
 
+    @abc.abstractmethod
+    def get_funding_rate(self, symbol: str) -> float:
+        pass
+
+    @abc.abstractmethod
+    def get_maker_fee(self) -> float:
+        pass
+
+    @abc.abstractmethod
+    def get_taker_fee(self) -> float:
+        pass
+
     @abc.abstractmethod
     def get_funding_rate_resolution(self) -> int:
         pass
+
+    @property
+    def should_auto_trunc(self):
+        return self.user_preferences['settings'].get('auto_truncate', False)
diff --git a/blankly/exchanges/interfaces/keyless/keyless.py b/blankly/exchanges/interfaces/keyless/keyless.py
index f4d6548f..4476d889 100644
--- a/blankly/exchanges/interfaces/keyless/keyless.py
+++ b/blankly/exchanges/interfaces/keyless/keyless.py
@@ -26,12 +26,12 @@
 
 class KeylessExchange(Exchange):
     def __init__(self, maker_fee=0, taker_fee=0, portfolio_name=None, settings_path=None,
-                 price_reader: [PriceReader, list] = None):
+                 price_reader: [PriceReader, list] = None, initial_account_values: dict = None):
         Exchange.__init__(self, "keyless", portfolio_name, settings_path)
 
         self.calls = KeylessAPI(maker_fee, taker_fee, price_reader)
 
-        self.interface = PaperTradeInterface(self.calls)
+        self.interface = PaperTradeInterface(self.calls, initial_account_values)
 
         # This one must be exported manually
         blankly.reporter.export_used_exchange("keyless")
diff --git a/blankly/exchanges/interfaces/keyless/keyless_api.py b/blankly/exchanges/interfaces/keyless/keyless_api.py
index 16c7ae9e..954ff60e 100644
--- a/blankly/exchanges/interfaces/keyless/keyless_api.py
+++ b/blankly/exchanges/interfaces/keyless/keyless_api.py
@@ -19,6 +19,7 @@
 
 
 from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
+from blankly.exchanges.orders.limit_order import LimitOrder
 from blankly.utils.utils import AttributeDict, get_base_asset, get_quote_asset, aggregate_prices_by_resolution, \
     extract_price_by_resolution
 
@@ -79,6 +80,12 @@ def get_products(self):
     def market_order(self, symbol: str, side: str, size: float):
         self.__invalid_live()
 
+    def take_profit_order(self, symbol: str, price: float, size: float):
+        self.__invalid_live()
+
+    def stop_loss_order(self, symbol: str, price: float, size: float):
+        self.__invalid_live()
+
     def limit_order(self, symbol: str, side: str, price: float, size: float):
         self.__invalid_live()
 
@@ -133,3 +140,5 @@ def get_order_filter(self, symbol: str) -> dict:
 
     def get_price(self, symbol: str, time=None):
         self.__invalid_live()
+
+
diff --git a/blankly/exchanges/interfaces/kucoin/kucoin_interface.py b/blankly/exchanges/interfaces/kucoin/kucoin_interface.py
index 91ed8a74..ea486b65 100644
--- a/blankly/exchanges/interfaces/kucoin/kucoin_interface.py
+++ b/blankly/exchanges/interfaces/kucoin/kucoin_interface.py
@@ -25,6 +25,8 @@
 from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
 from blankly.exchanges.orders.limit_order import LimitOrder
 from blankly.exchanges.orders.market_order import MarketOrder
+from blankly.exchanges.orders.stop_loss import StopLossOrder
+from blankly.exchanges.orders.take_profit import TakeProfitOrder
 from blankly.utils.exceptions import APIException, InvalidOrder
 
 
@@ -44,7 +46,6 @@ 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:
@@ -184,57 +185,12 @@ def market_order(self, symbol, side, size) -> MarketOrder:
             "orderId": "5bd6e9286d99522a52e458de"
         }
         """
-        order = {
-            'symbol': symbol,
-            'side': side,
-            'size': size,
-            'type': 'market',
-        }
+        if self.should_auto_trunc:
+            size = utils.trunc(size, self.get_asset_precision(symbol))
+        order = utils.build_order_info(0, side, size, symbol, 'market')
 
         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"])
-
-        response_details = self._trade.get_order_details(response['orderId'])
-        """
-        {
-            "id": "5c35c02703aa673ceec2a168", //
-            "symbol": "BTC-USDT", //
-            "opType": "DEAL",
-            "type": "limit", //
-            "side": "buy", //
-            "price": "10",
-            "size": "2", //
-            "funds": "0",
-            "dealFunds": "0.166",
-            "dealSize": "2",
-            "fee": "0",
-            "feeCurrency": "USDT",
-            "stp": "",
-            "stop": "",
-            "stopTriggered": false,
-            "stopPrice": "0",
-            "timeInForce": "GTC",
-            "postOnly": false,
-            "hidden": false,
-            "iceberg": false,
-            "visibleSize": "0",
-            "cancelAfter": 0,
-            "channel": "IOS",
-            "clientOid": "",
-            "remark": "",
-            "tags": "",
-            "isActive": false, //
-            "cancelExist": false,
-            "createdAt": 1547026471000, //
-            "tradeType": "TRADE"
-        }
-        """
-
-        response_details["created_at"] = response_details.pop('createdAt')
-        response_details["status"] = response_details.pop('isActive')
-        response_details = utils.isolate_specific(needed, response_details)
+        response_details = self._fetch_response_details(needed, response)
         return MarketOrder(order, response_details, self)
 
     @utils.order_protection
@@ -249,64 +205,59 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
         """
         needed = self.needed['limit_order']
 
-        order = {
-            'symbol': symbol,
-            'side': side,
-            'size': size,
-            'price': price,
-            'type': 'limit',
-        }
+        if self.should_auto_trunc:
+            size = utils.trunc(size, self.get_asset_precision(symbol))
+        order = utils.build_order_info(price, side, size, symbol, 'limit')
         response = self._trade.create_limit_order(symbol, side, size, price)
+        response_details = self._fetch_response_details(needed, response)
+        return LimitOrder(order, response_details, self)
+
+    def _fetch_response_details(self, needed, response):
         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", //
-                   "symbol": "BTC-USDT", //
-                   "opType": "DEAL",
-                   "type": "limit", //
-                   "side": "buy", //
-                   "price": "10",
-                   "size": "2", //
-                   "funds": "0",
-                   "dealFunds": "0.166",
-                   "dealSize": "2",
-                   "fee": "0",
-                   "feeCurrency": "USDT",
-                   "stp": "",
-                   "stop": "",
-                   "stopTriggered": false,
-                   "stopPrice": "0",
-                   "timeInForce": "GTC", //
-                   "postOnly": false,
-                   "hidden": false,
-                   "iceberg": false,
-                   "visibleSize": "0",
-                   "cancelAfter": 0,
-                   "channel": "IOS",
-                   "clientOid": "",
-                   "remark": "",
-                   "tags": "",
-                   "isActive": false, //
-                   "cancelExist": false,
-                   "createdAt": 1547026471000, //
-                   "tradeType": "TRADE"
-               }
-               """
-
         response_details["created_at"] = response_details.pop('createdAt')
-        response_details["time_in_force"] = response_details.pop('timeInForce')
-        response_details["status"] = response_details.pop('isActive')
-        if response_details["status"]:
-            response_details["status"] = 'pending'
-        else:
-            response_details["status"] = 'done'
+        if 'timeInForce' in response_details:
+            response_details["time_in_force"] = response_details.pop('timeInForce')
+        response_details["status"] = 'pending' if response_details.pop('isActive') else 'done'
         response_details = utils.isolate_specific(needed, response_details)
-        return LimitOrder(order, response_details, self)
+        return response_details
+
+    @utils.order_protection
+    def stop_loss_order(self, symbol, price, size) -> StopLossOrder:
+        """
+            Used for take profit order
+            Args:
+                symbol: currency to buy
+                price: price to set limit order
+                size: amount of currency (like BTC)
+        """
+        needed = self.needed['stop_loss']
+
+        side = 'sell'
+        order = utils.build_order_info(price, side, size, symbol, 'stop_loss')
+        response = self._trade.create_market_stop_order(symbol, side, price, size, stop='loss')
+        response_details = self._fetch_response_details(needed, response)
+        return StopLossOrder(order, response_details, self)
+
+    @utils.order_protection
+    def take_profit_order(self, symbol, price, size) -> TakeProfitOrder:
+        """
+            Used for take profit order
+            Args:
+                symbol: currency to buy
+                price: price to set limit order
+                size: amount of currency (like BTC)
+        """
+        needed = self.needed['take_profit']
+
+        side = 'sell'
+        order = utils.build_order_info(price, side, size, symbol, 'take_profit')
+        response = self._trade.create_market_stop_order(symbol, side, price, size, stop='entry')
+        response_details = self._fetch_response_details(needed, response)
+        return TakeProfitOrder(order, response_details, self)
 
     def cancel_order(self, symbol, order_id) -> dict:
         """
@@ -365,7 +316,8 @@ def get_open_orders(self, symbol=None):
         if symbol is None:
             open_orders = list(self.__correct_api_call(self._trade.get_order_list(status='active')["items"]))
         else:
-            open_orders = list(self.__correct_api_call(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 []
@@ -564,7 +516,7 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
 
         try:
             epoch_start += resolution
-            df = (df.iloc[int(-(epoch_stop-epoch_start)/resolution):]).reset_index(drop=True)
+            df = (df.iloc[int(-(epoch_stop - epoch_start) / resolution):]).reset_index(drop=True)
         except Exception:
             pass
 
@@ -667,4 +619,4 @@ def get_price(self, symbol) -> float:
         response = self.__correct_api_call(response)
         if 'msg' in response:
             raise APIException("Error: " + response['msg'])
-        return float(response['price'])
\ No newline at end of file
+        return float(response['price'])
diff --git a/blankly/exchanges/interfaces/oanda/oanda_api.py b/blankly/exchanges/interfaces/oanda/oanda_api.py
index 165245e6..537dec04 100644
--- a/blankly/exchanges/interfaces/oanda/oanda_api.py
+++ b/blankly/exchanges/interfaces/oanda/oanda_api.py
@@ -242,6 +242,42 @@ def place_limit_order(self, instrument: str, units: float, price: float, account
 
         return self._send_request('post', self.__api_url + endpoint, data=data)
 
+    def place_stop_loss(self, instrument: str, units: float, price: float, accountid: str = None):
+        if accountid is None:
+            accountid = self.default_account
+        assert isinstance(accountid, str)
+        endpoint = f'/v3/accounts/{accountid}/orders'
+
+        order_request = {
+            "type": "STOP",
+            "instrument": instrument,
+            "units": units,
+            "price": price,
+            "timeInForce": "GTC"
+        }
+        data = OrderedDict()
+        data["order"] = order_request
+
+        return self._send_request('post', self.__api_url + endpoint, data=data)
+
+    def place_take_profit(self, instrument: str, units: float, price: float, accountid: str = None):
+        if accountid is None:
+            accountid = self.default_account
+        assert isinstance(accountid, str)
+        endpoint = f'/v3/accounts/{accountid}/orders'
+
+        order_request = {
+            "type": "TAKE_PROFIT",
+            "instrument": instrument,
+            "units": units,
+            "price": price,
+            "timeInForce": "GTC"
+        }
+        data = OrderedDict()
+        data["order"] = order_request
+
+        return self._send_request('post', self.__api_url + endpoint, data=data)
+
     """
     Trade Endpoints
     """
diff --git a/blankly/exchanges/interfaces/oanda/oanda_interface.py b/blankly/exchanges/interfaces/oanda/oanda_interface.py
index e513ee54..2ae710c3 100644
--- a/blankly/exchanges/interfaces/oanda/oanda_interface.py
+++ b/blankly/exchanges/interfaces/oanda/oanda_interface.py
@@ -17,7 +17,6 @@
 """
 
 import time
-import dateparser as dp
 from datetime import datetime as dt
 from typing import Union
 
@@ -27,7 +26,9 @@
 from blankly.exchanges.interfaces.oanda.oanda_api import OandaAPI
 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.exchanges.orders.stop_loss import StopLossOrder
+from blankly.exchanges.orders.take_profit import TakeProfitOrder
+from blankly.utils import utils, time_builder
 from blankly.utils.exceptions import APIException, InvalidOrder
 
 
@@ -210,6 +211,9 @@ def cash():
     def market_order(self, symbol: str, side: str, size: float) -> MarketOrder:
         symbol = self.__convert_blankly_to_oanda(symbol)
 
+        if self.should_auto_trunc:
+            size = utils.trunc(size, self.get_asset_precision(symbol))
+
         # Make sure that default trunc has been established - init may have been skipped
         if self.default_trunc is None:
             self.init_exchange()
@@ -229,25 +233,36 @@ def market_order(self, symbol: str, side: str, size: float) -> MarketOrder:
         resp = self.calls.place_market_order(symbol, qty_to_buy)
 
         needed = self.needed['market_order']
-        order = {
-            'size': qty_to_buy,
-            'side': side,
-            'symbol': symbol,
-            'type': 'market'
-        }
-        if 'orderRejectTransaction' in resp:
-            raise APIException(resp['errorMessage'])
-        resp['symbol'] = self.__convert_symbol_to_blankly(resp['orderCreateTransaction']['instrument'])
-        resp['id'] = resp['orderCreateTransaction']['id']
-        resp['created_at'] = resp['orderCreateTransaction']['time']
-        resp['size'] = abs(qty_to_buy)
-        resp['status'] = "active"
-        resp['type'] = 'market'
-        resp['side'] = side
-
-        resp = utils.isolate_specific(needed, resp)
+        order = utils.build_order_info(0, side, qty_to_buy, symbol, 'market')
+        resp = self._fix_response(needed, 0, resp, side, qty_to_buy, 'market', False)
         return MarketOrder(order, resp, self)
 
+    @utils.order_protection
+    def stop_loss_order(self, symbol: str, price: float, size: float) -> StopLossOrder:
+        symbol = self.__convert_blankly_to_oanda(symbol)
+        side = 'sell'
+        size *= -1
+
+        resp = self.calls.place_stop_loss(symbol, size, price)
+        needed = self.needed['stop_loss']
+        order = utils.build_order_info(0, side, size, symbol, 'stop_loss')
+
+        resp = self._fix_response(needed, price, resp, side, size, 'stop_loss')
+        return StopLossOrder(order, resp, self)
+
+    @utils.order_protection
+    def take_profit_order(self, symbol: str, price: float, size: float) -> TakeProfitOrder:
+        symbol = self.__convert_blankly_to_oanda(symbol)
+        side = 'sell'
+        size *= -1
+
+        resp = self.calls.place_take_profit(symbol, size, price)
+        needed = self.needed['take_profit']
+        order = utils.build_order_info(0, side, size, symbol, 'take_profit')
+
+        resp = self._fix_response(needed, price, resp, side, size, 'take_profit')
+        return TakeProfitOrder(order, resp, self)
+
     @utils.order_protection
     def limit_order(self, symbol: str, side: str, price: float, size: float) -> LimitOrder:
         symbol = self.__convert_blankly_to_oanda(symbol)
@@ -260,14 +275,12 @@ def limit_order(self, symbol: str, side: str, price: float, size: float) -> Limi
 
         resp = self.calls.place_limit_order(symbol, size, price)
         needed = self.needed['limit_order']
-        order = {
-            'size': size,
-            'side': side,
-            'price': price,
-            'symbol': symbol,
-            'type': 'limit'
-        }
+        order = utils.build_order_info(0, side, size, symbol, 'limit')
+
+        resp = self._fix_response(needed, price, resp, side, size, 'limit')
+        return LimitOrder(order, resp, self)
 
+    def _fix_response(self, needed, price, resp, side, size, type, set_tif=True):
         try:
             resp['symbol'] = self.__convert_symbol_to_blankly(resp['orderCreateTransaction']['instrument'])
         except KeyError as e:
@@ -277,15 +290,16 @@ def limit_order(self, symbol: str, side: str, price: float, size: float) -> Limi
                 raise e
         resp['id'] = resp['orderCreateTransaction']['id']
         resp['created_at'] = resp['orderCreateTransaction']['time']
-        resp['price'] = price
+        if price:
+            resp['price'] = price
         resp['size'] = abs(size)
         resp['status'] = "active"
-        resp['time_in_force'] = 'GTC'
-        resp['type'] = 'limit'
+        if set_tif:
+            resp['time_in_force'] = 'GTC'
+        resp['type'] = type
         resp['side'] = side
-
         resp = utils.isolate_specific(needed, resp)
-        return LimitOrder(order, resp, self)
+        return resp
 
     def cancel_order(self, symbol, order_id) -> dict:
         # Either the Order’s OANDA-assigned OrderID or the Order’s client-provided ClientID prefixed by the “@” symbol
@@ -347,7 +361,7 @@ def overridden_history(self, symbol, epoch_start, epoch_stop, resolution, **kwar
     def get_product_history(self, symbol: str, epoch_start: float, epoch_stop: float, resolution: int):
         symbol = self.__convert_blankly_to_oanda(symbol)
 
-        resolution = int(utils.time_interval_to_seconds(resolution))
+        resolution = int(time_builder.time_interval_to_seconds(resolution))
 
         if resolution not in self.multiples_keys:
             utils.info_print("Granularity is not an accepted granularity...rounding to nearest valid value.")
@@ -425,27 +439,18 @@ def get_price(self, symbol: str) -> float:
         return float(resp['orderBook']['price'])
 
     def homogenize_order(self, order):
-        def add_details(order_: dict):
-            if float(order_['units']) < 0:
-                order_['side'] = 'sell'
-            else:
-                order_['side'] = 'buy'
-            order_['time_in_force'] = 'GTC'
-            renames_ = [['instrument', 'symbol'],
-                        ['createTime', 'created_at'],
-                        ['state', 'status'],  # TODO: handle status
-                        ['units', 'size']]
-            return utils.rename_to(renames_, order_)
-        if order['type'] == "MARKET":
-            order['type'] = 'market'
-            order = add_details(order)
-
-        elif order['type'] == "LIMIT":
-            order['type'] = 'limit'
-            order = add_details(order)
+        order['type'] = order['type'].lower()
+
+        if float(order['units']) < 0:
+            order['side'] = 'sell'
         else:
-            # TODO: handle other order types
-            pass
+            order['side'] = 'buy'
+        order['time_in_force'] = 'GTC'
+        renames = [['instrument', 'symbol'],
+                    ['createTime', 'created_at'],
+                    ['state', 'status'],  # TODO: handle status
+                    ['units', 'size']]
+        order = utils.rename_to(renames, order)
 
         needed = self.choose_order_specificity(order['type'])
         order = utils.isolate_specific(needed, order)
@@ -476,6 +481,7 @@ def _handle_input_time_conv(date: Union[str, dt, float] = None) -> float:
         if isinstance(date, float):
             pass
         elif isinstance(date, str):
+            import dateparser as dp
             date = dp.parse(date).timestamp()
         elif isinstance(date, dt):
             date = date.timestamp()
diff --git a/blankly/exchanges/interfaces/okx/okx.py b/blankly/exchanges/interfaces/okx/okx.py
index d496c9d1..176694b1 100644
--- a/blankly/exchanges/interfaces/okx/okx.py
+++ b/blankly/exchanges/interfaces/okx/okx.py
@@ -16,15 +16,18 @@ def __init__(self, portfolio_name=None, keys_path="keys.json", settings_path=Non
 
         # if self.preferences["settings"]["use_sandbox"]:
         calls = {
-            'market': MarketAPI(api_key=keys['API_KEY'], api_secret_key=keys['API_SECRET'],passphrase=keys['API_PASS'],
+            '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'],
+            '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'],
+            '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'],
+            '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)
diff --git a/blankly/exchanges/interfaces/okx/okx_api.py b/blankly/exchanges/interfaces/okx/okx_api.py
index e038b193..194d72a3 100644
--- a/blankly/exchanges/interfaces/okx/okx_api.py
+++ b/blankly/exchanges/interfaces/okx/okx_api.py
@@ -286,28 +286,17 @@ def _request(self, method, request_path, params):
 
         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)
 
@@ -685,6 +674,7 @@ 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'):
@@ -701,29 +691,29 @@ def get_balances(self, 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=''):
+    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}
+                  'toInstId': toInstId, 'loanTrans': loanTrans}
         return self._request_with_params(POST, FUNDS_TRANSFER, params)
 
     # Get Transfer State
-    def transfer_state(self, transId,type=''):
+    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}
+    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}
+    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}
+    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
@@ -731,8 +721,8 @@ 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}
+    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
@@ -740,54 +730,50 @@ 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
+    # Get Piggy Balance
     def get_piggy_balance(self, ccy=''):
         params = {}
         if ccy:
-            params = {'ccy':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}
+    # Get Deposit Lightning
+    def get_deposit_lightning(self, ccy, amt, to=""):
+        params = {'ccy': ccy, 'amt': amt}
         if to:
-            params = {'to':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}
+    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}
+        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}
+    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,}
+    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,}
+    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)
+    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):
 
@@ -873,4 +859,4 @@ def get_underlying(self, instType):
     # 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
+        return self._request_with_params(GET, VIP_INTEREST_RATE_LOAN_QUOTA, params)
diff --git a/blankly/exchanges/interfaces/okx/okx_interface.py b/blankly/exchanges/interfaces/okx/okx_interface.py
index cb235e7d..10297759 100644
--- a/blankly/exchanges/interfaces/okx/okx_interface.py
+++ b/blankly/exchanges/interfaces/okx/okx_interface.py
@@ -6,6 +6,7 @@
 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.exchanges.orders.take_profit import TakeProfitOrder
 from blankly.utils.exceptions import APIException, InvalidOrder
 
 
@@ -47,7 +48,6 @@ def get_products(self):
             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
@@ -61,7 +61,7 @@ def get_account(self, symbol=None) -> utils.AttributeDict:
         accounts = self._account.get_account()
 
         parsed_dictionary = utils.AttributeDict({})
-        #We have to sort through it if the accounts are none
+        # 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:
@@ -69,7 +69,8 @@ def get_account(self, symbol=None) -> utils.AttributeDict:
             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'],
+                    '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
@@ -101,53 +102,37 @@ def market_order(self, symbol, side, size) -> MarketOrder:
                 "error_code": "0"
             }
         """
-        order = {
-            'symbol': symbol,
-            'size': size,
-            'side': side,
-            'type': 'market'
-        }
+        if self.should_auto_trunc:
+            size = utils.trunc(size, self.get_asset_precision(symbol))
+        order = utils.build_order_info(0, side, size, symbol, '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)
-
+        response_details = self._fetch_response_details(needed, response, symbol, False)
+        return MarketOrder(order, response_details, self)
 
     @utils.order_protection
-    def limit_order(self, symbol, side, price, size) -> LimitOrder:
+    def stop_loss_order(self, symbol, price, size) -> TakeProfitOrder:
         """
-           Used for buying or selling limit orders
+           Used for stop-loss 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
+               size: amount of currency (like BTC)
         """
-        needed = self.needed['limit_order']
+        needed = self.needed['stop_loss']
 
-        order = {
-            'symbol': symbol,
-            'side': side,
-            'size': size,
-            'price': price,
-            'type': 'limit',
-        }
+        side = 'sell'
+        order = utils.build_order_info(price, side, size, symbol, 'stop_loss')
 
-        response = self._trade.place_order(symbol, 'cash', side, 'limit', size, px=price)
+        response = self._trade.place_algo_order(symbol, 'cash', side, 'conditional', sz=size, slTriggerPx=price,
+                                                slOrdPx=-1)
+        response_details = self._fetch_response_details(needed, response, symbol)
+        return TakeProfitOrder(order, response_details, self)
+
+    def _fetch_response_details(self, needed, response, symbol, set_tif=True):
         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']
@@ -156,9 +141,49 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
         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'
-
+        if set_tif:
+            response_details["time_in_force"] = 'GTC'
         response_details = utils.isolate_specific(needed, response_details)
+        return response_details
+
+    @utils.order_protection
+    def take_profit_order(self, symbol, price, size) -> TakeProfitOrder:
+        """
+           Used for take-profit orders
+           Args:
+               symbol: currency to buy
+               price: price to set limit order
+               size: amount of currency (like BTC)
+        """
+        needed = self.needed['take_profit']
+
+        side = 'sell'
+        order = utils.build_order_info(price, side, size, symbol, 'take_profit')
+
+        response = self._trade.place_algo_order(symbol, 'cash', side, 'conditional', sz=size, tpTriggerPx=price,
+                                                tpOrdPx=-1)
+        response_details = self._fetch_response_details(needed, response, symbol)
+        return TakeProfitOrder(order, response_details, self)
+
+    @utils.order_protection
+    def limit_order(self, symbol, side, price, size) -> LimitOrder:
+        """
+           Used for buying or selling limit orders
+           Args:
+               symbol: currency to buy
+               side: buy/sell
+               price: price to set limit order
+               size: amount of currency (like BTC) for the limit to be valued
+        """
+        needed = self.needed['limit_order']
+        if self.should_auto_trunc:
+            result = utils.trunc(size, self.get_asset_precision(symbol))
+            if result != 0:
+                size = result
+        order = utils.build_order_info(price, side, size, symbol, 'limit')
+
+        response = self._trade.place_order(symbol, 'cash', side, 'limit', size, px=price)
+        response_details = self._fetch_response_details(needed, response, symbol)
         return LimitOrder(order, response_details, self)
 
     def cancel_order(self, symbol: str, order_id: str) -> dict:
@@ -292,7 +317,8 @@ def get_product_history(self, symbol, epoch_start, epoch_stop, 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]
+        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.")
@@ -356,10 +382,13 @@ def get_order_filter(self, symbol: str) -> dict:
         base_min_size = float(products.pop('minSz'))
         base_increment = float(products.pop('tickSz'))
 
+        base = products.pop('baseCcy')
+        quote = products.pop('quoteCcy')
+
         return {
-            "symbol": products.pop('uly'),
-            "base_asset": products.pop('baseCcy'),
-            "quote_asset": products.pop('quoteCcy'),
+            "symbol": base + '-' + quote,
+            "base_asset": base,
+            "quote_asset": quote,
             "max_orders": 1000000000000,
             "limit_order": {
                 "base_min_size": base_min_size,  # Minimum size to buy
@@ -398,5 +427,3 @@ def get_price(self, symbol) -> float:
         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
index c4d406d3..f788b531 100644
--- a/blankly/exchanges/interfaces/okx/okx_websocket.py
+++ b/blankly/exchanges/interfaces/okx/okx_websocket.py
@@ -26,7 +26,7 @@
 
 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",
+                 pre_event_callback=None, initially_stopped=False, websocket_url="wss://ws.okx.com:8443/ws/v5/public",
                  **kwargs):
         """
         Create and initialize the ticker
@@ -37,7 +37,7 @@ def __init__(self, symbol, stream, log=None,
         """
         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)
+        super().__init__(symbol, stream, log, log_message, websocket_url, pre_event_callback, kwargs)
 
         self.__pre_event_callback_filled = False
 
@@ -81,7 +81,7 @@ def on_message(self, ws, message):
         # Manage price events and fire for each manager attached
         if self.stream == "books":
             interface_message = self.__interface_callback(received_dict)
-        else: # self.stream == 'tickers':
+        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
@@ -108,8 +108,8 @@ def on_open(self, ws):
             'args': [{
                 'channel': self.stream,
                 'instId': self.symbol,
-                }]
-            })
+            }]
+        })
         ws.send(request)
 
     def restart_ticker(self):
diff --git a/blankly/exchanges/interfaces/paper_trade/futures/__init__.py b/blankly/exchanges/interfaces/paper_trade/futures/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/blankly/exchanges/interfaces/paper_trade/futures/futures_paper_trade.py b/blankly/exchanges/interfaces/paper_trade/futures/futures_paper_trade.py
new file mode 100644
index 00000000..72e8a73f
--- /dev/null
+++ b/blankly/exchanges/interfaces/paper_trade/futures/futures_paper_trade.py
@@ -0,0 +1,25 @@
+from blankly.exchanges.futures.futures_exchange import FuturesExchange
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+from blankly.exchanges.interfaces.paper_trade.backtesting_wrapper import BacktestingWrapper
+from blankly.exchanges.interfaces.paper_trade.futures.futures_paper_trade_interface import FuturesPaperTradeInterface
+
+
+class FuturesPaperTrade(FuturesExchange):
+    def __init__(self,
+                 exchange,
+                 portfolio_name: str = None,
+                 preferences_path: str = None,
+                 initial_account_values: dict = None):
+        super().__init__("futures_paper_trade", portfolio_name, preferences_path)
+
+        self.exchange = exchange
+        self._interface = FuturesPaperTradeInterface(exchange.get_type(), self.exchange.interface,
+                                                     initial_account_values)
+
+    @property
+    def calls(self):
+        return self.exchange.calls
+
+    @property
+    def interface(self) -> FuturesExchangeInterface:
+        return self._interface
diff --git a/blankly/exchanges/interfaces/paper_trade/futures/futures_paper_trade_interface.py b/blankly/exchanges/interfaces/paper_trade/futures/futures_paper_trade_interface.py
new file mode 100644
index 00000000..83a03428
--- /dev/null
+++ b/blankly/exchanges/interfaces/paper_trade/futures/futures_paper_trade_interface.py
@@ -0,0 +1,445 @@
+import dataclasses
+import functools
+import random
+from collections import defaultdict
+from typing import List, Optional
+
+from blankly.enums import MarginType, HedgeMode, PositionMode, Side, TimeInForce, ContractType, OrderStatus, OrderType
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+from blankly.exchanges.interfaces.paper_trade.backtesting_wrapper import BacktestingWrapper
+from blankly.exchanges.orders.futures.futures_order import FuturesOrder
+from blankly.utils import utils as utils
+from copy import deepcopy
+
+from blankly.utils.exceptions import InvalidOrder, BacktestingException
+
+
+class FuturesPaperTradeInterface(FuturesExchangeInterface, BacktestingWrapper):
+    interface: FuturesExchangeInterface
+    paper_account: dict
+    paper_positions: dict
+    next_fund: int
+
+    _funding_rate_cache: list
+
+    # mapping of contract names -> quote currency used
+    # BTC-PERP -> USDT/USD
+    _quote_map: dict
+
+    # orders
+    _placed_orders: dict
+    _executed_orders: dict
+    _canceled_orders: dict
+    _funds_held: dict
+
+    # not used, just for funsies
+    margin_types: dict
+
+    # needed for backtesting_wrapper:
+    traded_assets: list
+
+    @property
+    def paper_trade_orders(self) -> list:
+        return []
+
+    @property
+    def executed_orders(self) -> list:
+        return []
+
+    @property
+    def canceled_orders(self) -> list:
+        return []
+
+    @property
+    def market_order_execution_details(self) -> list:
+        return []
+
+    def add_asset(self, asset):
+        if asset not in self.traded_assets:
+            self.traded_assets.append(asset)
+
+    @staticmethod
+    def convert_symbol_name(symbol: str):
+        base, quote = symbol.split('-')
+        return base + '-PERP', quote
+
+    def add_symbol(self, symbol):
+        contract_name, quote = self.convert_symbol_name(symbol)
+        self._quote_map[contract_name] = quote
+        self.add_asset(contract_name)
+        self.add_asset(quote)
+
+    def __init__(self, exchange_name: str, interface: FuturesExchangeInterface, account_values: dict = None):
+        self.interface = interface
+        super().__init__(exchange_name, interface.calls)
+        BacktestingWrapper.__init__(self)
+        self.next_fund = 0
+
+        self._placed_orders = {}
+        self._executed_orders = {}
+        self._canceled_orders = {}
+        self._funds_held = {}
+        self._quote_map = {}
+        self._funding_rate_cache = []
+
+        # same functionality as 'real' FuturesExchangeInterface's
+        self.paper_account = defaultdict(lambda: {'available': 0, 'hold': 0})
+        self.paper_positions = {}
+        self.traded_assets = []
+
+        self.margin_type = {}
+        self.margin_type_global_default = MarginType.CROSSED
+        self.leverage = {}
+        self.leverage_global_default = 1
+
+        self.init_exchange()
+        self.override_local_account(account_values or {})
+
+    def init_exchange(self):
+        self.interface.init_exchange()
+
+    @functools.lru_cache(None)
+    def get_products(self, symbol: str = None) -> dict:
+        return self.interface.get_products(symbol)
+
+    def get_account(self, symbol: str = None) -> dict:
+        acc = deepcopy(self.paper_account)
+        if symbol:
+            try:
+                return acc[symbol]
+            except KeyError as e:
+                if self.backtesting:
+                    raise KeyError(
+                        "Symbol not found. This can be caused by an invalid quote currency in backtest.json.", e)
+                else:
+                    raise KeyError("Symbol not found.", e)
+        return acc
+
+    def get_position(self, symbol: str = None) -> Optional[dict]:
+        if symbol:
+            return self.paper_positions.get(symbol, None)
+        return self.paper_positions
+
+    def market_order(self, symbol: str, side: Side, size: float, position: PositionMode = PositionMode.BOTH,
+                     reduce_only: bool = False) -> FuturesOrder:
+        return self._place_order(OrderType.MARKET, symbol, side, size, 0, position, reduce_only)
+
+    def limit_order(self, symbol: str, side: Side, price: float, size: float,
+                    position: PositionMode = PositionMode.BOTH,
+                    reduce_only: bool = False, time_in_force: TimeInForce = None) -> FuturesOrder:
+        return self._place_order(OrderType.LIMIT, symbol, side, size, price, position, reduce_only)
+
+    def take_profit_order(self, symbol: str, side: Side, price: float, size: float,
+                          position: PositionMode = PositionMode.BOTH) -> FuturesOrder:
+        return self._place_order(OrderType.TAKE_PROFIT, symbol, side, size, price, position)
+
+    def stop_loss_order(self, symbol: str, side: Side, price: float, size: float,
+                        position: PositionMode = PositionMode.BOTH) -> FuturesOrder:
+        return self._place_order(OrderType.STOP, symbol, side, size, price, position)
+
+    def should_run_order(self, order):
+        price = self.get_price(order.symbol)
+
+        if order.type == OrderType.MARKET:
+            return True
+
+        if order.type == OrderType.LIMIT:
+            if order.side == Side.BUY:
+                return price <= order.limit_price
+            elif order.side == Side.SELL:
+                return price >= order.limit_price
+
+        raise InvalidOrder()
+
+    def set_hedge_mode(self, hedge_mode: HedgeMode):
+        if hedge_mode == HedgeMode.HEDGE:
+            raise ValueError('Hedge mode not supported')
+
+    def get_hedge_mode(self):
+        return HedgeMode.ONEWAY
+
+    def set_leverage(self, leverage: float, symbol: str = None):
+        if len(self.paper_positions):
+            raise BacktestingException('can\'t set leverage with open positions')
+        if symbol:
+            self.leverage[symbol] = leverage
+        else:
+            self.leverage_global_default = leverage
+
+    def get_leverage(self, symbol: str = None) -> float:
+        return self.leverage.get(symbol, self.leverage_global_default)
+
+    def set_margin_type(self, symbol: str, type: MarginType):
+        if len(self.paper_positions):
+            raise BacktestingException('can\'t set margin type with open positions')
+        if type == MarginType.ISOLATED:
+            raise BacktestingException('isolated margin not supported')
+        self.margin_type[symbol] = type
+
+    def get_margin_type(self, symbol: str) -> MarginType:
+        return self.margin_type.get(symbol, MarginType.CROSSED)
+
+    def cancel_order(self, symbol: str, order_id: int) -> FuturesOrder:
+        try:
+            order = self._placed_orders[order_id][1]
+            del self._placed_orders[order_id]
+            assert order.symbol == symbol
+            return dataclasses.replace(order, status=OrderStatus.CANCELED)
+        except KeyError:
+            raise BacktestingException('order not found')
+
+    def get_open_orders(self, symbol: str = None) -> List[FuturesOrder]:
+        return [dataclasses.replace(o) for o in self._placed_orders]
+
+    def get_order(self, symbol: str, order_id: int) -> FuturesOrder:
+        try:
+            order = self._placed_orders[order_id][1]
+            assert order.symbol == symbol
+            return dataclasses.replace(order, status=OrderStatus.CANCELED)
+        except KeyError:
+            raise Exception('order not found')
+
+    def get_funding_rate(self, symbol: str):
+        if self.backtesting:
+            return self.get_backtesting_funding_rate(symbol)
+        else:
+            return self.interface.get_funding_rate(symbol)
+
+    def get_backtesting_funding_rate(self, symbol: str):
+        # check if cache needs to be refreshed
+        # TODO this all need to be moved into events framework once that's done
+        time = self.time()
+        if not self._funding_rate_cache or self._funding_rate_cache[-1]['time'] < time:
+            self._funding_rate_cache = self.get_funding_rate_history(symbol, time, None)
+        for i, o in self._funding_rate_cache[:-1].enumerate():
+            if o['time'] < time:
+                last = o['rate']
+                next = self._funding_rate_cache[i + 1]['time']
+                return last, next
+        print(f'failed to download funding rate at time {time}')
+        return 0.0001, time + (time % self.get_funding_rate_resolution())
+
+    def get_price(self, symbol: str) -> float:
+        if symbol.endswith('-PERP'):
+            quote = self._quote_map[symbol]
+            symbol = symbol.split('-')[0] + '-' + quote
+        if self.backtesting:
+            return self.get_backtesting_price(symbol)
+        else:
+            return self.interface.get_price(symbol)
+
+    def get_funding_rate_history(self, symbol: str, epoch_start: int, epoch_stop: int) -> list:
+        return self.interface.get_funding_rate_history(symbol, epoch_start, epoch_stop)
+
+    def get_funding_rate_resolution(self) -> int:
+        return self.interface.get_funding_rate_resolution()
+
+    def get_product_history(self, symbol, epoch_start, epoch_stop, resolution):
+        if self.backtesting:
+            return utils.extract_price_by_resolution(self.full_prices, symbol, epoch_start, epoch_stop, resolution)
+        else:
+            return self.interface.get_product_history(symbol, epoch_start, epoch_stop, resolution)
+
+    def evaluate_traded_account_assets(self):
+        pass
+
+    def override_local_account(self, value_dictionary: dict):
+        # if python3.9 we could use the new dict update operator here :(
+        self.paper_account.update({
+            asset: {'available': value, 'hold': 0} for asset, value in value_dictionary.items()
+        })
+
+    def do_funding(self, symbol: str, rate: float):
+        # positive rate = longs pay for shorts
+        # negative rate = shorts pay for longs
+        if symbol not in self.paper_positions:
+            return
+        position = self.paper_positions[symbol]['size']
+        if (rate > 0) == (position > 0):
+            # we lose money :(
+            # either funding rate is positive and we are long
+            # or funding rate is negative and we are short
+            m = -1
+        else:
+            m = 1
+        self.paper_positions[symbol]['size'] *= 1 + (abs(rate) * m)
+
+    def calculate_position_value(self, symbol):
+        position = self.paper_positions.get(symbol, None)
+        if not position:
+            return 0
+        entry_price = position['exchange_specific']['entry_price']
+        current_price = self.get_price(symbol) * abs(position['size'])
+        return entry_price + (current_price - entry_price) * self.get_leverage(symbol)
+
+    def evaluate_limits(self):
+        self.check_margin_call()
+        self.execute_orders()
+
+    def execute_orders(self):
+        # TODO reduce_only on limit order "technically" broken
+        for id, order_data in list(self._placed_orders.items()):
+            is_closing, order = order_data
+            order: FuturesOrder
+
+            if not self.should_run_order(order):
+                continue
+
+            product = self.get_products(order.symbol)
+            base, quote = order.symbol.split('-')
+
+            asset_price = order.limit_price or self.get_price(order.symbol)
+            # if we held funds for the order, use those orelse just default to current price * ordersize
+            held_curr, notional = self._funds_held.pop(id, (quote, asset_price * order.size))
+
+            assert held_curr == quote
+
+            # copy order to _executed_orders
+            del self._placed_orders[id]
+            self._executed_orders[id] = dataclasses.replace(order, status=OrderStatus.FILLED, price=notional)
+
+            position_size = (self.get_position(order.symbol) or {'size': 0})['size']
+            size_diff = order.size * (1 if order.side == Side.BUY else -1)
+
+            position = self.paper_positions.get(order.symbol, None)
+            entry_price = position['exchange_specific']['entry_price'] if position else 0
+            entry_price += notional
+            # we sold off our position
+            if is_closing:
+                value = self.calculate_position_value(order.symbol)
+                self.paper_account[quote]['available'] += value
+            else:
+                self.paper_account[quote]['hold'] -= notional
+
+            # this overwrites but it's fine, we don't need to update
+            new_position = position_size + size_diff
+
+            # minimum possible position size you can buy, times two
+            # TODO improve this to just take minimum position size from exchange
+            if abs(new_position) >= 2 * utils.precision_to_increment(product['size_precision']):
+                self.paper_positions[order.symbol] = {
+                    'symbol': order.symbol,
+                    'base_asset': base,
+                    'quote_asset': quote,
+                    'size': new_position,
+                    'position': PositionMode.BOTH,
+                    'leverage': self.get_leverage(order.symbol),
+                    'margin_type': self.get_margin_type(order.symbol),
+                    'contract_type': ContractType.PERPETUAL,
+                    'exchange_specific': {
+                        'entry_price': entry_price
+                    }
+                }
+            else:
+                del self.paper_positions[order.symbol]
+            self.paper_account[base + '-PERP']['available'] = new_position
+
+    def check_margin_call(self):
+        called = []
+        for symbol, position in self.paper_positions.items():
+            value = self.calculate_position_value(symbol)
+            if self.cash + value < 0:
+                # placing orders in here fucks with the dictionary
+                called.append((symbol, position))
+
+        for symbol, position in called:
+            self.market_order(symbol, Side.BUY if position['size'] < 0 else Side.SELL, abs(position['size']),
+                              reduce_only=True)
+
+    def _place_order(self, type: OrderType, symbol: str, side: Side, size: float, limit_price: float = 0,
+                     position_mode: PositionMode = PositionMode.BOTH, reduce_only: bool = False,
+                     time_in_force: TimeInForce = TimeInForce.GTC) -> FuturesOrder:
+        self.add_symbol(symbol)
+        product = self.get_products(symbol)
+
+        if self.should_auto_trunc:
+            size = utils.trunc(size, product['size_precision'])
+
+        if not product:
+            raise ValueError(f'invalid symbol {symbol}')
+
+        if position_mode != PositionMode.BOTH:
+            raise ValueError(f'only PositionMode.BOTH is supported for paper trading at this time')
+
+        if size <= 0:
+            raise ValueError('cannot place empty order')
+
+        if limit_price < 0 or (limit_price == 0 and type != OrderType.MARKET):
+            raise ValueError('invalid limit price')
+
+        # place funds on hold
+        if type == OrderType.MARKET:
+            price = self.get_price(symbol)
+        elif reduce_only:
+            raise ValueError('reduce_only only supported for market order')
+        else:
+            price = limit_price
+
+        position = self.get_position(symbol)
+        # close position if we are selling from >0 position or buying <0 position
+        # no position == we are never closing
+        is_closing = self.is_closing_position(position, side)
+        if not position and reduce_only:
+            raise Exception('reduce_only set to True but there is no position to reduce')
+
+        if reduce_only and not is_closing:
+            raise Exception(f'reduce_only set to True but order side ({side}) is opening or growing position')
+
+        if reduce_only:
+            size = min(size, abs(position['size']))
+
+        if is_closing and size > abs(position['size']):
+            # we need to close position and then buy/sell extra w/ the extra funds
+            # this is weird behavior but it's what binance futures does
+            # for now raise exception in backtesting, nobody is going to do this anyways
+            # and the workaround is simple, just close your position first
+            raise Exception(f'order size ({size}) is greater than position ({abs(position["size"])}), '
+                            'close your position first and then place order')
+
+        base, quote = symbol.split('-')
+        acc = self.paper_account[quote]
+
+        fee = self.get_taker_fee()
+        if is_closing:
+            funds = (price * size) * (1 - fee)
+        else:
+            funds = (price * size) * (1 + fee)
+
+        order_id = self.gen_order_id()
+        if not is_closing:
+            if acc['available'] < funds:
+                raise Exception('not enough funds')
+
+            acc['available'] -= funds
+            acc['hold'] += funds
+            self._funds_held[order_id] = (quote, funds)
+
+        order = FuturesOrder(symbol=symbol, id=order_id, size=size, status=OrderStatus.OPEN, type=type,
+                             contract_type=ContractType.PERPETUAL, side=side, position=PositionMode.BOTH,
+                             limit_price=limit_price,
+                             price=0, time_in_force=TimeInForce.GTC, response={}, interface=self.interface)
+        self._placed_orders[order_id] = (is_closing, order)
+
+        self.execute_orders()
+
+        return order
+
+    @staticmethod
+    def is_closing_position(position, side):
+        if not position or position['size'] == 0:
+            return False
+        if (position['size'] > 0) == (side == Side.SELL):
+            return True
+        return False
+
+    @staticmethod
+    def gen_order_id():
+        return random.randrange(10 ** 4, 10 ** 5)
+
+    @functools.lru_cache(None)
+    def get_maker_fee(self) -> float:
+        return self.interface.get_maker_fee()
+
+    @functools.lru_cache(None)
+    def get_taker_fee(self) -> float:
+        return self.interface.get_taker_fee()
diff --git a/blankly/exchanges/interfaces/paper_trade/metrics.py b/blankly/exchanges/interfaces/paper_trade/metrics.py
index 6c4349b1..9f0c33e5 100644
--- a/blankly/exchanges/interfaces/paper_trade/metrics.py
+++ b/blankly/exchanges/interfaces/paper_trade/metrics.py
@@ -20,7 +20,7 @@
 from blankly.utils.time_builder import build_year
 
 
-def periods_per_year(period : int) -> float:
+def periods_per_year(period: int) -> float:
     """
     Find how many trading periods occur within a trading year
 
@@ -34,7 +34,7 @@ def periods_per_year(period : int) -> float:
     # 24 hours becomes 6.5 (hours the market is open) AND the risk_free_rate
     # goes to zero
     trading_seconds_per_year = 252.0 * 24.0 * 60.0 * 60.0
-    ppy = trading_seconds_per_year/period
+    ppy = trading_seconds_per_year / period
     return ppy
 
 
diff --git a/blankly/exchanges/interfaces/paper_trade/paper_trade.py b/blankly/exchanges/interfaces/paper_trade/paper_trade.py
index e2bc3ef8..70a20769 100644
--- a/blankly/exchanges/interfaces/paper_trade/paper_trade.py
+++ b/blankly/exchanges/interfaces/paper_trade/paper_trade.py
@@ -28,7 +28,7 @@ def __init__(self, authenticated_exchange: Exchange, initial_account_values: dic
         # This construction is unique because it doesn't make the interface super call for construction
         self.interface = PaperTradeInterface(authenticated_exchange.get_interface(),
                                              initial_account_values)
-        self.calls = authenticated_exchange.get_direct_calls()
+        self.calls = authenticated_exchange.calls
 
     def start_limit_order_watch(self):
         """
diff --git a/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py b/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
index c6318229..f2270dc8 100644
--- a/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
+++ b/blankly/exchanges/interfaces/paper_trade/paper_trade_interface.py
@@ -28,6 +28,8 @@
 from blankly.exchanges.interfaces.paper_trade.backtesting_wrapper import BacktestingWrapper
 from blankly.exchanges.orders.limit_order import LimitOrder
 from blankly.exchanges.orders.market_order import MarketOrder
+from blankly.exchanges.orders.stop_loss import StopLossOrder
+from blankly.exchanges.orders.take_profit import TakeProfitOrder
 from blankly.utils.exceptions import APIException, InvalidOrder
 
 
@@ -102,11 +104,15 @@ def evaluate_traded_account_assets(self):
             if (accounts[i]['available'] + accounts[i]['hold']) != 0 and i not in self.traded_assets:
                 self.traded_assets.append(i)
 
-    def is_backtesting(self):
+    def backtesting_time(self):
         """
         Return the backtest time if we're backtesting, if not just return None. If it's None the caller
          assumes no backtesting. The function being overridden always returns None
         """
+        # This is for the inits because it happens with both live calls and in the past
+        if self.initial_time is not None and not self.backtesting:
+            return self.initial_time
+        # This is for the actual price loops
         if self.backtesting:
             return self.time()
         else:
@@ -142,7 +148,7 @@ def start_paper_trade_watchdog(self):
         # TODO, this process could use variable update time/websocket usage, poll `time` and a variety of settings
         #  to create a robust trading system
         # Create the watchdog for watching limit orders
-        self.__thread = threading.Thread(target=self.__paper_trade_watchdog(), daemon=True)
+        self.__thread = threading.Thread(target=self.__paper_trade_watchdog, daemon=True)
         self.__thread.start()
         self.__run_watchdog = True
 
@@ -155,6 +161,7 @@ def __paper_trade_watchdog(self):
         """
         Internal order watching system
         """
+        utils.info_print('Evaluating paper limit orders every 10 seconds...')
         while True:
             time.sleep(10)
             if not self.__run_watchdog:
@@ -232,8 +239,9 @@ def evaluate_limits(self):
                 "settled": false
             }
             """
-            if index['type'] == 'limit' and index['status'] == 'pending':
+            if index['type'] in ('limit', 'stop_loss') and index['status'] == 'pending':
                 current_price = prices[index['symbol']]
+                limit_price = index['price']
 
                 if index['side'] == 'buy':
                     if index['price'] > current_price:
@@ -270,7 +278,8 @@ def evaluate_limits(self):
 
                         self.paper_trade_orders[i] = order
                 elif index['side'] == 'sell':
-                    if index['price'] < prices[index['symbol']]:
+                    if limit_price < current_price and index['type'] == 'limit' \
+                            or current_price <= limit_price and index['type'] == 'stop_loss':
                         # Take everything off hold
 
                         asset_id = index['symbol']
@@ -337,16 +346,18 @@ def get_account(self, symbol=None) -> utils.AttributeDict:
                 else:
                     raise KeyError("Symbol not found.")
 
+    def take_profit_order(self, symbol: str, price: float, size: float) -> TakeProfitOrder:
+        # we don't simulate partial fills, this is the same as take_profit
+        order = self.limit_order(symbol, 'sell', price, size)
+        return TakeProfitOrder(order._LimitOrder__order, order._LimitOrder__response, order.Interface)
+
+    def stop_loss_order(self, symbol: str, price: float, size: float):
+        return self.limit_order(symbol, 'sell', price, size, stop_loss=True)
+
     def market_order(self, symbol, side, size) -> MarketOrder:
         if not self.backtesting:
             print("Paper Trading...")
         needed = self.needed['market_order']
-        order = {
-            'size': size,
-            'side': side,
-            'symbol': symbol,
-            'type': 'market'
-        }
         creation_time = self.time()
         price = self.get_price(symbol)
         funds = price*size
@@ -367,7 +378,9 @@ def market_order(self, symbol, side, size) -> MarketOrder:
         base_decimals = self.__get_decimals(base_increment)
 
         # Test if funds has more decimals than the increment. The increment is the maximum resolution of the quote.
-        if self.__get_decimals(size) > base_decimals:
+        if self.should_auto_trunc:
+            size = utils.trunc(size, base_decimals)
+        elif self.__get_decimals(size) > base_decimals:
             raise InvalidOrder("Size resolution is too high, the highest resolution allowed for this symbol is: " +
                                str(base_increment) + ". You specified " + str(size) +
                                ". Try using blankly.trunc(size, decimal_number) to match the exchange resolution.")
@@ -380,6 +393,12 @@ def market_order(self, symbol, side, size) -> MarketOrder:
             shortable = False
             quantity_decimals = self.__get_decimals(market_limits['limit_order']['base_increment'])
 
+        order = {
+            'size': size,
+            'side': side,
+            'symbol': symbol,
+            'type': 'market'
+        }
         qty = size
 
         # Test the purchase
@@ -450,7 +469,7 @@ def market_order(self, symbol, side, size) -> MarketOrder:
         })
         return MarketOrder(order, response, self)
 
-    def limit_order(self, symbol, side, price, size) -> LimitOrder:
+    def limit_order(self, symbol, side, price, size, stop_loss=False) -> LimitOrder:
         """
         Used for buying or selling limit orders
         Args:
@@ -484,7 +503,7 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
             'side': side,
             'price': price,
             'symbol': symbol,
-            'type': 'limit'
+            'type': 'stop_loss' if stop_loss else 'limit'
         }
         creation_time = self.time()
 
@@ -513,17 +532,29 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
         price_increment = order_filter['limit_order']['price_increment']
         price_increment_decimals = self.__get_decimals(price_increment)
 
-        if self.__get_decimals(price) > price_increment_decimals:
+        if self.should_auto_trunc:
+            price = utils.trunc(price, price_increment_decimals)
+        elif self.__get_decimals(price) > price_increment_decimals:
             raise InvalidOrder("Fund resolution is too high, minimum resolution is: " + str(price_increment) +
                                ". Try using blankly.trunc(size, decimal_number) to match the exchange resolution.")
 
         base_increment = order_filter['limit_order']['base_increment']
         base_decimals = self.__get_decimals(base_increment)
 
-        if self.__get_decimals(size) > base_decimals:
+        if self.should_auto_trunc:
+            size = utils.trunc(size, base_decimals)
+        elif self.__get_decimals(size) > base_decimals:
             raise InvalidOrder("Fund resolution is too high, minimum resolution is: " + str(base_increment) +
                                '. Try using blankly.trunc(size, decimal_number) to match the exchange resolution.')
 
+        order = {
+            'size': size,
+            'side': side,
+            'price': price,
+            'symbol': symbol,
+            'type': 'limit'
+        }
+
         # Test the trade
         self.local_account.test_trade(symbol, side, size, price, quote_resolution=price_increment_decimals,
                                       base_resolution=base_decimals, shortable=False)
@@ -541,7 +572,7 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
             'size': str(size),
             'status': 'pending',
             "time_in_force": "GTC",
-            'type': 'limit',
+            'type': 'stop_loss' if stop_loss else 'limit',
             'side': str(side),
             # 'stp': 'dc',
             # 'post_only': 'false',
@@ -577,7 +608,8 @@ def limit_order(self, symbol, side, price, size) -> LimitOrder:
             self.local_account.update_hold(base, hold + size)
         else:
             raise APIException(f"Invalid side {side}")
-        return LimitOrder(order, response, self)
+        # TODO this is super stinky but refactoring this code is even stinkier
+        return StopLossOrder(order, response, self) if stop_loss else LimitOrder(order, response, self)
 
     def cancel_order(self, symbol, order_id) -> dict:
         """
diff --git a/blankly/exchanges/managers/orderbook_manager.py b/blankly/exchanges/managers/orderbook_manager.py
index a1b05e61..55599899 100644
--- a/blankly/exchanges/managers/orderbook_manager.py
+++ b/blankly/exchanges/managers/orderbook_manager.py
@@ -458,14 +458,14 @@ def okx_snapshot_update(self, update):
             "asks": []
         }
 
-        buys = update['data'][0]['bids'] # [0][:-1]
+        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 = update['data'][0]['asks']  # [0][:-1]
         sells[0] = sells[0][:-1]
         for i in range(len(sells)):
             sell = sells[i]
diff --git a/blankly/exchanges/orders/stop_loss.py b/blankly/exchanges/orders/stop_loss.py
new file mode 100644
index 00000000..7599f8fb
--- /dev/null
+++ b/blankly/exchanges/orders/stop_loss.py
@@ -0,0 +1,63 @@
+"""
+    Class to manage a limit order lifecycle - a way to determine individual information about each buy and sell
+    Copyright (C) 2022 Matias Kotlik, Emerson Dove
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+from blankly.exchanges.orders.order import Order
+
+
+class StopLossOrder(Order):
+    """
+    (Buying or selling (string), amount in currency (BTC/XLM), ticker object (so we can get time and value),
+    limit if there is one)
+    """
+
+    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
+        super().__init__(self.__response, self.__order, self.Interface)
+
+    def get_price(self) -> float:
+        """
+        Get the price that the order was set at. For limits this will be user-specified, for markets this will
+        be market price.
+        """
+        return self.__response["price"]
+
+    def get_time_in_force(self) -> str:
+        """
+        Get the exchange's set time_in_force value.
+        """
+        return self.__response["time_in_force"]
+
+    def __str__(self):
+        return_string = super().__str__()
+
+        return_string = self.add_new_line(return_string, "Stop Loss Order Parameters: ")
+
+        return_string = self.add_new_line(return_string, "Time In Force: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_time_in_force())
+
+        return_string = self.add_new_line(return_string, "Price: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_price())
+
+        return return_string
+
+    # override this here to homogenize these strings
+    def get_type(self) -> str:
+        return 'stop_loss'
diff --git a/blankly/exchanges/orders/take_profit.py b/blankly/exchanges/orders/take_profit.py
new file mode 100644
index 00000000..86b688ec
--- /dev/null
+++ b/blankly/exchanges/orders/take_profit.py
@@ -0,0 +1,63 @@
+"""
+    Class to manage a limit order lifecycle - a way to determine individual information about each buy and sell
+    Copyright (C) 2022 Matias Kotlik, Emerson Dove
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Lesser General Public License as published
+    by the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Lesser General Public License for more details.
+
+    You should have received a copy of the GNU Lesser General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+"""
+from blankly.exchanges.orders.order import Order
+
+
+class TakeProfitOrder(Order):
+    """
+    (Buying or selling (string), amount in currency (BTC/XLM), ticker object (so we can get time and value),
+    limit if there is one)
+    """
+
+    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
+        super().__init__(self.__response, self.__order, self.Interface)
+
+    def get_price(self) -> float:
+        """
+        Get the price that the order was set at. For limits this will be user-specified, for markets this will
+        be market price.
+        """
+        return self.__response["price"]
+
+    def get_time_in_force(self) -> str:
+        """
+        Get the exchange's set time_in_force value.
+        """
+        return self.__response["time_in_force"]
+
+    def __str__(self):
+        return_string = super().__str__()
+
+        return_string = self.add_new_line(return_string, "Take Profit Order Parameters: ")
+
+        return_string = self.add_new_line(return_string, "Time In Force: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_time_in_force())
+
+        return_string = self.add_new_line(return_string, "Price: ", newline=False)
+        return_string = self.add_new_line(return_string, self.get_price())
+
+        return return_string
+
+    # override this here to homogenize these strings
+    def get_type(self) -> str:
+        return 'take_profit'
diff --git a/blankly/exchanges/strategy_logger.py b/blankly/exchanges/strategy_logger.py
index 97c985bf..4ec70dad 100644
--- a/blankly/exchanges/strategy_logger.py
+++ b/blankly/exchanges/strategy_logger.py
@@ -38,13 +38,13 @@ def get_calls(self):
         No logging implemented
         """
         return self.interface.get_calls()
-    
+
     def get_exchange_type(self):
         """
         No logging implemented
         """
         return self.interface.get_exchange_type()
-    
+
     def get_account(self, symbol: str = None) -> AttributeDict:
         """
         No logging implemented
@@ -54,6 +54,7 @@ def get_account(self, symbol: str = None) -> AttributeDict:
     """
     These next three queries have large responses. It is unclear if this quantity of data is useful or necessary
     """
+
     def get_products(self):
         """
         No logging implemented
@@ -66,7 +67,7 @@ def get_product_history(self, symbol: str, epoch_start: float,
         No logging implemented
         """
         return self.interface.get_product_history(symbol, epoch_start, epoch_stop, resolution)
-    
+
     def history(self,
                 symbol: str,
                 to: Union[str, int] = 200,
@@ -80,7 +81,15 @@ def history(self,
         return self.interface.history(symbol, to=to,
                                       resolution=resolution, start_date=start_date,
                                       end_date=end_date, return_as=return_as)
-    
+
+    # no logging for these on platform yet
+    def take_profit_order(self, symbol: str, price: float, size: float) -> LimitOrder:
+        pass
+
+    # no logging for these on platform yet
+    def stop_loss_order(self, symbol: str, price: float, size: float) -> LimitOrder:
+        pass
+
     def market_order(self, symbol: str, side: str, size: float) -> MarketOrder:
         out = self.interface.market_order(symbol, side, size)
 
@@ -96,7 +105,7 @@ def market_order(self, symbol: str, side: str, size: float) -> MarketOrder:
         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)
 
@@ -113,7 +122,7 @@ def limit_order(self, symbol: str, side: str, price: float, size: float) -> Limi
         except Exception:
             pass
         return out
-    
+
     def cancel_order(self, symbol: str, order_id: str) -> dict:
         """
         No logging implemented
@@ -153,7 +162,7 @@ def get_order_filter(self, symbol: str):
         No logging implemented
         """
         return self.interface.get_order_filter(symbol)
-    
+
     def get_price(self, symbol: str) -> float:
         """
         No logging implemented
@@ -163,13 +172,14 @@ def get_price(self, symbol: str) -> float:
     """
     No logging implemented for these properties
     """
+
     @property
     def account(self) -> AttributeDict:
         """
         No logging implemented
         """
         return self.interface.account
-    
+
     @property
     def orders(self) -> list:
         """
@@ -183,4 +193,3 @@ def cash(self) -> float:
         No logging implemented
         """
         return self.interface.cash
-    
diff --git a/blankly/frameworks/model/model.py b/blankly/frameworks/model/model.py
index b4b0b3c7..bb3d9763 100644
--- a/blankly/frameworks/model/model.py
+++ b/blankly/frameworks/model/model.py
@@ -16,23 +16,27 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 import threading
+import abc
 import typing
 
+from blankly.exchanges.abc_base_exchange import ABCBaseExchange
+from blankly.exchanges.futures.futures_exchange import FuturesExchange
 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.futures.futures_paper_trade import FuturesPaperTrade
 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):
+class Model(abc.ABC):
+    def __init__(self, exchange: ABCBaseExchange):
         self.__exchange = exchange
         self.__exchange_cache = self.__exchange
         self.is_backtesting = False
 
-        self.interface = exchange.interface
+        self.interface = exchange.get_interface()
 
         self.has_data = True
 
@@ -42,15 +46,17 @@ def __init__(self, exchange: Exchange):
 
     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)
+        if isinstance(self.__exchange, Exchange):
+            self.__exchange = PaperTrade(self.__exchange)
+        elif isinstance(self.__exchange, FuturesExchange):
+            self.__exchange = FuturesPaperTrade(self.__exchange)
+        else:
+            raise NotImplementedError
         self.interface = self.__exchange.interface
         backtest = self.__backtester.run(args,
                                          initial_account_values=initial_values,
@@ -68,10 +74,17 @@ def backtest(self, args, initial_values: dict = None, settings_path: str = None,
     def run(self, args: typing.Any = None) -> threading.Thread:
         thread = threading.Thread(target=self.main, args=(args,))
         thread.start()
+        # Don't force them to always enable limit order watch
+        if isinstance(self.__exchange, Exchange) and isinstance(self.__exchange, PaperTrade):
+            self.__exchange.start_limit_order_watch()
         return thread
 
+    @abc.abstractmethod
     def main(self, args):
-        raise NotImplementedError("Add a main function to your strategy to run the model.")
+        pass
+
+    def teardown(self):
+        pass
 
     def event(self, type_: str, data: any):
         """
diff --git a/blankly/frameworks/screener/screener.py b/blankly/frameworks/screener/screener.py
index cdf137cf..b16a1bb5 100644
--- a/blankly/frameworks/screener/screener.py
+++ b/blankly/frameworks/screener/screener.py
@@ -15,7 +15,7 @@
     You should have received a copy of the GNU Lesser General Public License
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
-
+import time
 import typing
 from typing import List
 from blankly.frameworks.screener.screener_runner import ScreenerRunner
@@ -63,6 +63,8 @@ def __init__(self, exchange: Exchange,
             cron_settings = load_deployment_settings()['screener']['schedule']
             blankly._screener_runner = ScreenerRunner(cron_settings)
 
+        self.status = {}
+
         # TODO export the symbols here as a list
         self.exchange = exchange
         self.symbols = symbols
@@ -89,6 +91,7 @@ def __init__(self, exchange: Exchange,
         self.__run()
 
     def __run(self):
+        self.status['startTime'] = time.time()
         init = self.__callables['init']
         if callable(init):
             init(self.screener_state)
@@ -110,6 +113,7 @@ def __run(self):
             raise TypeError("Must pass a callable for the evaluator.")
 
         for i in self.symbols:
+            start_time = time.time()
             # Parse the types for the symbol
             # If it's a dictionary it's A ok but if it's a non-dict give it the value column
             result = evaluator(i, self.screener_state)
@@ -117,6 +121,7 @@ def __run(self):
                 result = {
                     'value': result
                 }
+            result['symbol_time'] = time.time() - start_time
             self.raw_results[i] = result
 
         self.symbols = self.screener_state.symbols
@@ -137,6 +142,9 @@ def __run(self):
 
         self.symbols = self.screener_state.symbols
 
+        self.status['stopTime'] = time.time()
+        self.status['timeElapsed'] = self.status['stopTime'] - self.status['startTime']
+
         blankly.reporter.export_screener_result(self)
 
     def notify(self, message: str = None):
diff --git a/blankly/frameworks/strategy/futures_strategy.py b/blankly/frameworks/strategy/futures_strategy.py
index 572b7842..c093a4e0 100644
--- a/blankly/frameworks/strategy/futures_strategy.py
+++ b/blankly/frameworks/strategy/futures_strategy.py
@@ -23,29 +23,10 @@
 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
+from blankly.frameworks.strategy.strategy import Strategy
 
 
-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
+class FuturesStrategy(Strategy):
+
+    def teardown(self):
+        pass
diff --git a/blankly/frameworks/strategy/order.py b/blankly/frameworks/strategy/order.py
index 3275d081..7425a39f 100644
--- a/blankly/frameworks/strategy/order.py
+++ b/blankly/frameworks/strategy/order.py
@@ -31,7 +31,7 @@ def __init__(self, order_type, side, amount, price=None):
         if self.amount <= 0:
             raise ValueError("Size must be greater than zero, but was given: {}".format(size))
         self.amount = amount
-        if self.type == 'limit' and price == None:
+        if self.type == 'limit' and price is None:
             raise ValueError("Limit orders require a price but None was given")
 
         if price <= 0:
diff --git a/blankly/frameworks/strategy/strategy.py b/blankly/frameworks/strategy/strategy.py
index fcee8f22..d1b8e017 100644
--- a/blankly/frameworks/strategy/strategy.py
+++ b/blankly/frameworks/strategy/strategy.py
@@ -22,6 +22,7 @@
 import warnings
 
 import blankly
+from blankly.exchanges.abc_base_exchange import ABCBaseExchange
 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
@@ -52,20 +53,20 @@ def construct_strategy(self, schedulers, orderbook_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
+    def rest_event(self, **event):
+        callback = event['callback']  # type: callable
+        symbol = event['symbol']  # type: str
+        resolution = event['resolution']  # type: int
+        variables = event['variables']  # type: dict
+        type_ = event['type']  # type: EventType
+        state = event['state']  # type: StrategyState
 
         state.variables = variables
         state.resolution = resolution
 
         if type_ == EventType.bar_event:
             if not self.is_backtesting:
-                bar_time = kwargs['bar_time']
+                bar_time = event['bar_time']
                 while True:
                     # Sometimes coinbase doesn't download recent data correctly
                     try:
@@ -85,6 +86,7 @@ def rest_event(self, **kwargs):
                 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)
@@ -123,22 +125,29 @@ def grab_price(threaded_symbol):
         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
+    def run_price_events(self, events: list):
+        # run all events once at start
+        for event in events:
+            event['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]
+            events.sort(key=lambda d: d['next_run'])
+            event = events[0]
 
             # Sleep the difference
-            self.sleep(next_event['next_run'] - self.time)
+            self.sleep(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']
+            next_run = self.rest_event(**event)
+            if next_run:
+                # if rest_event returns something, run this event again at that time
+                # this implies the event did *not* run
+                event['next_run'] = next_run
+                event['was_delayed'] = True
+            else:
+                # otherwise, the event ran. we can revalue account and re-run normally @ `resolution` intervals
+                self.backtester.value_account()
+                event['next_run'] += event['resolution']
 
     def main(self, args):
         if self.is_backtesting:
@@ -154,11 +163,11 @@ def run_backtest(self):
             kwargs['state'].strategy.interface = self.interface
         self.__run_init()
 
-        kwargs_list = []
+        events = []
         for scheduler in self.schedulers:
-            kwargs_list.append(scheduler.get_kwargs())
+            events.append(scheduler.get_kwargs())
 
-        self.run_price_events(kwargs_list)
+        self.run_price_events(events)
 
     def __run_init(self):
         # Switch to live mode for the inits
@@ -219,12 +228,15 @@ class Strategy(StrategyBase):
     __exchange: Exchange
     interface: ABCExchangeInterface
 
-    def __init__(self, exchange: Exchange):
+    def __init__(self, exchange: ABCBaseExchange):
         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 teardown(self):
+        pass
+
     def backtest(self,
                  to: str = None,
                  initial_values: dict = None,
@@ -360,7 +372,6 @@ def start(self):
             warnings.warn("Aborted attempt to start a live strategy a backtest configuration")
             return
         self.model.run()
-        self.model.teardown()
 
     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 7761a9e1..1acd8e29 100644
--- a/blankly/frameworks/strategy/strategy_base.py
+++ b/blankly/frameworks/strategy/strategy_base.py
@@ -200,7 +200,12 @@ def __custom_price_event(self,
         )
 
         # Export a new symbol to the backend
-        blankly.reporter.export_used_symbol(symbol)
+        if symbol is not None:
+            if isinstance(symbol, list):
+                for i in symbol:
+                    blankly.reporter.export_used_symbol(i)
+            else:
+                blankly.reporter.export_used_symbol(symbol)
 
     @staticmethod
     def __websocket_callback(tick, symbol, user_callback, state_object):
diff --git a/blankly/futures.py b/blankly/futures/__init__.py
similarity index 85%
rename from blankly/futures.py
rename to blankly/futures/__init__.py
index 6c1f01c3..2ebe77b3 100644
--- a/blankly/futures.py
+++ b/blankly/futures/__init__.py
@@ -3,4 +3,5 @@
 from blankly.frameworks.strategy import FuturesStrategy
 from blankly.frameworks.strategy import FuturesStrategyState
 
-from blankly.enums import MarginType, HedgeMode
\ No newline at end of file
+from blankly.enums import MarginType, HedgeMode
+
diff --git a/blankly/futures/utils.py b/blankly/futures/utils.py
new file mode 100644
index 00000000..3e230782
--- /dev/null
+++ b/blankly/futures/utils.py
@@ -0,0 +1,25 @@
+from blankly.enums import Side
+from blankly.exchanges.interfaces.futures_exchange_interface import FuturesExchangeInterface
+from blankly.frameworks.strategy import FuturesStrategyState
+
+
+def close_position(symbol: str, state: FuturesStrategyState):
+    """
+    Exit a position
+    Args:
+        state: the StrategyState
+        symbol: the symbol to sell
+    """
+    position = state.interface.get_position(symbol)
+    if not position:
+        return
+
+    if position['size'] < 0:
+        side = Side.BUY
+    elif position['size'] > 0:
+        side = Side.SELL
+    else:
+        # wtf?
+        return
+
+    state.interface.market_order(symbol, side, abs(position['size']), reduce_only=True)
diff --git a/blankly/utils/__init__.py b/blankly/utils/__init__.py
index 8f560f35..ef0e78aa 100644
--- a/blankly/utils/__init__.py
+++ b/blankly/utils/__init__.py
@@ -1,1 +1,2 @@
 from blankly.utils.utils import *
+from blankly.utils.time_builder import time_interval_to_seconds
diff --git a/blankly/utils/exceptions.py b/blankly/utils/exceptions.py
index b302970b..9b8b6408 100644
--- a/blankly/utils/exceptions.py
+++ b/blankly/utils/exceptions.py
@@ -27,3 +27,7 @@ class APIException(Exception):
 
 class AuthError(Exception):
     pass
+
+
+class BacktestingException(Exception):
+    pass
diff --git a/blankly/utils/utils.py b/blankly/utils/utils.py
index c53f7652..e90bb5e6 100644
--- a/blankly/utils/utils.py
+++ b/blankly/utils/utils.py
@@ -21,16 +21,12 @@
 import sys
 import decimal
 import os
-from datetime import datetime as dt, timezone
-from math import ceil, trunc as math_trunc
+from datetime import datetime as dt
+from math import trunc as math_trunc
 from typing import Union
 
-import dateutil.parser as dp
 import numpy as np
 import pandas as pd
-import pandas_market_calendars as mcal
-
-from blankly.utils.time_builder import time_interval_to_seconds
 
 # Copy of settings to compare defaults vs overrides
 default_general_settings = {
@@ -38,6 +34,7 @@
         "use_sandbox_websockets": False,
         "websocket_buffer_size": 10000,
         "test_connectivity_on_auth": True,
+        "auto_truncate": False,
 
         "coinbase_pro": {
             "cash": "USD"
@@ -101,17 +98,17 @@
 }
 
 default_notify_settings = {
-  "email": {
-    "port": 465,
-    "smtp_server": "smtp.website.com",
-    "sender_email": "email_attached_to_smtp_account@web.com",
-    "receiver_email": "email_to_send_to@web.com",
-    "password": "my_password"
-  },
-  "text": {
-    "phone_number": "1234567683",
-    "provider": "verizon"
-  }
+    "email": {
+        "port": 465,
+        "smtp_server": "smtp.website.com",
+        "sender_email": "email_attached_to_smtp_account@web.com",
+        "receiver_email": "email_to_send_to@web.com",
+        "password": "my_password"
+    },
+    "text": {
+        "phone_number": "1234567683",
+        "provider": "verizon"
+    }
 }
 
 default_deploy_settings = {
@@ -119,7 +116,7 @@
     "python_version": '3.7',
     "requirements": "./requirements.txt",
     "working_directory": ".",
-    "ignore_files": ['price_caches'],
+    "ignore_files": ['price_caches', '.git', '.idea', '.vscode'],
     "backtest_args": {
         'to': '1y'
     },
@@ -175,7 +172,7 @@ def __compare_dicts(self, default_settings, user_settings):
                     user_settings[k] = v
         return user_settings
 
-    def load(self, override_path=None) -> dict:
+    def load(self, override_path=None, override_allow_nonexistent=False) -> dict:
         # Make overrides sticky by changing how the default is set. This is cool because the user can put settings in
         # obscure locations and then continue to load from them
         if override_path is not None:
@@ -185,7 +182,7 @@ def load(self, override_path=None) -> dict:
             try:
                 preferences = load_json_file(self.__default_path)
             except FileNotFoundError:
-                if self.__allow_nonexistent:
+                if self.__allow_nonexistent or override_allow_nonexistent:
                     return self.__default_settings
                     # self.write(self.__default_settings)
                     # # Recursively run this
@@ -227,12 +224,12 @@ def write(self, json_information: dict, override_path: str = None):
                                         "working directory!", allow_nonexistent=True)
 
 
-def load_user_preferences(override_path=None) -> dict:
-    return general_settings.load(override_path)
+def load_user_preferences(override_path=None, override_allow_nonexistent=False) -> dict:
+    return general_settings.load(override_path, override_allow_nonexistent=override_allow_nonexistent)
 
 
-def load_backtest_preferences(override_path=None) -> dict:
-    return backtest_settings.load(override_path)
+def load_backtest_preferences(override_path=None, override_allow_nonexistent=False) -> dict:
+    return backtest_settings.load(override_path, override_allow_nonexistent=override_allow_nonexistent)
 
 
 def load_deployment_settings() -> dict:
@@ -258,6 +255,7 @@ def pretty_print_json(json_object, actually_print=True):
 
 
 def epoch_from_iso8601(iso8601: str) -> float:
+    import dateutil.parser as dp
     return dp.parse(iso8601).timestamp()
 
 
@@ -274,6 +272,7 @@ def convert_input_to_epoch(value: Union[str, dt]) -> float:
 def iso8601_from_epoch(epoch) -> str:
     return dt.utcfromtimestamp(epoch).isoformat() + 'Z'
 
+
 # Removed due to sklearn dependency
 # def get_price_derivative(ticker, point_number):
 #     """
@@ -342,6 +341,8 @@ def to_blankly_symbol(symbol, exchange, quote_guess=None) -> str:
     if exchange == "ftx":
         return symbol.replace("/", "-")
 
+    return symbol
+
 
 def __check_ending(full_string, checked_ending) -> bool:
     check_length = len(checked_ending)
@@ -357,6 +358,7 @@ def to_exchange_symbol(blankly_symbol, exchange):
         return blankly_symbol
     if exchange == 'ftx':
         return blankly_symbol.replace("-", "/")
+    return blankly_symbol
 
 
 def get_base_asset(symbol):
@@ -390,6 +392,8 @@ def rename_to(keys_array, renaming_dictionary):
         mutated_dictionary["exchange_specific"] = {}
 
     for i in keys_array:
+        if i[0] not in renaming_dictionary:
+            continue
         if i[1] in renaming_dictionary:
             # If we're here this key has already been defined, push it to the specific
             mutated_dictionary["exchange_specific"][i[1]] = renaming_dictionary[i[1]]
@@ -437,7 +441,8 @@ def isolate_specific(needed, compare_dictionary):
 
     # Now we need to remove the keys that we appended to exchange_specific
     for k, v in exchange_specific.items():
-        del compare_dictionary[k]
+        if k in compare_dictionary:
+            del compare_dictionary[k]
 
     # If there exists the exchange specific dict in the compare dictionary
     # This is done because after renaming, if there are naming conflicts they will already have been pushed here
@@ -656,39 +661,6 @@ def ceil_date(date, **kwargs):
     return dt.fromtimestamp(date.timestamp() + secs - date.timestamp() % secs)
 
 
-OVERESTIMATE_CONSTANT = 1.5
-
-
-def get_estimated_start_from_limit(limit, end_epoch, resolution_str, resolution_multiplier):
-
-    nyse = mcal.get_calendar('NYSE')
-    required_length = ceil(limit * OVERESTIMATE_CONSTANT)
-    resolution = time_interval_to_seconds(resolution_str)
-
-    if resolution == 60 and limit < (1440 / resolution_multiplier):
-        return end_epoch - 4 * 86400  # worst case is three day weekend at 9:30am open
-
-    if resolution == 3600 and limit < (24 / resolution_multiplier):
-        return end_epoch - 4 * 86400  # worst case is three day weekend at 9:30am open
-
-    temp_start = end_epoch - limit * resolution * resolution_multiplier
-    end_date = dt.fromtimestamp(end_epoch, tz=timezone.utc)
-    start_date = dt.fromtimestamp(temp_start, tz=timezone.utc)
-
-    schedule = nyse.schedule(start_date=start_date, end_date=end_date)
-    date_range = mcal.date_range(schedule, frequency='1D')
-
-    count = 1
-    while len(date_range) < required_length:
-        temp_start -= 3600 * OVERESTIMATE_CONSTANT * count
-        start_date = dt.fromtimestamp(temp_start)
-        schedule = nyse.schedule(start_date=start_date, end_date=end_date)
-        date_range = mcal.date_range(schedule, frequency='1D')
-        count += 1
-
-    return temp_start
-
-
 class AttributeDict(dict):
     """
     This is adds functions to the dictionary class, no other modifications. This gives dictionaries abilities like:
@@ -698,6 +670,7 @@ class AttributeDict(dict):
     Basically you can get and set attributes with a dot instead of [] - like dict.available rather than
      dict['available']
     """
+
     def __getattr__(self, attr):
         # Try catch is wrapped to support copying objects
         try:
@@ -743,6 +716,7 @@ class Email:
     Object wrapper for simplifying interaction with SMTP servers & the blankly.reporter.email function.
     Alternatively a notify.json can be created which automatically integrates with blankly.reporter.email()
     """
+
     def __init__(self, smtp_server: str, sender_email: str, password: str, port: int = 465):
         """
         Create the email wrapper:
@@ -768,7 +742,7 @@ def send(self, receiver_email: str, message: str):
                                receiver_email=receiver_email, password=self.__password, port=self.__port)
 
 
-def count_decimals(number) -> int:
+def count_decimals(number: float) -> int:
     """
     Count the number of decimals in a given float: 1.4335 -> 4 or 3 -> 0
     """
@@ -794,11 +768,13 @@ 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
 
 
@@ -806,10 +782,12 @@ def order_protection(func):
     """
     Decorator to provide protection against live orders inside backtest environment
     """
+
     def wrapper(*args, **kwargs):
         if blankly._backtesting:
             raise Exception("Blocked attempt at live order inside backtesting environment")
         return func(*args, **kwargs)
+
     return wrapper
 
 
@@ -838,6 +816,10 @@ def increment_to_precision(increment: float) -> int:
     return math.floor(-math.log10(increment))
 
 
+def precision_to_increment(precision: int) -> float:
+    return 10 ** (-precision)
+
+
 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]
@@ -857,7 +839,7 @@ def aggregate_prices_by_resolution(price_dict, symbol_, resolution_, data_) -> d
     return price_dict
 
 
-def extract_price_by_resolution(prices, symbol, epoch_start, epoch_stop, resolution,):
+def extract_price_by_resolution(prices, symbol, epoch_start, epoch_stop, resolution, ):
     if symbol in prices:
         if resolution in prices[symbol]:
             price_set = prices[symbol][resolution]
@@ -867,3 +849,15 @@ def extract_price_by_resolution(prices, symbol, epoch_start, epoch_stop, resolut
         raise LookupError(f"Prices for this symbol ({symbol}) not found")
 
     return trim_df_time_column(price_set, epoch_start - resolution, epoch_stop)
+
+
+def build_order_info(price, side, size, symbol, type_) -> dict:
+    order = {
+        'size': size,
+        'side': side,
+        'symbol': symbol,
+        'type': type_
+    }
+    if price:
+        order['price'] = price
+    return order
diff --git a/examples/futures_rsi.py b/examples/futures_rsi.py
index d3b86091..01077bda 100644
--- a/examples/futures_rsi.py
+++ b/examples/futures_rsi.py
@@ -18,50 +18,44 @@
 
 import blankly
 from blankly import Side
-from blankly.futures import FuturesStrategyState, FTXFutures, FuturesStrategy, MarginType
+from blankly.futures import FuturesStrategyState, FuturesStrategy, BinanceFutures
+
+from blankly.futures.utils import close_position
 
 
 def price_event(price, symbol, state: FuturesStrategyState):
     state.variables['history'].append(price)
+
     rsi = blankly.indicators.rsi(state.variables['history'])
 
+    position = state.interface.get_position('BTC-USDT')
+    position_size = position['size'] if position else 0
+
     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)
+        side = Side.BUY
     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)
+        side = Side.SELL
+    else:
+        close_position(symbol, state)
+        return
 
+    if (position_size != 0) and (side == Side.BUY) == (position_size > 0):
+        return
 
-def init(symbol, state: FuturesStrategyState):
-    close_position(symbol, state)
+    order_size = (state.interface.cash / price) * 0.99
 
-    # Set initial leverage and margin type
-    state.interface.set_leverage(symbol, 1)
+    if order_size <= 0:
+        return
 
-    state.interface.set_margin_type(symbol, MarginType.ISOLATED)
+    state.interface.market_order(symbol, side=side, size=order_size)
+
+
+def init(symbol, state: FuturesStrategyState):
+    close_position(symbol, state)
 
     state.variables['history'] = state.interface.history(
         symbol, to=150, return_as='deque',
@@ -69,13 +63,13 @@ def init(symbol, state: FuturesStrategyState):
 
 
 if __name__ == "__main__":
-    exchange = FTXFutures()
+    exchange = BinanceFutures()
 
     strategy = FuturesStrategy(exchange)
     strategy.add_price_event(price_event,
                              init=init,
                              teardown=close_position,
                              symbol='BTC-USDT',
-                             resolution='1m')
+                             resolution='5m')
 
-    strategy.start()
+    strategy.backtest(to='1mo', initial_values={'USDT': 10000})
diff --git a/examples/futures_tutorial.py b/examples/futures_tutorial.py
new file mode 100644
index 00000000..9212c46b
--- /dev/null
+++ b/examples/futures_tutorial.py
@@ -0,0 +1,43 @@
+import blankly
+from blankly import futures, Side
+from blankly.futures import FuturesStrategyState
+from blankly.futures.utils import close_position
+
+
+def price_event(price, symbol, state: FuturesStrategyState):
+    prev_price = state.variables['prev_price']
+    position = state.interface.get_position(symbol)
+
+    # if the price rose more than 1,000 and we don't already have a short position, then short sell
+    if not position and price - prev_price >= 1000:
+        order_size = (state.interface.cash / price) * 0.99
+        state.interface.market_order(symbol, Side.SELL, order_size)
+
+    # if the price stablized and we *do* have a short position, close our position.
+    elif position and abs(price - prev_price) <= 100:
+        # we use abs(position['size']) here because position['size'] can (and will) be negative, since we have taken a short position.
+        state.interface.market_order(symbol, Side.BUY, abs(position['size']), reduce_only=True)
+
+    state.variables['prev_price'] = price
+
+
+# This function will be run before our algorithm starts
+def init(symbol, state: FuturesStrategyState):
+    # Close any open positions
+    close_position(symbol, state)
+
+    # Give the algo the previous price as context
+    last_price = state.interface.history(symbol, to=1, return_as='deque', resolution=state.resolution)['close'][-1]
+    state.variables['prev_price'] = last_price
+
+
+if __name__ == "__main__":
+    exchange = futures.BinanceFutures()
+    strategy = futures.FuturesStrategy(exchange)
+
+    strategy.add_price_event(price_event, init=init, teardown=close_position, symbol='BTC-USDT', resolution='1d')
+
+    if blankly.is_deployed:
+        strategy.start()
+    else:
+        strategy.backtest(to='1y', initial_values={'USDT': 10000})
diff --git a/examples/golden_cross.py b/examples/golden_cross.py
index c29d930a..bab914ac 100644
--- a/examples/golden_cross.py
+++ b/examples/golden_cross.py
@@ -29,8 +29,8 @@ def price_event(price, symbol, state: StrategyState):
     slope_sma50 = (sma50[-1] - sma50[-5]) / 5  # get the slope of the last 5 SMA50 Data Points
     prev_diff = diff[-2]
     curr_diff = diff[-1]
-    is_cross_up = slope_sma50 > 0 and curr_diff >= 0 and prev_diff < 0
-    is_cross_down = slope_sma50 < 0 and curr_diff <= 0 and prev_diff > 0
+    is_cross_up = slope_sma50 > 0 > prev_diff and curr_diff >= 0
+    is_cross_down = slope_sma50 < 0 < prev_diff and curr_diff <= 0
     # comparing prev diff with current diff will show a cross
     if is_cross_up and not variables['owns_position']:
         interface.market_order(symbol, 'buy', int(interface.cash/price))
diff --git a/examples/leveragedSPY.py b/examples/leveragedSPY.py
index bf4a6320..bdc021c5 100644
--- a/examples/leveragedSPY.py
+++ b/examples/leveragedSPY.py
@@ -5,8 +5,8 @@
 
 
 def compare_price_event(prices, symbols, state: StrategyState):
-    ''' Strategy: When the market is doing well, the strategy takes on more risk by holding a leveraged S&P 500 ETF.
-                    When the market is shaky, it folds into treasury bonds.'''
+    """ Strategy: When the market is doing well, the strategy takes on more risk by holding a leveraged S&P 500 ETF.
+                    When the market is shaky, it folds into treasury bonds."""
     # keep track of history (close price) of all four tickers:
     # - 'BND' (Vanguard Total Bond Market Index Fund ETF)
     # - 'BIL' (SPDR Bloomberg 1-3 Month T-Bill ETF)
@@ -21,8 +21,10 @@ def compare_price_event(prices, symbols, state: StrategyState):
         return
 
     # Calculate the 60d cumulative sum of BND and BIL
-    state.variables['cum_return_BND_60'] = cum_returns(state.variables['BND_history'][-60], state.variables['BND_history'][-1])
-    state.variables['cum_return_BIL_60'] = cum_returns(state.variables['BIL_history'][-60], state.variables['BIL_history'][-1])
+    state.variables['cum_return_BND_60'] = cum_returns(state.variables['BND_history'][-60],
+                                                       state.variables['BND_history'][-1])
+    state.variables['cum_return_BIL_60'] = cum_returns(state.variables['BIL_history'][-60],
+                                                       state.variables['BIL_history'][-1])
 
     # If 60d cumulative return of BND is greater than 60d cumulative return of BIL -> market is doing well
     if state.variables['cum_return_BND_60'] > state.variables['cum_return_BIL_60']:
@@ -33,7 +35,7 @@ def compare_price_event(prices, symbols, state: StrategyState):
 
         # Buy the UPRO shares if we have any cash left
         price = state.variables['UPRO_history'][-1]
-        size = trunc(state.interface.cash/price, 2)
+        size = trunc(state.interface.cash / price, 2)
         if size > 0:
             state.interface.market_order(symbol='UPRO', side='buy', size=size)
     else:
@@ -43,7 +45,7 @@ def compare_price_event(prices, symbols, state: StrategyState):
             state.interface.market_order(symbol='UPRO', side='sell', size=curr_value_UPRO)
         # Buy the IEF shares if we have any cash left (treasury bond)
         price = state.variables['IEF_history'][-1]
-        size = trunc(state.interface.cash/price, 2)
+        size = trunc(state.interface.cash / price, 2)
         if size > 0:
             state.interface.market_order(symbol='IEF', side='buy', size=size)
 
@@ -53,13 +55,12 @@ def init(symbols, state: StrategyState):
     for symbol in symbols:
         history_name = str(symbol) + '_history'
         state.variables[history_name] = state.interface.history(symbol, to=150, return_as='deque',
-                                                         resolution=state.resolution)['close']
+                                                                resolution=state.resolution)['close']
     # Initialize the variables needed for the compare_price_event
     state.variables['cum_return_BND_60'] = 0
     state.variables['cum_return_BIL_60'] = 0
 
 
-
 if __name__ == "__main__":
     # Authenticate Alpaca strategy
     exchange = Alpaca(portfolio_name="epic portfolio")
@@ -74,4 +75,4 @@ def init(symbols, state: StrategyState):
     # strategy.start()
     # Or backtest using this
     results = strategy.backtest(to='2y', initial_values={'USD': 10000})
-    print(results)
\ No newline at end of file
+    print(results)
diff --git a/examples/settings.json b/examples/settings.json
index ff677fce..e194252e 100644
--- a/examples/settings.json
+++ b/examples/settings.json
@@ -3,6 +3,7 @@
     "use_sandbox_websockets": false,
     "websocket_buffer_size": 10000,
     "test_connectivity_on_auth": true,
+    "auto_truncate": true,
     "coinbase_pro": {
       "cash": "USD"
     },
@@ -11,8 +12,7 @@
       "binance_tld": "us"
     },
     "binance_futures": {
-      "cash": "USDT",
-      "margin_type": "USDT-M"
+      "cash": "USDT"
     },
     "alpaca": {
       "websocket_stream": "iex",
diff --git a/examples/take_profit.py b/examples/take_profit.py
new file mode 100644
index 00000000..c84e309c
--- /dev/null
+++ b/examples/take_profit.py
@@ -0,0 +1,37 @@
+import blankly
+from blankly import Side
+
+
+def price_event(price, symbol, state: blankly.StrategyState):
+    order = state.variables.get('order', None)
+
+    if order is None:
+        market = state.interface.market_order(symbol, Side.BUY, state.interface.cash / price)
+        order_size = state.interface.get_account(symbol)['available']
+
+        # place a take-profit order at 20% above the current price
+        stop_loss = state.interface.take_profit_order(symbol, price * 1.2, order_size)
+
+        state.variables['order'] = stop_loss.get_id()
+        return
+
+    order_status = state.interface.get_order(symbol, state.variables['order'])['status']
+    if order_status in ('done', 'filled'):
+        # our take-profit order was executed
+        pass
+
+
+if __name__ == "__main__":
+    # Authenticate coinbase pro strategy
+    exchange = blankly.CoinbasePro()
+
+    # Use our strategy helper on coinbase pro
+    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='ETH-USD', resolution='1d')
+
+    if blankly.is_deployed:
+        strategy.start()
+    else:
+        strategy.backtest(to='1y', initial_values={'USD': 10000})
diff --git a/setup.py b/setup.py
index 4340fc97..15e19824 100644
--- a/setup.py
+++ b/setup.py
@@ -14,9 +14,9 @@
     name='blankly',  # How you named your package folder (MyLib)
     packages=find_packages(),
     # packages=['blankly'],  # Potentially should be the same thing as name
-    version='v1.17.9-beta',
+    version='v1.17.12-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
+    description='Rapidly build, backtest & deploy trading bots',  # Give a short description about your library
     long_description=long_description,
     long_description_content_type='text/markdown',
     ext_modules=[
@@ -35,10 +35,9 @@
         'alpaca-trade-api >= 1.4.2',
         'bokeh >= 2.4.2',
         'dateparser >= 1.1.0',
-        'newtulipy >= 0.4.6',
+        'newnewtulipy >= 0.4.6.3',
         'numpy >= 1.21.4',
         'pandas >= 1.1.5',
-        'pandas_market_calendars >= 3.2',
         'python-binance >= 1.0.15',
         'requests >= 2.26.0',
         'websocket-client >= 1.2.1',


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 8c356c3c..493313fe 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,6 +1,6 @@
 # This runs all blankly Module Tests
 
-name: blankly Module Tests
+name: Blankly Module Tests
 
 on: [push, workflow_dispatch]
 
diff --git a/blankly/exchanges/interfaces/paper_trade/abc_backtest_controller.py b/blankly/exchanges/interfaces/paper_trade/abc_backtest_controller.py
index 303e00e6..e6866ea8 100644
--- a/blankly/exchanges/interfaces/paper_trade/abc_backtest_controller.py
+++ b/blankly/exchanges/interfaces/paper_trade/abc_backtest_controller.py
@@ -17,7 +17,7 @@
 """
 import abc
 import typing
-from blankly.data.data_reader import PriceReader, EventReader, TickReader
+from blankly.data.data_reader import PriceReader, JsonEventReader, TickReader, DataReader
 
 
 class ABCBacktestController(abc.ABC):
@@ -40,7 +40,7 @@ def add_custom_prices(self, price_reader: PriceReader):
         pass
 
     @abc.abstractmethod
-    def add_custom_events(self, event_reader: EventReader):
+    def add_custom_events(self, event_reader: DataReader):
         pass
 
     @abc.abstractmethod
diff --git a/blankly/exchanges/interfaces/paper_trade/backtest/format_platform_result.py b/blankly/exchanges/interfaces/paper_trade/backtest/format_platform_result.py
index c252d729..1a585cfc 100644
--- a/blankly/exchanges/interfaces/paper_trade/backtest/format_platform_result.py
+++ b/blankly/exchanges/interfaces/paper_trade/backtest/format_platform_result.py
@@ -64,10 +64,8 @@ def __compress_dict_series(values_column: dict, time_dictionary: 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]]
+        # This used to compress by saving the last value, but now it does not
+        output_dict[time_dictionary[time_keys[i]]] = values_column[values_keys[i]]
 
     return output_dict
 
@@ -93,12 +91,12 @@ def __parse_backtest_trades(trades: list, limit_executed: list, limit_canceled:
             # 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']
+                    trades[i]['executed_time'] = j['executed_time']
                     break
 
             for j in limit_canceled:
                 if trades[i]['id'] == j['id']:
-                    trades[i]['canceledTime'] = j['canceled_time']
+                    trades[i]['canceled_time'] = j['canceled_time']
                     break
         elif trades[i]['type'] == 'market':
             # This adds in the execution price for the market orders
@@ -137,8 +135,6 @@ def format_platform_result(backtest_result):
             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
diff --git a/blankly/exchanges/interfaces/paper_trade/backtest_controller.py b/blankly/exchanges/interfaces/paper_trade/backtest_controller.py
index e3a59390..73e95acb 100644
--- a/blankly/exchanges/interfaces/paper_trade/backtest_controller.py
+++ b/blankly/exchanges/interfaces/paper_trade/backtest_controller.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 json
 import os
 import time
@@ -35,6 +36,7 @@
 
 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.futures.futures_paper_trade_interface import FuturesPaperTradeInterface
 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, write_backtest_preferences, info_print, update_progress, \
@@ -44,7 +46,7 @@
 
 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
+from blankly.data.data_reader import PriceReader, TickReader, DataReader, FundingRateEventReader
 
 
 def to_string_key(separated_list):
@@ -436,8 +438,8 @@ def sort_identifier(identifier_: dict):
                                                                    f'{symbol},'
                                                                    f'{int(j[0])},'
                                                                    f'{int(j[1]) + resolution},'  # This adds resolution 
-                                                                                                 # back to the exported
-                                                                                                 # time series
+                        # back to the exported
+                        # time series
                                                                    f'{resolution}.csv'),
                                         index=False)
 
@@ -532,7 +534,7 @@ def add_prices(self,
     def add_custom_prices(self, price_reader: PriceReader):
         self.__price_readers.append(price_reader)
 
-    def add_custom_events(self, event_reader: EventReader):
+    def add_custom_events(self, event_reader: DataReader):
         self.__event_readers.append(event_reader)
 
     def add_tick_events(self, tick_reader: TickReader):
@@ -607,8 +609,8 @@ def format_account_data(self, interface: PaperTradeInterface, local_time) -> \
                                  int, typing.Any]]]:
 
         # This is done so that only traded assets are evaluated.
-        true_available = {}
-        true_account = {}
+        true_available: dict = {}
+        true_account: dict = {}
         for i in interface.traded_assets:
             # Grab the account status
             true_account[i] = interface.get_account(i)
@@ -621,7 +623,11 @@ def format_account_data(self, interface: PaperTradeInterface, local_time) -> \
         no_trade_value = 0
 
         # Save this up front so that it can be removed from the price calculation (it's always a value of 1 anyway)
-        quote_value = true_account[self.quote_currency]['available'] + true_account[self.quote_currency]['hold']
+        try:
+            quote_value = true_account[self.quote_currency]['available'] + true_account[self.quote_currency]['hold']
+        except KeyError as e:
+            raise KeyError(f"Failed looking up {e}. Try changing your quote_account_value_in in backtest.json to be "
+                           f"{e}.")
         try:
             del true_account[self.quote_currency]
         except KeyError:
@@ -634,7 +640,9 @@ def format_account_data(self, interface: PaperTradeInterface, local_time) -> \
             currency_pair = i
 
             # Convert to quote (this could be optimized a bit)
-            if interface.get_exchange_type() != 'alpaca':
+            is_stonks = interface.get_exchange_type() == 'alpaca'
+            is_future = currency_pair.endswith('PERP')
+            if not (is_stonks or is_future):
                 currency_pair += '-'
                 currency_pair += self.quote_currency
 
@@ -644,8 +652,8 @@ def format_account_data(self, interface: PaperTradeInterface, local_time) -> \
             except KeyError:
                 # Must be a currency we have no data for
                 price = 0
-            value_total += price * true_available[i]
-            no_trade_value += price * no_trade_available[i]
+            value_total += price * abs(true_available[i])
+            no_trade_value += price * abs(no_trade_available[i])
 
         # Make sure to add the time key in
         true_available['time'] = local_time
@@ -685,6 +693,8 @@ def advance_time_and_price_index(self):
         def handle_blankly_tick(type_: str, data):
             if type_ == 'tick':
                 self.model.websocket_update(data)
+            elif type_ == "funding_rate":
+                self.interface.do_funding(data['symbol'], data['rate'])
 
         def run_events():
             # Ensure that we don't index error here
@@ -787,7 +797,7 @@ def run(self,
         self.backtest_settings_path = backtest_settings_path
         self.show_progress = self.preferences['settings']['show_progress_during_backtest']
 
-        if not exchange.get_type() == "paper_trade":
+        if not exchange.get_type().endswith("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()
@@ -796,11 +806,15 @@ def run(self,
 
         # Figure out our traded assets here
         self.prices = self.sync_prices()
+        # add funding rate events for futures trading
+        if isinstance(self.interface, FuturesPaperTradeInterface):
+            for symbol in self.prices:
+                self.add_custom_events(FundingRateEventReader(symbol, self.user_start, self.user_stop, self.interface))
         # Now ensure all events are processed
         self.parse_events()
-        for i in self.prices:
-            base = get_base_asset(i)
-            quote = get_quote_asset(i)
+        for symbol in self.prices:
+            base = get_base_asset(symbol)
+            quote = get_quote_asset(symbol)
             if base not in self.interface.traded_assets:
                 self.interface.traded_assets.append(base)
             if quote not in self.interface.traded_assets:
@@ -865,6 +879,7 @@ def check_if_any_column_has_prices(price_dict: dict) -> bool:
 
             # Find the first time in the list
             self.initial_time = copy.copy(self.user_start)
+            self.interface.initial_time = self.initial_time
 
         if self.prices == {} and self.events == []:
             raise ValueError("No data given. "
@@ -915,6 +930,8 @@ def check_if_any_column_has_prices(price_dict: dict) -> bool:
                 update_progress(1)
         except Exception:
             traceback.print_exc()
+        finally:
+            self.model.teardown()
 
         # Reset time to indicate we are no longer in a backtest
         self.time = None
@@ -981,11 +998,11 @@ def is_number(s):
         except ZeroDivisionError as e_:
             metrics_indicators['Cumulative Returns (%)'] = f'failed: {e_}'
 
-        def attempt(math_callable: typing.Callable, dict_of_dataframes: dict, kwargs: dict = None):
+        def attempt(math_callable: typing.Callable, dict_of_dataframes: dict, kwargs_: dict = None):
             try:
-                if kwargs is None:
-                    kwargs = {}
-                result = math_callable(dict_of_dataframes, **kwargs)
+                if kwargs_ is None:
+                    kwargs_ = {}
+                result = math_callable(dict_of_dataframes, **kwargs_)
                 if result == np.NAN:
                     result = None
                 return result
@@ -1042,9 +1059,9 @@ def attempt(math_callable: typing.Callable, dict_of_dataframes: dict, kwargs: di
         history_and_returns['returns'] = history_and_returns['returns'].where(history_and_returns['returns'].notnull(),
                                                                               None)
         # Lastly remove Nan values in the metrics
-        for i in metrics_indicators:
-            if not isinstance(metrics_indicators[i], str) and np.isnan(metrics_indicators[i]):
-                metrics_indicators[i] = None
+        for symbol in metrics_indicators:
+            if not isinstance(metrics_indicators[symbol], str) and np.isnan(metrics_indicators[symbol]):
+                metrics_indicators[symbol] = None
 
         # Assign all these new values back to the result object
         result_object.history_and_returns = history_and_returns
@@ -1165,7 +1182,7 @@ def add_trace(self_, figure_, time__, data_, label):
                     'successful': True,
                     'status_summary': 'Completed',
                     'status_details': '',
-                    'time_elapsed': stop_clock-start_clock,
+                    'time_elapsed': stop_clock - start_clock,
                     'backtest_id': platform_result['backtest_id']
                 }, headers={
                     'api_key': api_key,
diff --git a/blankly/exchanges/interfaces/paper_trade/backtest_result.py b/blankly/exchanges/interfaces/paper_trade/backtest_result.py
index 0a5198d9..c875b12a 100644
--- a/blankly/exchanges/interfaces/paper_trade/backtest_result.py
+++ b/blankly/exchanges/interfaces/paper_trade/backtest_result.py
@@ -17,7 +17,7 @@
 """
 
 from pandas import DataFrame, to_datetime, Timestamp
-from blankly.utils.utils import time_interval_to_seconds as _time_interval_to_seconds, info_print
+from blankly.utils import time_interval_to_seconds as _time_interval_to_seconds, info_print
 
 
 class BacktestResult:
diff --git a/blankly/exchanges/interfaces/paper_trade/backtesting_wrapper.py b/blankly/exchanges/interfaces/paper_trade/backtesting_wrapper.py
index 9059f99d..29b302d6 100644
--- a/blankly/exchanges/interfaces/paper_trade/backtesting_wrapper.py
+++ b/blankly/exchanges/interfaces/paper_trade/backtesting_wrapper.py
@@ -26,6 +26,9 @@ def __init__(self):
             'time': 0
         }
 
+        # Use this in the inits
+        self.initial_time = None
+
         self.full_prices = {}
 
     def set_backtesting(self, status: bool):
diff --git a/examples/keyless_backtest.py b/examples/keyless_backtest.py
index 09070091..d1fd11b3 100644
--- a/examples/keyless_backtest.py
+++ b/examples/keyless_backtest.py
@@ -11,10 +11,10 @@ def price_event(price, symbol, state: blankly.StrategyState):
     if rsi[-1] < 30 and not curr_value:
         # Dollar cost average buy
         buy = state.interface.cash / price
-        state.interface.market_order(symbol, side='buy', size=blankly.trunc(buy, 5))
+        state.interface.market_order(symbol, side='buy', size=buy)
     elif rsi[-1] > 70 and curr_value:
         # Dollar cost average sell
-        state.interface.market_order(symbol, side='sell', size=blankly.trunc(curr_value, 5))
+        state.interface.market_order(symbol, side='sell', size=curr_value)
 
 
 def init(symbol, state: blankly.StrategyState):
diff --git a/tests/cli/test_init/test_init.py b/tests/cli/test_init/test_init.py
deleted file mode 100644
index 495f2469..00000000
--- a/tests/cli/test_init/test_init.py
+++ /dev/null
@@ -1,128 +0,0 @@
-import json
-import os
-import unittest
-import shutil
-import subprocess
-
-
-def convert_binance(exchange_name):
-    if exchange_name == "binance.us" or exchange_name == "binance.com":
-        return 'binance'
-    else:
-        return exchange_name
-
-
-def run_cli(commands: list, cwd=f'./tests/cli'):
-    my_env = os.environ.copy()
-    if 'PYTHONPATH' not in my_env:
-        my_env["PYTHONPATH"] = os.path.abspath('./')
-
-    script_abs = os.path.abspath('./blankly/deployment/cli.py')
-    all_commands = ['python', script_abs] + commands
-    p = subprocess.Popen(all_commands, cwd=cwd, env=my_env)
-    p.wait()
-
-
-def check_backtest_settings(settings: str, expected_defaults: dict):
-    settings = json.loads(settings)
-    assert(settings['settings']['quote_account_value_in'] == expected_defaults['quote_currency'])
-
-
-def check_deploy_settings(settings: str, expected_defaults: dict):
-    # Just check if it will serialize
-    json.loads(settings)
-    assert True
-
-
-def check_bot_script(bot: str, expected_defaults: dict):
-    assert(expected_defaults['exchange'] in bot)
-
-
-def check_keys_settings(keys: str, expected_defaults: dict):
-    keys = json.loads(keys)
-    exchange = convert_binance(expected_defaults['exchange_name'])
-    assert(exchange in keys)
-
-
-def check_requirements(requirements: str, expected_defaults: dict):
-    assert('blankly' in requirements)
-
-
-def check_settings(settings: str, expected_defaults: dict):
-    settings = json.loads(settings)
-    exchange = convert_binance(expected_defaults['exchange_name'])
-    if 'tld' in expected_defaults:
-        if settings['settings'][exchange][exchange + '_tld'] != expected_defaults['tld']:
-            print(f"Incorrect tld found in exchange {settings['exchange_name']}")
-            assert False
-
-    assert(settings['settings'][exchange]['cash'] == expected_defaults['quote_currency'])
-
-
-class TestInit(unittest.TestCase):
-    exchanges = ['binance.com', 'binance.us', 'coinbase_pro', 'alpaca', 'ftx', 'oanda']
-    inits_folder = 'generated_inits'
-    exchange_defaults = {
-        'alpaca': {
-            'exchange': 'blankly.Alpaca',
-            'quote_currency': 'USD'
-        },
-        'binance.com': {
-            'tld': 'com',
-            'exchange': 'blankly.Binance',
-            'quote_currency': 'USDT'
-        },
-        'binance.us': {
-            'tld': 'us',
-            'exchange': 'blankly.Binance',
-            'quote_currency': 'USDT'
-        },
-        'coinbase_pro': {
-            'exchange': 'blankly.CoinbasePro',
-            'quote_currency': 'USD'
-        },
-        'ftx': {
-            'exchange': 'blankly.FTX',
-            'quote_currency': 'USD'
-        },
-        'kucoin': {
-            'exchange': 'blankly.Kucoin',
-            'quote_currency': 'USDT'
-        },
-        'oanda': {
-            'exchange': 'blankly.Oanda',
-            'quote_currency': 'USD'
-        }
-    }
-
-    @classmethod
-    def setUpClass(cls) -> None:
-        try:
-            shutil.rmtree(f'./tests/cli/test_init/{cls.inits_folder}')
-        except FileNotFoundError:
-            print("No previous test folder found...")
-        os.mkdir(f'./tests/cli/test_init/{cls.inits_folder}')
-        for i in cls.exchanges:
-            os.mkdir(f'./tests/cli/test_init/{cls.inits_folder}/{i}')
-
-    def test_init(self):
-        def parse_init_folder(filepath: str, exchange):
-            files = ['backtest.json', 'blankly.json', 'bot.py', 'keys.json', 'requirements.txt', 'settings.json']
-            evaluators = [check_backtest_settings, check_deploy_settings, check_bot_script, check_keys_settings,
-                          check_requirements, check_settings]
-            for j in range(len(files)):
-                contents = open(filepath + '/' + files[j]).read()
-
-                # Copy the exchange name root into the dict
-                defaults_with_root = self.exchange_defaults[exchange]
-                defaults_with_root['exchange_name'] = exchange
-
-                # Evaluate here
-                evaluators[j](contents, defaults_with_root)
-
-            # This passed
-            self.assertTrue(True)
-
-        for i in self.exchanges:
-            run_cli(['init', i], cwd=f'./tests/cli/test_init/{self.inits_folder}/{i}')
-            parse_init_folder(f'./tests/cli/test_init/{self.inits_folder}/{i}', i)
diff --git a/tests/config/cancel_orders.py b/tests/config/cancel_orders.py
new file mode 100644
index 00000000..91f47c96
--- /dev/null
+++ b/tests/config/cancel_orders.py
@@ -0,0 +1,21 @@
+import blankly
+
+if __name__ == "__main__":
+    exchanges = [
+        blankly.Alpaca(),
+        blankly.Binance(),
+        blankly.CoinbasePro(),
+        blankly.Kucoin(),
+        blankly.Oanda()
+    ]
+
+    for i in exchanges:
+        try:
+            orders = i.interface.get_open_orders()
+
+            for order in orders:
+                print(f"Canceling {order['id']} on {order['symbol']}")
+                i.interface.cancel_order(order['symbol'], order['id'])
+        except Exception as e:
+            print(e)
+            continue
diff --git a/tests/config/settings.json b/tests/config/settings.json
index 18d75567..e7dd3b5c 100644
--- a/tests/config/settings.json
+++ b/tests/config/settings.json
@@ -17,6 +17,9 @@
       "cash": "USDT",
       "binance_tld": "us"
     },
+    "binance_futures": {
+      "cash": "USDT"
+    },
     "ftx": {
       "ftx_tld": "us"
     },
diff --git a/tests/config/usdt_backtest.json b/tests/config/usdt_backtest.json
new file mode 100644
index 00000000..b1da8b24
--- /dev/null
+++ b/tests/config/usdt_backtest.json
@@ -0,0 +1,19 @@
+{
+  "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": "USDT",
+    "ignore_user_exceptions": true,
+    "risk_free_return_rate": 0.0
+  }
+}
diff --git a/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py b/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py
index e6d6b10a..170e7378 100644
--- a/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py
+++ b/tests/exchanges/interfaces/alpaca/test_alpaca_interface.py
@@ -24,7 +24,7 @@
 import blankly
 
 
-class mock_alpaca_direct_calls:
+class MockAlpacaDirectCalls:
     def __init__(self, *args, **kwargs):
         pass
 
@@ -189,7 +189,7 @@ def list_assets(self, *args, **kwargs):
         return mock_products_response
 
 
-class mock_alpaca_interface:
+class MockAlpacaInterface:
     def __init__(self, *args, **kwargs):
         pass
 
@@ -202,7 +202,7 @@ def alpaca_mock_interface(mocker: MockerFixture):
     keys_file_path = Path("tests/config/keys.json").resolve().__str__()
     settings_file_path = Path("tests/config/settings.json").resolve().__str__()
 
-    mocker.patch("alpaca_trade_api.REST", new=mock_alpaca_direct_calls)
+    mocker.patch("alpaca_trade_api.REST", new=MockAlpacaDirectCalls)
     alpaca = blankly.Alpaca(keys_path=keys_file_path,
                             settings_path=settings_file_path,
                             portfolio_name="alpaca test portfolio")
diff --git a/tests/exchanges/interfaces/alpaca/test_alpaca_interface_functional.py b/tests/exchanges/interfaces/alpaca/test_alpaca_interface_functional.py
index bb832c8e..aaca7c58 100644
--- a/tests/exchanges/interfaces/alpaca/test_alpaca_interface_functional.py
+++ b/tests/exchanges/interfaces/alpaca/test_alpaca_interface_functional.py
@@ -22,8 +22,6 @@
 from pathlib import Path
 
 import alpaca_trade_api
-import dateparser
-import dateparser as dp
 import pytest
 import pytz
 
@@ -127,16 +125,18 @@ def test_get_product_history(alpaca_interface: AlpacaInterface) -> None:
 
 
 def test_get_product_history_est_timezone(alpaca_interface: AlpacaInterface) -> None:
+    import dateparser
     start = dateparser.parse("2021-02-04 9:30AM EST").timestamp()
     end = dateparser.parse("2021-02-04 9:35AM EST").timestamp()
 
     return_df = alpaca_interface.get_product_history("AAPL", start, end, 60)
-    desired_timestamp = dp.parse("2021-02-04 14:30:00+00:00").timestamp()
+    desired_timestamp = dateparser.parse("2021-02-04 14:30:00+00:00").timestamp()
     assert return_df.iloc[0]['time'] == desired_timestamp
     assert (len(return_df) == 6)
 
 
 def test_get_product_history_custom(alpaca_interface: AlpacaInterface) -> None:
+    import dateparser
     assert isinstance(alpaca_interface.calls, alpaca_trade_api.REST)
     end = dateparser.parse("2021-02-04 9:35AM EST")
 
@@ -157,6 +157,7 @@ def test_get_price(alpaca_interface: AlpacaInterface) -> None:
     price = alpaca_interface.get_price("AAPL")
     assert isinstance(price, float)
 
+
 def test_get_open_orders(alpaca_interface: AlpacaInterface) -> None:
     orders = alpaca_interface.get_open_orders()
     print(orders)
diff --git a/tests/exchanges/interfaces/binance/test_binance_interface.py b/tests/exchanges/interfaces/binance/test_binance_interface.py
index b81b1fa6..9ed027ae 100644
--- a/tests/exchanges/interfaces/binance/test_binance_interface.py
+++ b/tests/exchanges/interfaces/binance/test_binance_interface.py
@@ -27,7 +27,7 @@
 from blankly.utils.utils import AttributeDict
 
 
-class BinanceInterface_test(unittest.TestCase):
+class TestBinanceInterface(unittest.TestCase):
     @classmethod
     def setUpClass(cls) -> None:
         cls.Binance = blankly.Binance(portfolio_name="Spot Test Key",
@@ -119,4 +119,4 @@ def test_get_price(binance_interface: BinanceInterface) -> None:
     # todo: just call a couple of popular coins and see if they all return floats
     btcusd = 'BTC-USDT'
     resp = binance_interface.get_price(btcusd)
-    assert type(resp) is float
\ No newline at end of file
+    assert type(resp) is float
diff --git a/tests/exchanges/interfaces/ftx/test_ftx_interface.py b/tests/exchanges/interfaces/ftx/test_ftx_interface.py
index 495400f5..a737aeb7 100644
--- a/tests/exchanges/interfaces/ftx/test_ftx_interface.py
+++ b/tests/exchanges/interfaces/ftx/test_ftx_interface.py
@@ -38,8 +38,8 @@ def ftx_interface() -> FTXInterface:
     settings_file_path = Path("../../../config/settings.json").resolve()
 
     ftx = blankly.FTX(keys_path=keys_file_path,
-                               settings_path=settings_file_path,
-                               portfolio_name='Main Account')
+                      settings_path=settings_file_path,
+                      portfolio_name='Main Account')
 
     # auth_obj = FTXAuth(str(keys_file_path), "Main Account")
     # _, ftx_interface = DirectCallsFactory.create("ftx", auth_obj, str(settings_file_path))
diff --git a/tests/exchanges/test_interface_homogeneity.py b/tests/exchanges/test_interface_homogeneity.py
index 5498b7ee..e074c8a9 100644
--- a/tests/exchanges/test_interface_homogeneity.py
+++ b/tests/exchanges/test_interface_homogeneity.py
@@ -20,9 +20,9 @@
 import unittest
 from datetime import datetime as dt
 
-import dateparser
 import numpy
 import pandas as pd
+import pytest
 
 import blankly
 from blankly.exchanges.orders.limit_order import LimitOrder
@@ -85,7 +85,6 @@ def setUpClass(cls) -> None:
                                                settings_path="./tests/config/settings.json")
         cls.Coinbase_Pro_Interface = cls.Coinbase_Pro.get_interface()
         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",
@@ -218,17 +217,6 @@ def check_market_order(self, order: MarketOrder, side, 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']
@@ -271,7 +259,7 @@ def check_account_delta(before: dict, after: dict, order: MarketOrder) -> None:
 
             # 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'])
+            self.check_account_delta_market(initial_value, after_value, order_responses[-1]['order'])
 
             order_responses.append({
                 'order': i.market_order(get_valid_symbol(type_), 'sell', size),
@@ -299,9 +287,9 @@ def check_account_delta(before: dict, after: dict, order: MarketOrder) -> None:
                 except Exception as e:
                     print(f"Failed canceling order for reason {e} - may have already executed")
 
-    def check_limit_order(self, limit_order: LimitOrder, expected_side: str, size, product_id):
+    def check_limit_order(self, limit_order: LimitOrder, expected_side: str, size, product_id, type_='limit'):
         self.assertEqual(limit_order.get_side(), expected_side)
-        self.assertEqual(limit_order.get_type(), 'limit')
+        self.assertEqual(limit_order.get_type(), type_)
         self.assertEqual(limit_order.get_time_in_force(), 'GTC')
         # TODO fix status homogeneity
         # self.assertEqual(limit_order.get_status(), {'status': 'new'})
@@ -309,42 +297,18 @@ def check_limit_order(self, limit_order: LimitOrder, expected_side: str, size, p
         self.assertEqual(limit_order.get_symbol(), product_id)
 
     def test_limit_order(self):
-        """
-        This function tests a few components of market orders:
-        - Opening market orders
-        - Monitoring market orders using the order status function
-        - Comparing with open orders
-        - Canceling orders
-        """
         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
-
-                before['available'] = float(before['available'])  # added this and line below
-                after['available'] = float(after['available'])
-
-                before['hold'] = float(before['hold'])  # added this and line below
-                after['hold'] = float(after['hold'])
-
-                self.assertAlmostEqual(before['available'], after['available'] + (order.get_price() * order.get_size()),
-                                       places=1)
-
-                # 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)
-
             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)
+                self.check_account_delta_limit(initial_account, after_buy_account, buy)
 
             sell = interface.limit_order(symbol, 'sell', sell_price, size)
             self.check_limit_order(sell, 'sell', size, symbol)
@@ -445,6 +409,172 @@ def check_account_delta(before: dict, after: dict, order: LimitOrder) -> None:
 
         self.assertTrue(compare_responses(cancels, force_exchange_specific=False))
 
+    def evaluate_tp_sl_order(self, sorted_orders: dict, interface, type_, symbol: str, sell_price: [float, int],
+                             size: [float, int]) -> list:
+        if type_ == 'stop_loss':
+            order_func = interface.stop_loss_order
+        elif type_ == 'take_profit':
+            order_func = interface.take_profit_order
+        else:
+            raise ValueError('order type must be take_profit or stop_loss')
+
+        sell = order_func(symbol, sell_price, size)
+        self.check_limit_order(sell, 'sell', size, symbol, type_)
+        sorted_orders[sell.exchange] = {'sell': sell}
+        return [sell]
+
+    def test_take_profit_order(self):
+        limits = []
+        sorted_orders = {}
+
+        limits += self.evaluate_tp_sl_order(sorted_orders, self.Alpaca_Interface, 'take_profit', 'AAPL', 100000, 1)
+
+        limits += self.evaluate_tp_sl_order(sorted_orders, self.Coinbase_Pro_Interface, 'take_profit', 'BTC-USD',
+                                            100000, 1)
+        limits += self.evaluate_tp_sl_order(sorted_orders, self.Kucoin_Interface, 'take_profit', 'ETH-USDT', 100000,
+                                            1)
+
+        responses = []
+        status = []
+        cancels = []
+
+        open_orders = {
+            'coinbase_pro': self.Coinbase_Pro_Interface.get_open_orders('BTC-USD'),
+            'kucoin': self.Kucoin_Interface.get_open_orders('ETH-USDT'),
+            'alpaca': self.Alpaca_Interface.get_open_orders('AAPL'),
+        }
+
+        # Simple test to ensure that some degree of orders have been placed
+        for i in open_orders:
+            self.assertTrue(len(open_orders[i]) >= 2)
+
+        # Just scan through both simultaneously to reduce code copying
+        all_orders = open_orders['coinbase_pro']
+        all_orders = all_orders + open_orders['kucoin']
+        all_orders = all_orders + open_orders['alpaca']
+
+        # 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 all_orders:
+                if i.get_id() == j['id']:
+                    found = True
+                    self.assertTrue(compare_dictionaries(i.get_response(), j))
+                    break
+            self.assertTrue(found)
+
+        for i in limits:
+            responses.append(i.get_response())
+            status.append(i.get_status(full=True))
+
+        self.assertTrue(compare_responses(responses))
+        self.assertTrue(compare_responses(status))
+
+        cancels.append(self.Kucoin_Interface.cancel_order('ETH-USDT', sorted_orders['kucoin']['sell'].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', sorted_orders['alpaca']['sell'].get_id()))
+
+        self.assertTrue(compare_responses(cancels, force_exchange_specific=False))
+
+    def check_account_delta_market(self, 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'] = float(before['available'])
+        after['available'] = float(after['available'])
+
+        self.assertAlmostEqual(blankly.trunc(before['available'], 2) + order.get_size(),
+                               blankly.trunc(after['available'], 2), delta=1)
+
+    def check_account_delta_limit(self, before: dict, after: dict, order: LimitOrder) -> None:
+        # On a buy the quote asset should get moved to hold
+
+        before['available'] = float(before['available'])  # added this and line below
+        after['available'] = float(after['available'])
+
+        before['hold'] = float(before['hold'])  # added this and line below
+        after['hold'] = float(after['hold'])
+
+        self.assertAlmostEqual(before['available'], after['available'] + (order.get_price() * order.get_size()),
+                               places=1)
+
+        # 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)
+
+    def test_stop_loss_order(self):
+        limits = []
+        sorted_orders = {}
+
+        limits += self.evaluate_tp_sl_order(sorted_orders, self.Alpaca_Interface, 'stop_loss', 'AAPL', 1, 1)
+
+        limits += self.evaluate_tp_sl_order(sorted_orders, self.Coinbase_Pro_Interface, 'stop_loss', 'BTC-USD', 0.01,
+                                            1)
+        limits += self.evaluate_tp_sl_order(sorted_orders, self.Kucoin_Interface, 'stop_loss', 'ETH-USDT', 0.01, 1)
+
+        responses = []
+        status = []
+        cancels = []
+
+        open_orders = {
+            'coinbase_pro': self.Coinbase_Pro_Interface.get_open_orders('BTC-USD'),
+            'kucoin': self.Kucoin_Interface.get_open_orders('ETH-USDT'),
+            'alpaca': self.Alpaca_Interface.get_open_orders('AAPL'),
+        }
+
+        # Simple test to ensure that some degree of orders have been placed
+        for i in open_orders:
+            self.assertTrue(len(open_orders[i]) >= 2)
+
+        # Just scan through both simultaneously to reduce code copying
+        all_orders = open_orders['coinbase_pro']
+        all_orders = all_orders + open_orders['kucoin']
+        all_orders = all_orders + open_orders['alpaca']
+
+        # 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 all_orders:
+                if i.get_id() == j['id']:
+                    found = True
+                    self.assertTrue(compare_dictionaries(i.get_response(), j))
+                    break
+            self.assertTrue(found)
+
+        for i in limits:
+            responses.append(i.get_response())
+            status.append(i.get_status(full=True))
+
+        self.assertTrue(compare_responses(responses))
+        self.assertTrue(compare_responses(status))
+
+        cancels.append(self.Kucoin_Interface.cancel_order('ETH-USDT', sorted_orders['kucoin']['sell'].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', sorted_orders['alpaca']['sell'].get_id()))
+
+        self.assertTrue(compare_responses(cancels, force_exchange_specific=False))
+
     def test_get_keys(self):
         responses = []
         for i in self.interfaces:
@@ -500,6 +630,7 @@ def test_point_based_history(self):
             self.check_product_history_types(response)
 
     def test_point_with_end_history(self):
+        import dateparser
         responses = []
 
         arbitrary_date: dt = dateparser.parse("8/23/21")
@@ -530,6 +661,7 @@ def test_point_with_end_history(self):
             self.check_product_history_types(i[0])
 
     def test_start_with_end_history(self):
+        import dateparser
         responses = []
 
         # This initial selection could fail because of the slightly random day that they delete their data
diff --git a/tests/indicators/test_moving_averages.py b/tests/indicators/test_moving_averages.py
index 41cfb1f5..02d0dc11 100644
--- a/tests/indicators/test_moving_averages.py
+++ b/tests/indicators/test_moving_averages.py
@@ -16,7 +16,6 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-
 import pickle
 import unittest
 from pathlib import Path
@@ -37,7 +36,7 @@ def setUpClass(cls) -> None:
         data_path = Path("tests/config/test_data.p").resolve()
         with open(data_path, 'rb') as f:
             cls.data = pickle.load(f)
-        
+
     def test_one_param_moving_averages(self):
         period = self.data['periods'][0]
 
@@ -97,4 +96,4 @@ def tests_vwma(self):
         volume = self.data['volume']
         for period in self.data['periods']:
             vwma_res = vwma(self.data['close'], volume, period)
-            self.assertTrue(compare_equal(vwma_res, self.data[period]['vwma']))
\ No newline at end of file
+            self.assertTrue(compare_equal(vwma_res, self.data[period]['vwma']))
diff --git a/tests/indicators/test_oscillators.py b/tests/indicators/test_oscillators.py
index c1fa68ae..5747ca70 100644
--- a/tests/indicators/test_oscillators.py
+++ b/tests/indicators/test_oscillators.py
@@ -16,7 +16,6 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-
 import pickle
 import unittest
 from pathlib import Path
@@ -54,7 +53,6 @@ def test_chande_momentum_oscillator(self):
             chande_res = chande_momentum_oscillator(self.data['close'], period)
             self.assertTrue(compare_equal(chande_res, self.data[period]['chande_momentum_oscillator']))
 
-
     def test_absolute_price_oscillator(self):
         short_period = self.data['short_period']
         long_period = self.data['long_period']
@@ -73,6 +71,7 @@ def test_stochastic_oscillator(self):
         pct_k_period = self.data['pct_k_period']
         pct_k_slowing_period = self.data['pct_k_slowing_period']
         pct_d_period = self.data['pct_d_period']
-        res = stochastic_oscillator(self.data['high'], self.data['low'], self.data['close'], pct_k_period, pct_k_slowing_period, pct_d_period)
+        res = stochastic_oscillator(self.data['high'], self.data['low'], self.data['close'], pct_k_period,
+                                    pct_k_slowing_period, pct_d_period)
         # use the first one since general period doesn't matter
-        self.assertTrue(compare_equal(res, self.data[self.data['periods'][0]]['stochastic_oscillator']))
\ No newline at end of file
+        self.assertTrue(compare_equal(res, self.data[self.data['periods'][0]]['stochastic_oscillator']))
diff --git a/tests/indicators/test_statistics.py b/tests/indicators/test_statistics.py
index ab4eb9b4..5a4ed602 100644
--- a/tests/indicators/test_statistics.py
+++ b/tests/indicators/test_statistics.py
@@ -71,4 +71,4 @@ def test_one_param_moving_averages_diff_period(self):
         self.assertTrue(compare_equal(min_res, self.data[period]['min_period']))
 
         max_res = max_period(self.data['close'], period)
-        self.assertTrue(compare_equal(max_res, self.data[period]['max_period']))
\ No newline at end of file
+        self.assertTrue(compare_equal(max_res, self.data[period]['max_period']))
diff --git a/tests/metrics/test_portfolio_metrics.py b/tests/metrics/test_portfolio_metrics.py
index fddd27fc..181376ae 100644
--- a/tests/metrics/test_portfolio_metrics.py
+++ b/tests/metrics/test_portfolio_metrics.py
@@ -16,7 +16,6 @@
     along with this program.  If not, see <https://www.gnu.org/licenses/>.
 """
 
-
 import unittest
 
 from blankly.metrics import *
@@ -34,11 +33,11 @@ def test_cagr(self):
         truth = 0.2247448714
         result = cagr(self.start_value, self.end_value, self.years)
         self.assertAlmostEqual(truth, result)
-    
+
     def test_cum_returns(self):
         value = cum_returns(self.start_value, self.end_value)
         self.assertEqual(0.5, value)
-    
+
     def test_sortino(self):
         truth = 38.93662218111633
         result = sortino(self.returns)
@@ -52,4 +51,4 @@ def test_sharpe(self):
     def test_maxdrawdown(self):
         truth = -0.07880
         result = max_drawdown(self.returns)
-        self.assertAlmostEqual(truth, result)
\ No newline at end of file
+        self.assertAlmostEqual(truth, result)
diff --git a/tests/new_interface_tests/conftest.py b/tests/new_interface_tests/conftest.py
index 4c6bea91..2b2cb390 100644
--- a/tests/new_interface_tests/conftest.py
+++ b/tests/new_interface_tests/conftest.py
@@ -1,26 +1,41 @@
 from _pytest.python import Metafunc
+
+from blankly.exchanges.interfaces.abc_base_exchange_interface import ABCBaseExchangeInterface
 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
+from tests.new_interface_tests.test_utils import FUTURES_EXCHANGES, get_symbols, SPOT_EXCHANGES
 
 
 def gen_id(arg):
-    if isinstance(arg, FuturesExchange):
-        return arg.get_type()
-    elif isinstance(arg, FuturesExchangeInterface):
+    if isinstance(arg, ABCBaseExchangeInterface):
         return arg.get_exchange_type()
     return arg
 
 
 def pytest_generate_tests(metafunc: Metafunc):
-    if 'futures_interface' in metafunc.fixturenames:
+    interface_params = [p_name for p_name in metafunc.fixturenames if p_name.endswith('interface')]
+    if len(interface_params) < 1:
+        return  # no interface params is fine
+
+    if len(interface_params) > 1:
+        raise Exception('maximum one interface parameter per test function')
+
+    interface_param = interface_params[0]
+
+    if interface_param == 'futures_interface':
         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)
+    elif interface_param == 'spot_interface':
+        interfaces = [ex.interface for ex in SPOT_EXCHANGES]
+    elif interface_param == 'interface':
+        interfaces = [ex.interface for ex in FUTURES_EXCHANGES + SPOT_EXCHANGES]
+    else:
+        raise Exception('invalid interface type: ' + interface_param)
 
+    if 'symbol' in metafunc.fixturenames:
+        vals = [(interface, symbol) for interface in interfaces
+                for symbol in get_symbols(interface)]
+        metafunc.parametrize(f'{interface_param},symbol', vals, ids=gen_id)
+    else:
+        metafunc.parametrize(interface_param, interfaces, ids=gen_id)
diff --git a/tests/new_interface_tests/test_interfaces.py b/tests/new_interface_tests/test_interfaces.py
index add0439d..7e453cd7 100644
--- a/tests/new_interface_tests/test_interfaces.py
+++ b/tests/new_interface_tests/test_interfaces.py
@@ -7,56 +7,79 @@
 from _pytest.python_api import approx
 
 from blankly.enums import Side, OrderType, ContractType, OrderStatus, HedgeMode, MarginType, PositionMode, TimeInForce
+from blankly.exchanges.interfaces.abc_base_exchange_interface import ABCBaseExchangeInterface
+from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
 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, \
+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):
+def valid_product_helper(interface: ABCBaseExchangeInterface, 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
+    if isinstance(interface, FuturesExchangeInterface):
+        exc = interface.to_exchange_symbol(symbol)
+        bly = interface.to_blankly_symbol(exc)
+    else:
+        exc = utils.to_exchange_symbol(symbol, interface.get_exchange_type())
+        bly = utils.to_blankly_symbol(exc, interface.get_exchange_type(), quote)
+    assert bly == 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_symbols(interface: ABCBaseExchangeInterface):
+    if interface.get_exchange_type().startswith('alpaca'):
+        pytest.skip('stonk symbols can be whatever they wanna be')
+
+    if isinstance(interface, FuturesExchangeInterface):
+        products = interface.get_products().values()
+    else:
+        products = interface.get_products()
+
+    for product in products:
+        valid_product_helper(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()
+        products = futures_interface.get_position().values()
     for product in products:
         valid_product_helper(futures_interface, product)
 
 
 @homogeneity_testing
-def test_get_products(futures_interface: FuturesExchangeInterface):
+def test_get_products_futures(futures_interface: FuturesExchangeInterface):
     return list(futures_interface.get_products().values())
 
 
 @homogeneity_testing
-def test_get_products_symbol(futures_interface: FuturesExchangeInterface,
-                             symbol: str):
+def test_get_products_spot(spot_interface: ExchangeInterface):
+    return spot_interface.get_products()
+
+
+@homogeneity_testing
+def test_get_products_symbol_futures(futures_interface: FuturesExchangeInterface,
+                                     symbol: str):
     return futures_interface.get_products(symbol)
 
 
 @homogeneity_testing
-def test_get_account(futures_interface: FuturesExchangeInterface):
+def test_get_account_futures(futures_interface: FuturesExchangeInterface):
     return list(futures_interface.get_account().values())
 
 
+@homogeneity_testing
+def test_get_account_spot(spot_interface: FuturesExchangeInterface):
+    return list(spot_interface.get_account().values())
+
+
 @homogeneity_testing
 def test_get_positions(futures_interface: FuturesExchangeInterface):
-    return list(futures_interface.get_positions().values())
+    return list(futures_interface.get_position().values())
 
 
 @homogeneity_testing
@@ -73,11 +96,11 @@ def test_get_positions(futures_interface: FuturesExchangeInterface,
     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)
+    position = futures_interface.get_position(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)
@@ -85,21 +108,27 @@ def test_get_positions(futures_interface: FuturesExchangeInterface,
     return position
 
 
-def test_simple_open_close(futures_interface: FuturesExchangeInterface,
+def test_simple_open_close(interface: ABCBaseExchangeInterface,
                            symbol: str) -> None:
-    close_position(futures_interface, symbol)
+    close_position(interface, symbol)
 
     # buy
-    size = buy(futures_interface, symbol).size
-    assert futures_interface.get_positions(symbol).size == approx(size)
+    if isinstance(interface, FuturesExchangeInterface):
+        size = buy(interface, symbol).size
+        assert interface.get_position(symbol)['size'] == approx(size)
+    else:
+        size = buy(interface, symbol).get_size()
+        base = utils.get_base_asset(symbol)
+        assert interface.get_account(base)['available'] == approx(size)
 
-    close_position(futures_interface, symbol)
+    close_position(interface, symbol)
 
-    # sell
-    size = sell(futures_interface, symbol).size
-    assert futures_interface.get_positions(symbol).size == approx(-size)
+    if isinstance(interface, FuturesExchangeInterface):
+        # sell/short
+        size = sell(interface, symbol).size
+        assert interface.get_position(symbol)['size'] == approx(-size)
 
-    close_position(futures_interface, symbol)
+        close_position(interface, symbol)
 
 
 @homogeneity_testing
@@ -110,7 +139,7 @@ def test_order(futures_interface: FuturesExchangeInterface, symbol: str,
 
     price = futures_interface.get_price(symbol)
     product = futures_interface.get_products(symbol)
-    size = utils.trunc(13 / price, product.size_precision)
+    size = utils.trunc(13 / price, product['size_precision'])
     order = futures_interface.market_order(symbol, side, size)
 
     assert order.symbol == symbol
@@ -131,7 +160,7 @@ def test_order(futures_interface: FuturesExchangeInterface, symbol: str,
     assert res.price == approx(size * price, rel=0.01)
 
     # check our stake is increased/decreased
-    position_size = futures_interface.get_positions(symbol).size
+    position_size = futures_interface.get_position(symbol)['size']
     if side == Side.BUY:
         assert order.size == position_size
     else:
@@ -158,19 +187,19 @@ def test_priced_orders(futures_interface: FuturesExchangeInterface,
 
     price = futures_interface.get_price(symbol)
     product = futures_interface.get_products(symbol)
-    size = utils.trunc(20 / price, product.size_precision)
+    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)
+    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)
+        order = futures_interface.take_profit_order(symbol, side, limit_price, size)
     elif type == OrderType.STOP:
-        order = futures_interface.stop_loss(symbol, side, limit_price, size)
+        order = futures_interface.stop_loss_order(symbol, side, limit_price, size)
 
     assert order.symbol == symbol
     assert 0 <= order.id
@@ -285,13 +314,13 @@ def test_funding_rate_history(futures_interface: FuturesExchangeInterface,
 
     # non-perp contracts don't have funding rates
     if futures_interface.get_products(
-    )[symbol].contract_type != ContractType.PERPETUAL:
+    )[symbol]['contract_type'] != ContractType.PERPETUAL:
         assert len(history) == 0
         return
 
-    # test start and end times
+    # test start time
     assert start <= history[0]['time'] < start + day
-    assert end - day < history[-1]['time'] <= end
+    # assert end - day < history[-1]['time'] <= end
 
     # test ascending order
     assert sorted(history, key=itemgetter('time')) == history
@@ -313,3 +342,13 @@ def test_funding_rate_history(futures_interface: FuturesExchangeInterface,
                 # pytest.fail(f'too many incorrect resolutions: {errors=}')
 
     return history
+
+
+@homogeneity_testing
+def test_taker_fee(futures_interface: FuturesExchangeInterface):
+    return futures_interface.get_taker_fee()
+
+
+@homogeneity_testing
+def test_maker_fee(futures_interface: FuturesExchangeInterface):
+    return futures_interface.get_maker_fee()
diff --git a/tests/new_interface_tests/test_utils.py b/tests/new_interface_tests/test_utils.py
index c7fc7a3d..64cc0781 100644
--- a/tests/new_interface_tests/test_utils.py
+++ b/tests/new_interface_tests/test_utils.py
@@ -1,9 +1,26 @@
-from blankly.enums import OrderStatus, Side, ContractType, OrderType, PositionMode, TimeInForce
+from typing import Union
+
+from blankly.enums import ContractType, PositionMode, TimeInForce, Side, OrderStatus, OrderType
+from blankly.exchanges.interfaces.paper_trade.paper_trade import PaperTrade
+from blankly.exchanges.interfaces.coinbase_pro.coinbase_pro import CoinbasePro
+from blankly.exchanges.interfaces.exchange_interface import ExchangeInterface
+from blankly.exchanges.interfaces.okx.okx import Okx
+from blankly.exchanges.interfaces.kucoin.kucoin import Kucoin
+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.ftx.ftx import FTX
+from blankly.exchanges.abc_base_exchange import ABCBaseExchange
+from blankly.exchanges.interfaces.abc_base_exchange_interface import ABCBaseExchangeInterface
 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.interfaces.paper_trade.futures.futures_paper_trade_interface import FuturesPaperTradeInterface
+from blankly.exchanges.interfaces.paper_trade.paper_trade_interface import PaperTradeInterface
 from blankly.exchanges.orders.futures.futures_order import FuturesOrder
+from blankly.exchanges.orders.limit_order import LimitOrder
+from blankly.exchanges.orders.market_order import MarketOrder
 from blankly.utils import utils
 
 import time
@@ -16,18 +33,83 @@
 
 from contextlib import contextmanager
 
+# futures exchanges
+binance_futures = BinanceFutures(keys_path="./tests/config/keys.json",
+                                 preferences_path="./tests/config/settings.json",
+                                 portfolio_name="Futures Test Key")
+ftx_futures = FTXFutures(keys_path="./tests/config/keys.json",
+                         preferences_path="./tests/config/settings.json",
+                         portfolio_name="Dotcom Test Account")
+
+# spot exchanges
+coinbase_pro = CoinbasePro(portfolio_name="Sandbox Portfolio",
+                           keys_path='./tests/config/keys.json',
+                           settings_path="./tests/config/settings.json")
+okx = Okx(portfolio_name="okx sandbox portfolio",
+          keys_path='./tests/config/keys.json',
+          settings_path="./tests/config/settings.json")
+kucoin = Kucoin(portfolio_name="KC Sandbox Portfolio",
+                keys_path='./tests/config/keys.json',
+                settings_path="./tests/config/settings.json")
+binance = Binance(portfolio_name="Spot Test Key",
+                  keys_path='./tests/config/keys.json',
+                  settings_path="./tests/config/settings.json")
+alpaca = Alpaca(portfolio_name="alpaca test portfolio",
+                keys_path='./tests/config/keys.json',
+                settings_path="./tests/config/settings.json")
+oanda = Oanda(portfolio_name="oanda test portfolio",
+              keys_path='./tests/config/keys.json',
+              settings_path="./tests/config/settings.json")
+ftx = FTX(portfolio_name="Main Account",
+          keys_path='./tests/config/keys.json',
+          settings_path="./tests/config/settings.json")
+
 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"),
+    FuturesPaperTradeInterface(binance_futures.get_type() + '_paper_trade', binance_futures.interface, {'USDT': 1000}),
+    binance_futures,
+    ftx_futures,
+]
+SPOT_EXCHANGES = [
+    # PaperTradeInterface deferring to the subinterface get_exchange_type causes problems...
+    # TODO ?
+    PaperTrade(coinbase_pro, {'USD': 1000}),
+
+    # CoinbasePro testnet *sucks*. Trading on it is always fkn broken and they list like 3 symbols.
+    # It breaks all the tests.
+    # coinbase_pro,
+
+    okx,
+    kucoin,
+    binance,
+    alpaca,
+    oanda,
+    ftx
 ]
-SYMBOLS = {
-    'ftx_futures': ['SOL-USD', 'ETH-USD'],
-    'binance_futures': ['BCH-USDT', 'ETH-USDT']
-}
+
+for exchange in FUTURES_EXCHANGES + SPOT_EXCHANGES:
+    # override auto trunc for new tests
+    # old tests use the default if auto_truncate is not set, which is False
+    exchange.interface.user_preferences['settings']['auto_truncate'] = True
+
+
+def get_symbols(exchange: ABCBaseExchangeInterface):
+    exchange_type = exchange.get_exchange_type()
+
+    # crypto exchanges that use USD
+    if exchange_type.startswith('coinbase') \
+            or exchange_type.startswith('ftx'):
+        return ['BTC-USD']
+
+    # forex
+    if exchange_type.startswith('oanda'):
+        return ['EUR-USD', 'GBP/CAD']
+
+    # stonks
+    if exchange_type.startswith('alpaca'):
+        return ['AAPL', 'MSFT']
+
+    # everything else uses USDT
+    return ['ETH-USDT', 'BCH-USDT']
 
 
 def homogeneity_testing(func=None, check_values: bool = False):
@@ -79,7 +161,12 @@ def compare_values(other_name, other, this, check_values: bool):
         assert this == other, f'comparing to {other_name}: values are not equal.'
 
 
-def wait_till_filled(interface: FuturesExchangeInterface, order: FuturesOrder):
+def wait_till_filled(interface: ABCBaseExchangeInterface, order: Union[FuturesOrder, MarketOrder]):
+    if isinstance(interface, ExchangeInterface):
+        # don't bother, the lack of 'real' order statuses makes this too much of a pain
+        time.sleep(1)
+        return interface.get_order(order.get_symbol(), order.get_id())
+
     retries = 0
     res = interface.get_order(order.symbol, order.id)
     while res.status != OrderStatus.FILLED:
@@ -91,88 +178,126 @@ def wait_till_filled(interface: FuturesExchangeInterface, order: FuturesOrder):
     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
+def place_order(interface: ABCBaseExchangeInterface, symbol: str, side: Side, funds: int, reduce_only: bool = False):
+    if isinstance(interface, FuturesExchangeInterface):
+        product = interface.get_products(symbol)
+        price = interface.get_price(symbol)
+        precision = product['size_precision']
+    else:
+        product = next(p for p in interface.get_products() if p['symbol'] == symbol)
+        price = interface.get_price(symbol)
+        precision = utils.increment_to_precision(product['base_increment'])
+    order_size = utils.trunc(funds / price, precision)
+
+    if isinstance(interface, FuturesExchangeInterface):
+        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
+    else:
+        order = interface.market_order(symbol, side, order_size)
+        assert order.get_symbol() == symbol
+        # TODO these are not properly homogenized for spot markets
+        assert order.get_status()['status'].lower() in ('new', 'open', 'filled', 'done')
+        assert order.get_type().lower() == OrderType.MARKET
+        assert order.get_size() == approx(order_size)
+        assert order.get_side().lower() == side
 
     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
+    if isinstance(interface, FuturesExchangeInterface):
+        assert res.status == OrderStatus.FILLED
     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
+        assert res.get_status()["status"].lower() in ('closed', 'filled')
+
+    if isinstance(interface, FuturesExchangeInterface):
+        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
+    else:
+        assert res.get_price() == approx(price * order_size, rel=0.01)
+        assert res.get_size() == approx(order_size, rel=0.01)
 
     return order
 
 
-def sell(interface: FuturesExchangeInterface, symbol: str, funds: int = 20, reduce_only: bool = False):
+def sell(interface: ABCBaseExchangeInterface, 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):
+def buy(interface: ABCBaseExchangeInterface, 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():
+    for symbol in futures_interface.get_position():
         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
+    assert len(futures_interface.get_position()) == 0
 
 
-def close_position(interface: FuturesExchangeInterface, symbol: str):
+def close_position(interface: ABCBaseExchange, 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)
+    if isinstance(interface, FuturesExchangeInterface):
+        position = interface.get_position(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_position(symbol) is None
     else:
-        pytest.fail('position size is zero')
-    wait_till_filled(interface, order)
-    assert interface.get_positions(symbol) is None
+        base = utils.get_base_asset(symbol)
+        acc = interface.get_account()
+        if base not in acc:
+            return
+        product = next(p for p in interface.get_products() if p['symbol'] == symbol)
+        increment = product['base_increment']
+        precision = utils.increment_to_precision(increment)
+        position = utils.trunc(acc[base]['available'], precision)
+        if 0 < position:
+            order = interface.market_order(symbol, 'sell', position)
+            wait_till_filled(interface, order)
+            assert interface.get_account(base)['available'] == approx(0)
 
 
 @contextmanager
-def cancelling_order(interface: FuturesExchangeInterface, symbol: str):
+def cancelling_order(interface: ABCBaseExchangeInterface, symbol: str):
     """
     Context manager for placing and cancelling a limit order from within a test.
     Useful for testing paths that require active orders.
@@ -186,25 +311,39 @@ def cancelling_order(interface: FuturesExchangeInterface, symbol: str):
     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
+    if isinstance(interface, FuturesExchangeInterface):
+        order_size = utils.trunc(100 / price, product['size_precision'])
+        limit_price = utils.trunc(price * 0.90, 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
+    else:
+        order_size = utils.trunc(100 / price, utils.increment_to_precision(product['base_increment']))
+        limit_price = utils.trunc(price * 0.90, 2)  # 2 should be fine? we don't have quote asset increment here.
+        order = interface.limit_order(symbol, 'buy', limit_price, order_size)
+        assert order.get_symbol() == symbol
+        # TODO these are not properly homogenized for spot markets
+        assert order.get_status()['status'].lower() in ('new', 'open', 'filled')
+        assert order.get_type().lower() == OrderType.MARKET
+        assert order.get_size() == approx(order_size)
+        assert order.get_side().lower() == Side.BUY
+        assert order.get_price() == approx(limit_price)
 
     yield order
 
     res = interface.cancel_order(symbol, order.id)
-    assert res.status == OrderStatus.CANCELED
 
-    assert order != res
-    res.status = OrderStatus.OPEN
-    assert order == res
+    if isinstance(interface, FuturesExchangeInterface):
+        assert res.status == OrderStatus.CANCELED
+
+        assert order != res
+        res.status = OrderStatus.OPEN
+        assert order == res