diff --git a/catalyst/examples/arbitrage_with_interface.py b/catalyst/examples/arbitrage_with_interface.py new file mode 100644 index 00000000..66afd8ce --- /dev/null +++ b/catalyst/examples/arbitrage_with_interface.py @@ -0,0 +1,190 @@ +import talib +from logbook import Logger + +from catalyst.api import ( + order, + order_target_percent, + symbol, + record, + get_open_orders, +) +from catalyst.exchange.stats_utils import get_pretty_stats +from catalyst.utils.run_algo import run_algorithm + +algo_namespace = 'arbitrage_neo_eth' +log = Logger(algo_namespace) + + +def initialize(context): + log.info('initializing arbitrage algorithm') + + context.buying_exchange = 'bittrex' + context.selling_exchange = 'bitfinex' + + context.trading_pair_symbol = 'neo_eth' + context.trading_pairs = dict() + context.trading_pairs[context.buying_exchange] = \ + symbol(context.trading_pair_symbol, context.buying_exchange) + context.trading_pairs[context.selling_exchange] = \ + symbol(context.trading_pair_symbol, context.selling_exchange) + + context.entry_points = [ + dict(gap=0.001, amount=0.05), + dict(gap=0.002, amount=0.1), + ] + context.exit_points = [ + dict(gap=0, amount=0.05), + dict(gap=-0.001, amount=0.01), + ] + + context.MAX_POSITIONS = 50 + context.SLIPPAGE_ALLOWED = 0.02 + + pass + + +def place_order(context, amount, buying_price, selling_price, + action): + if action == 'enter': + buying_exchange = context.exchanges[context.buying_exchange] + buy_price = buying_price + + selling_exchange = context.exchanges[context.selling_exchange] + sell_price = selling_price + + elif action == 'exit': + buying_exchange = context.exchanges[context.selling_exchange] + buy_price = selling_price + + selling_exchange = context.exchanges[context.buying_exchange] + sell_price = buying_price + + else: + raise ValueError('invalid order action') + + base_currency = buying_exchange.base_currency + base_currency_amount = buying_exchange.portfolio.cash + + sell_balances = selling_exchange.get_balances() + sell_currency = context.trading_pairs[ + context.selling_exchange].market_currency + + if sell_currency in sell_balances: + market_currency_amount = sell_balances[sell_currency] + else: + log.warn('the selling exchange {} does not hold currency {}'.format( + selling_exchange.name, sell_currency + )) + return + + if base_currency_amount < amount: + log.warn('not enough {} ({}) to buy {}, adjusting the amount'.format( + base_currency, base_currency_amount, amount)) + amount = base_currency_amount + elif market_currency_amount < amount: + log.warn('not enough {} ({}) to sell {}, aborting'.format( + sell_currency, market_currency_amount, amount)) + return + + adj_buy_price = buy_price * (1 + context.SLIPPAGE_ALLOWED) + log.info('buying {} limit at {}{} on {}'.format( + amount, buying_price, context.trading_pair_symbol, + buying_exchange.name)) + order( + asset=context.trading_pairs[buying_exchange], + amount=amount, + limit_price=adj_buy_price + ) + + adj_sell_price = sell_price * (1 - context.SLIPPAGE_ALLOWED) + log.info('selling {} limit at {}{} on {}'.format( + amount, adj_sell_price, context.trading_pair_symbol, + selling_exchange.name)) + order( + asset=context.trading_pairs[selling_exchange], + amount=amount, + limit_price=adj_sell_price + ) + pass + + +def handle_data(context, data): + log.info('handling bar {}'.format(data.current_dt)) + + buying_price = data.current( + context.trading_pairs[context.buying_exchange], 'price') + log.info('price on buying exchange {exchange}: {price}'.format( + exchange=context.buying_exchange.upper(), + price=buying_price, + )) + + selling_price = data.current( + context.trading_pairs[context.selling_exchange], 'price') + + log.info('price on selling exchange {exchange}: {price}'.format( + exchange=context.selling_exchange.upper(), + price=selling_price, + )) + + # If for example, + # selling price = 50 + # buying price = 25 + # expected gap = 1 + + # If follows that, + # selling price - buying price / buying price + # 50 - 25 / 25 = 1 + gap = (selling_price - buying_price) / buying_price + log.info('the price gap: {} ({}%)'.format(gap, gap * 100)) + + # Consider the least ambitious entry point first + # Override of wider gap is found + entry_points = sorted( + context.entry_points, + key=lambda point: point['gap'], + ) + + buy_amount = None + for entry_point in entry_points: + if gap > entry_point['gap']: + buy_amount = entry_point['amount'] + + if buy_amount: + log.info('found buy trigger for amount: {}'.format(buy_amount)) + place_order(context, buy_amount, buying_price, selling_price, 'enter') + + else: + # Consider the narrowest exit gap first + # Override of wider gap is found + exit_points = sorted( + context.exit_points, + key=lambda point: point['gap'], + reverse=True + ) + + sell_amount = None + for exit_point in exit_points: + if gap < exit_point['gap']: + sell_amount = exit_point['amount'] + + if sell_amount: + log.info('found sell trigger for amount: {}'.format(sell_amount)) + place_order(context, sell_amount, buying_price, selling_price, + 'exit') + + +def analyze(context, stats): + log.info('the daily stats:\n{}'.format(get_pretty_stats(stats))) + pass + + +run_algorithm( + initialize=initialize, + handle_data=handle_data, + analyze=analyze, + exchange_name='bittrex,bitfinex', + live=True, + algo_namespace=algo_namespace, + base_currency='eth', + live_graph=False +) diff --git a/catalyst/exchange/algorithm_exchange.py b/catalyst/exchange/algorithm_exchange.py index 5321f971..86b836f2 100644 --- a/catalyst/exchange/algorithm_exchange.py +++ b/catalyst/exchange/algorithm_exchange.py @@ -11,42 +11,43 @@ # See the License for the specific language governing permissions and # limitations under the License. import os +import pickle import signal import sys -import pickle +from collections import deque from datetime import timedelta -from time import sleep from os import listdir from os.path import isfile, join -from collections import deque -import numpy as np +from time import sleep import logbook import pandas as pd -from catalyst.utils.preprocess import preprocess +from catalyst.assets._assets import TradingPair import catalyst.protocol as zp from catalyst.algorithm import TradingAlgorithm from catalyst.data.minute_bars import BcolzMinuteBarWriter, \ BcolzMinuteBarReader from catalyst.errors import OrderInBeforeTradingStart -from catalyst.exchange.simple_clock import SimpleClock -from catalyst.exchange.live_graph_clock import LiveGraphClock from catalyst.exchange.exchange_errors import ( ExchangeRequestError, ExchangePortfolioDataError, - ExchangeTransactionError -) + ExchangeTransactionError, + OrphanOrderError) from catalyst.exchange.exchange_utils import get_exchange_minute_writer_root, \ save_algo_object, get_algo_object, get_algo_folder, get_algo_df, \ save_algo_df +from catalyst.exchange.live_graph_clock import LiveGraphClock +from catalyst.exchange.simple_clock import SimpleClock from catalyst.exchange.stats_utils import get_pretty_stats from catalyst.finance.performance.period import calc_period_stats from catalyst.gens.tradesimulation import AlgorithmSimulator from catalyst.utils.api_support import ( api_method, disallowed_in_before_trading_start) -from catalyst.utils.input_validation import error_keywords, ensure_upper_case +from catalyst.utils.input_validation import error_keywords, ensure_upper_case, \ + expect_types +from catalyst.utils.preprocess import preprocess log = logbook.Logger("ExchangeTradingAlgorithm") @@ -406,7 +407,9 @@ class ExchangeTradingAlgorithm(TradingAlgorithm): self.minute_stats.append(minute_stats) self.add_pnl_stats(minute_stats) - self.add_custom_signals_stats(minute_stats) + if self.recorded_vars: + self.add_custom_signals_stats(minute_stats) + self.add_exposure_stats(minute_stats) print_df = pd.DataFrame(list(self.minute_stats)) @@ -481,23 +484,50 @@ class ExchangeTradingAlgorithm(TradingAlgorithm): @api_method @disallowed_in_before_trading_start(OrderInBeforeTradingStart()) + @expect_types(asset=TradingPair) def order(self, asset, amount, limit_price=None, stop_price=None, style=None): + """ + We use the exchange specific portfolio to place orders. + The cumulative portfolio does not contain open orders but exchange + portfolios do. + + :param asset: TradingPair + :param amount: float + :param limit_price: float + :param stop_price: float + :param style: Style + :return order: Order + The catalyst order object or None + """ + amount, style = self._calculate_order(asset, amount, limit_price, stop_price, style) order_id = self._order(asset, amount, limit_price, stop_price, style) + exchange = self.exchanges[asset.exchange] + exchange_portfolio = exchange.portfolio if order_id is not None: - order = self.portfolio.open_orders[order_id] - self.perf_tracker.process_order(order) - return order + + if order_id in exchange_portfolio.open_orders: + order = exchange_portfolio.open_orders[order_id] + self.perf_tracker.process_order(order) + return order + + else: + raise OrphanOrderError( + order_id=order_id, + exchange=exchange.name + ) else: + log.warn('unable to order {} {} on exchange {}'.format( + amount, asset.symbol, asset.exchange)) return None def round_order(self, amount): diff --git a/catalyst/exchange/bitfinex/bitfinex.py b/catalyst/exchange/bitfinex/bitfinex.py index fe32991a..6d0fb09f 100644 --- a/catalyst/exchange/bitfinex/bitfinex.py +++ b/catalyst/exchange/bitfinex/bitfinex.py @@ -40,6 +40,7 @@ class Bitfinex(Exchange): self.key = key self.secret = secret.encode('UTF-8') self.name = 'bitfinex' + self.color = 'green' self.assets = {} self.load_assets() self.base_currency = base_currency diff --git a/catalyst/exchange/bitfinex/symbols.json b/catalyst/exchange/bitfinex/symbols.json index 8ab44191..2134bd9f 100644 --- a/catalyst/exchange/bitfinex/symbols.json +++ b/catalyst/exchange/bitfinex/symbols.json @@ -1,4 +1,16 @@ { + "neobtc": { + "symbol": "neo_btc", + "start_date": "2017-09-07" + }, + "neousd": { + "symbol": "neo_usd", + "start_date": "2017-09-07" + }, + "neoeth": { + "symbol": "neo_eth", + "start_date": "2017-09-07" + }, "btcusd": { "symbol": "btc_usd", "start_date": "2010-01-01" diff --git a/catalyst/exchange/bittrex/bittrex.py b/catalyst/exchange/bittrex/bittrex.py index f56073c5..87b1c437 100644 --- a/catalyst/exchange/bittrex/bittrex.py +++ b/catalyst/exchange/bittrex/bittrex.py @@ -22,6 +22,7 @@ class Bittrex(Exchange): def __init__(self, key, secret, base_currency, portfolio=None): self.api = Bittrex_api(key=key, secret=secret.encode('UTF-8')) self.name = 'bittrex' + self.color = 'blue' self.base_currency = base_currency self._portfolio = portfolio diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index b0d0f5e2..7aaff3fc 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -14,7 +14,7 @@ from catalyst.errors import ( SymbolNotFound, ) from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \ - InvalidOrderStyle, BaseCurrencyNotFoundError + InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \ ExchangeLimitOrder, ExchangeStopOrder from catalyst.exchange.exchange_portfolio import ExchangePortfolio @@ -30,7 +30,6 @@ class Exchange: def __init__(self): self.name = None - self.trading_pairs = None self.assets = {} self._portfolio = None self.minute_writer = None @@ -110,7 +109,12 @@ class Exchange: asset = self.assets[key] if not asset: - raise SymbolNotFound(symbol=symbol) + supported_symbols = [pair.symbol for pair in self.assets.values()] + raise SymbolNotFoundOnExchange( + symbol=symbol, + exchange=self.name, + supported_symbols=supported_symbols + ) return asset diff --git a/catalyst/exchange/exchange_errors.py b/catalyst/exchange/exchange_errors.py index 276be89c..a0a137f9 100644 --- a/catalyst/exchange/exchange_errors.py +++ b/catalyst/exchange/exchange_errors.py @@ -94,6 +94,13 @@ class OrderNotFound(ZiplineError): ).strip() +class OrphanOrderError(ZiplineError): + msg = ( + 'Order {order_id} found in exchange {exchange} but not tracked by ' + 'the algorithm.' + ).strip() + + class OrderCancelError(ZiplineError): msg = ( 'Unable to cancel order {order_id} on exchange {exchange} {error}.' @@ -118,3 +125,18 @@ class MismatchingBaseCurrencies(ZiplineError): 'Unable to trade with base currency {base_currency} when the ' 'algorithm uses {algo_currency}.' ).strip() + + +class MismatchingBaseCurrenciesExchanges(ZiplineError): + msg = ( + 'Unable to trade with base currency {base_currency} when the ' + 'exchange {exchange_name} users {exchange_currency}.' + ).strip() + + +class SymbolNotFoundOnExchange(ZiplineError): + """ + Raised when a symbol() call contains a non-existant symbol. + """ + msg = ('Symbol {symbol} not found on exchange {exchange}. ' + 'Choose from: {supported_symbols}').strip() diff --git a/catalyst/exchange/live_graph_clock.py b/catalyst/exchange/live_graph_clock.py index 877f8115..be3b80a8 100644 --- a/catalyst/exchange/live_graph_clock.py +++ b/catalyst/exchange/live_graph_clock.py @@ -22,6 +22,9 @@ from logbook import Logger from matplotlib import pyplot as plt from matplotlib import style +from catalyst.exchange.exchange_errors import \ + MismatchingBaseCurrenciesExchanges + log = Logger('LiveGraphClock') style.use('dark_background') @@ -154,17 +157,31 @@ class LiveGraphClock(object): context = self.context df = context.exposure_stats + # TODO: list exchanges in graph + base_currency = None + positions = [] + for exchange_name in context.exchanges: + exchange = context.exchanges[exchange_name] + + if not base_currency: + base_currency = exchange.base_currency + elif base_currency != exchange.base_currency: + raise MismatchingBaseCurrenciesExchanges( + base_currency=base_currency, + exchange_name=exchange.name, + exchange_currency=exchange.base_currency + ) + + positions += exchange.portfolio.positions + ax.clear() ax.set_title('Exposure') ax.plot(df.index, df['base_currency'], '-', color='green', linewidth=1.0, - label='Base Currency: {}'.format( - context.exchange.base_currency.upper() - ) + label='Base Currency: {}'.format(base_currency.upper()) ) - positions = context.exchange.portfolio.positions symbols = [] for position in positions: symbols.append(position.symbol) @@ -172,10 +189,7 @@ class LiveGraphClock(object): ax.plot(df.index, df['long_exposure'], '-', color='blue', linewidth=1.0, - label='Long Exposure: {}'.format( - ', '.join(symbols).upper() - ) - ) + label='Long Exposure: {}'.format(', '.join(symbols).upper())) self.set_legend(ax) self.format_ax(ax)