BackExchange

Overview

BackExchange is a simulative exchange for backtesting your trading algorithms. Its API mimics that in ccxt library, which supports live trading on more than 90 mainstrem cryptocurrency exchanges. The intention is to make minimal difference between algorithms in the backtest and in the live trading.

The essential data taken by BackExchange are timestamped OHLCV tickers, which are fed through Quotes when it is first intialized. Its clock is controlled by the Timer. At each time bar, BackExchange takes and processes orders just like a real exchange.

Note

  • All tickers and balance are processed and returned with 8 decimal places.
  • Order matching is in favor of buyers. For example, if there is a sell order placed at price 10.0 and a buy order placed at 10.5, the order will be executed at 10.0.

Features

Order Queue

In event based backtesting, orders placed at this time bar are processed at next, while in live trading orders are processed instantly. Therefore, all orders submitted to BackExchange are first cached in an order queue, and wait to be processed at the beginning of the next time bar.

Order queue raises several complication:

  • As its name suggests, queued orders are processed in a first-in-first-out manner. This may cause later submitted orders rejected due to insufficient funds.
  • Submitted orders can be cancelled through BackExchange.cancel_submitted_order(). Cancelled submitted orders will not appear in closed order book, as if order requests are never sent to the exchange.
  • When an invalid order is submitted to the order queue, an exception may be raised at the moment when the order is submitted or the moment when the order is processed at the beginning of next time bar, depending on the time that the error can be detected. See Exceptions for more details.

See also

More about Order.

List and Delist

A common pitfall in backtesting strategies is survivor bias, which can happen when assets are delisted from an exchange but are not included in the testing data. BackExchange supports listing and delisting assets or trading pairs by simply checking existing trading pairs at the current time bar. All currently supported trading pairs and assets can be queried through BackExchange.fetch_markets().

If a previously existing trading pair doesn’t exist any more, all open orders under it will be forcely closed. If a previously existing asset doesn’t appear in any trading pair, it is considered as delisted. The remaining balance will be forcely withdrawn. Withdrawal history can be queried through BackExchange.fetch_deposit_history().

Slippage

In real life trading, orders are usually not filled at the ticker price for various reasons. This process, called slippage, is taken care in BackExchange in the following aspects:

  • Orders placed at this time bar is always processed at next to simulate time delay.
  • Buy and sell orders can be filled at different type of prices (for example, buy orders are filled at high price and sell orders are filled at low price in the ticker). These can be set when BackExchange is first initialized, or changed any time through BackExchange.buy_price and BackExchange.sell_price.
  • Transaction fee as fixed rate slippage. Buy orders are always filled 0.01x% higher than the ticker price and sell orders are always filled 0.01x% lower than the ticker price. x is the transaction fee rate in the unit of basis point. It can be set when BackExchange is first initialized, or changed any time through BackExchange.fee_rate.
  • Slippage model. Given ticker price and any custom data as input, the slippage model determines the amount and the price to be filled for a given order. It can be set when BackExchange is first initialized, or changed any time through BackExchange.slippage_model. Nyxar provides several predefined slippage models, such as spread slippage and volume slippage. Nyxar also supports user defined slippage model. See Slippage Model for more details.

API Reference

class BackExchange(timer, quotes[, buy_price=PriceType.Open, sell_price=PriceType.Open, fee_rate=0.05, slippage_model=SlippageBase())

BackExchange used for backtesting.

  • timer: Timer class used to control the clock of BackExchange.
  • quotes: Quotes class contains timestamped OHLCV tickers.
  • buy_price: Set buy_price. Defaults to ‘open’.
  • sell_price: Set sell_price. Defaults to ‘open’.
  • fee_rate: Set fee_rate. Defaults to 0.05.
  • slippage_model: Set slippage_model. Defaults to SlippageBase.

Attributes:

buy_price

The price types that all buy orders are filled at. Its value can be of one the following four strings: ‘open’, ‘high’, ‘low’, ‘close’.

sell_price

The price types that all sell orders are filled at. Its value can be of one the following four strings: ‘open’, ‘high’, ‘low’, ‘close’.

fee_rate

The fee rate imposed by the exchange on all orders in the unit of basis point. Buy orders are always filled 0.01 * fee_rate% higher than the ticker price and sell orders are always filled 0.01 * fee_rate% lower than the ticker price.

In practice, the fee is taken by deducting quote asset for buy orders, and base asset for sell orders. In other words, you will always receive less asset than the amount appears in the order.

slippage_model

The slippage model to determine how an order should be filled. See Slippage Model for more details.

User methods:

The following are user methods that resemble public APIs provided by an exchange.

fetch_timestamp()

Return the current timestamp in millisecond.

fetch_markets()

Return a tuple of dictionaries contain currently supported asset names and trading pair symbols.

fetch_ticker([symbol=''])

Return the OHLCV tickers of the current time bar for the given symbol. If symbol not specified, return tickers for all supported symbols.

>>> ex.fetch_ticker(symbol='FOO/BAR')
{'open': 1.2, 'high': 3.4, 'low': 5.6, 'close': 7.8, 'volume': 9.0}
>>> ex.fetch_ticker()
{'FOO': {'open': 1.2, 'high': 3.4, 'low': 5.6, 'close': 7.8, 'volume': 900.0},
 'BAR': {'open': 9.0, 'high': 7.8, 'low': 3.5, 'close': 4.6, 'volume': 120.2}, ...}.

The following are user methods that resemble private APIs provided by an exchange.

deposit(asset, amount)
withdraw(asset, amount)

Deposit / Withdraw amount of asset into the balance. Any negative amount will be cast to zero. Return successfully deposited / withdrawn amount.

fetch_balance()

Return all current balances in a dictionary.

>>> ex.fetch_balance()
{'FOO': {'total': 100.0, 'free': 99.5, 'used': 0.5},
 'BAR': {'total': 78.0, 'free': 78.0, 'used': 0}, ...}.
fetch_balance_in(target[, fee=False])

Return the total balance in the target asset, based on tickers at the current time bar. The method will automatically finds the most profitable way to convert an asset to target if there are more than one ways. A NotSupported exception will be raised if there exists an asset that is unable to convert to target.

If fee=True, the converted balance is computed by taking transaction fee into account. Defaults to False.

fetch_deposit_history()

Return a list of deposit and withdrawl history.

>>> ex.fetch_deposit_history()
[{'timestamp': 1517599560000, 'asset': 'FOO', 'amount': 100}, {'timestamp': 1517599620000, 'asset': 'FOO', 'amount': -5}]
create_market_buy_order(symbol, amount)
create_market_sell_order(symbol, amount)

Create and submit a market buy/sell order under symbol of amount to the order queue. Return the info of placed order.

>>> ex.create_market_buy_order('FOO/BAR', 100)
{'id': 693461813487499546,
'datetime': '2018-02-02 14:26:00',
'timestamp': 1517599560000,
'status': 'submitted',
'symbol': 'FOO/BAR',
'type': 'market',
'side': 'buy',
'price': 0,
'stop_price': 0,
'amount': 100,
'filled': 0,
'remaining': 100,
'transaction': [],
'fee': {}}
create_limit_buy_order(symbol, amount, price)
create_limit_sell_order(symbol, amount, price)

Create and submit a limit buy/sell order under symbol of amount to the order queue. The limit price of the order is price. Return the info of placed order.

create_stop_limit_buy_order(symbol, amount, price, stop_price)
create_stop_limit_sell_order(symbol, amount, price, stop_price)

Create and submit a stop limit buy/sell order under symbol of amount to the order queue. The limit price of the order is price, and the stop limit price is stop_price. Return the info of placed order.

cancel_submitted_order(order_id)

Cacnel the submitted order in the order queue whose id is order_id.

cancel_open_order(order_id)

Cancel the open order in the open order book whose id is order_id.

fetch_submitted_order(order_id)

Return Order.info of the submitted order in the order queue whose id is order_id.

fetch_submitted_orders([limit=500])

Return Order.info of last limit submitted orders in the order queue. If limit=0, return info of all submitted orders. limit defaults to 500.

fetch_order(order_id)

Return Order.info of the order whose id is order_id in the open order book or closed order book.

fetch_open_orders([symbol='', limit=500])

Return Order.info of last limit open orders in the open order book. If symbol is specified, only orders under that trading symbols are returned. Otherwise all open orders will be returned. If limit=0, return info of all open orders. limit defaults to 500.

fetch_closed_orders(symbol[, limit=500])

Return Order.info of last limit closed orders in the closed order book. Different from fetch_open_orders(), symbol must be specified.

Exceptions

exception NotSupported

Raised when an unsupported asset or trading pair symbol is queried.

exception InsufficientFunds

Raised when there are no enough funds to place an order. This exception will only be raised at the beginning of a time bar when the order is being processed by the exchange.

exception InvalidOrder

Raised when an invalid order is submitted. For invalid orders with negaive amount or price, this exception will be raised immediately when orders are created. For invalid orders with non-existing trading pair symbol, this exception will be raised at the beginning of the next time bar.

exception OrderNotFound

Raised when a particular order is not found (usually queried through order id) in the order book.

exception SlippageModelError

Raised when the transaction generated by the slippage model is invalid. For example, for an market order, the transaction.amount generated by the slippage model is not equal to order.amount.