From f4db9f7b1ead472feacf14c4c5470c5ff9d2b7ef Mon Sep 17 00:00:00 2001 From: fredfortier Date: Mon, 28 Aug 2017 22:50:46 -0400 Subject: [PATCH] Working on Bittrex implementation and unit tests --- catalyst/exchange/bitfinex/bitfinex.py | 20 ++++++------ catalyst/exchange/bittrex/bittrex.py | 40 ++++++++++++++++++----- catalyst/exchange/exchange.py | 43 ++++++++++++++----------- catalyst/exchange/exchange_errors.py | 11 +++++-- catalyst/exchange/exchange_execution.py | 39 ++++++++++++++++++++++ tests/exchange/test_bittrex.py | 10 +++--- 6 files changed, 119 insertions(+), 44 deletions(-) create mode 100644 catalyst/exchange/exchange_execution.py diff --git a/catalyst/exchange/bitfinex/bitfinex.py b/catalyst/exchange/bitfinex/bitfinex.py index 0afedd41..ec1908bf 100644 --- a/catalyst/exchange/bitfinex/bitfinex.py +++ b/catalyst/exchange/bitfinex/bitfinex.py @@ -1,11 +1,11 @@ import base64 -import numpy as np import hashlib import hmac import json import re import time +import numpy as np import pandas as pd import pytz import requests @@ -18,11 +18,9 @@ from catalyst.exchange.exchange import Exchange from catalyst.exchange.exchange_errors import ( ExchangeRequestError, InvalidHistoryFrequencyError, - InvalidOrderType) -from catalyst.finance.execution import (MarketOrder, - LimitOrder, - StopOrder, - StopLimitOrder) + InvalidOrderStyle) +from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \ + ExchangeStopLimitOrder, ExchangeStopOrder from catalyst.finance.order import Order, ORDER_STATUS from catalyst.protocol import Account @@ -368,14 +366,18 @@ class Bitfinex(Exchange): :return: """ exchange_symbol = self.get_symbol(asset) - if isinstance(style, LimitOrder) or isinstance(style, StopLimitOrder): + if isinstance(style, ExchangeLimitOrder) \ + or isinstance(style, ExchangeStopLimitOrder): price = style.get_limit_price(is_buy) order_type = 'limit' - elif isinstance(style, StopOrder): + + elif isinstance(style, ExchangeStopOrder): price = style.get_stop_price(is_buy) order_type = 'stop' + else: - raise InvalidOrderType() + raise InvalidOrderStyle(exchange=self.name, + style=style.__class__.__name__) req = dict( symbol=exchange_symbol, diff --git a/catalyst/exchange/bittrex/bittrex.py b/catalyst/exchange/bittrex/bittrex.py index 0b79fa07..59ecc335 100644 --- a/catalyst/exchange/bittrex/bittrex.py +++ b/catalyst/exchange/bittrex/bittrex.py @@ -1,13 +1,15 @@ -from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \ - ExchangeRequestError +import json + +import pandas as pd +from catalyst.assets._assets import TradingPair from logbook import Logger from six.moves import urllib -import json -import pandas as pd -from catalyst.exchange.exchange import Exchange from catalyst.exchange.bittrex.bittrex_api import Bittrex_api -from catalyst.assets._assets import TradingPair +from catalyst.exchange.exchange import Exchange +from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \ + ExchangeRequestError, InvalidOrderStyle +from catalyst.finance.execution import LimitOrder, StopLimitOrder log = Logger('Bittrex') @@ -16,8 +18,10 @@ URL2 = 'https://bittrex.com/Api/v2.0' class Bittrex(Exchange): def __init__(self, key, secret, base_currency, portfolio=None): - self.api = Bittrex_api(key=key, secret=secret) + self.api = Bittrex_api(key=key, secret=secret.encode('UTF-8')) self.name = 'bittrex' + self.base_currency = base_currency + self._portfolio = portfolio self.assets = dict() self.load_assets() @@ -78,7 +82,27 @@ class Bittrex(Exchange): def create_order(self, asset, amount, is_buy, style): log.info('creating order') - pass + exchange_symbol = self.get_symbol(asset) + if isinstance(style, LimitOrder) or isinstance(style, StopLimitOrder): + if isinstance(style, StopLimitOrder): + log.warn('{} will ignore the stop price'.format(self.name)) + + price = style.get_limit_price(is_buy) + try: + if is_buy: + order = self.api.buylimit(exchange_symbol, amount, price) + else: + order = self.api.selllimit(exchange_symbol, amount, price) + except Exception as e: + raise ExchangeRequestError(error=e) + + if 'uuid' in order: + return order['uuid'] + else: + raise ExchangeRequestError(error='Order uuid not found.') + else: + raise InvalidOrderStyle(exchange=self.name, + style=style.__class__.__name__) def get_open_orders(self, asset): pass diff --git a/catalyst/exchange/exchange.py b/catalyst/exchange/exchange.py index b002296c..790f6458 100644 --- a/catalyst/exchange/exchange.py +++ b/catalyst/exchange/exchange.py @@ -1,9 +1,8 @@ import abc -import random -from time import sleep import collections +import random from abc import ABCMeta, abstractmethod, abstractproperty -from datetime import timedelta +from time import sleep import numpy as np import pandas as pd @@ -14,15 +13,14 @@ from catalyst.data.data_portal import BASE_FIELDS from catalyst.errors import ( SymbolNotFound, ) -from catalyst.exchange.exchange_errors import InvalidOrderType +from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \ + InvalidOrderStyle +from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \ + ExchangeLimitOrder, ExchangeStopOrder +from catalyst.exchange.exchange_portfolio import ExchangePortfolio +from catalyst.exchange.exchange_utils import get_exchange_symbols from catalyst.finance.order import ORDER_STATUS from catalyst.finance.transaction import Transaction -from catalyst.exchange.exchange_utils import get_exchange_symbols -from catalyst.exchange.exchange_portfolio import ExchangePortfolio -from catalyst.finance.execution import (MarketOrder, - LimitOrder, - StopOrder, - StopLimitOrder) log = Logger('Exchange') @@ -385,7 +383,8 @@ class Exchange: def create_order(self, asset, amount, is_buy, style): pass - def order(self, asset, amount, limit_price, stop_price, style=None): + def order(self, asset, amount, limit_price=None, stop_price=None, + style=None): """Place an order. Parameters @@ -430,21 +429,27 @@ class Exchange: return None if asset.base_currency != self.base_currency.lower(): - raise NotImplementedError( - 'Currency pairs must share their base with the exchange.' + raise MismatchingBaseCurrencies( + base_currency=asset.base_currency, + algo_currency=self.base_currency ) is_buy = (amount > 0) if limit_price is not None and stop_price is not None: - style = StopLimitOrder(limit_price, stop_price, - exchange=self.name) + style = ExchangeStopLimitOrder(limit_price, stop_price, + exchange=self.name) elif limit_price is not None: - style = LimitOrder(limit_price, exchange=self.name) + style = ExchangeLimitOrder(limit_price, exchange=self.name) + elif stop_price is not None: - style = StopOrder(stop_price, exchange=self.name) - elif style is None: - raise InvalidOrderType() + style = ExchangeStopOrder(stop_price, exchange=self.name) + + elif style is not None: + raise InvalidOrderStyle(exchange=self.name, + style=style.__class__.__name__) + else: + raise ValueError('Incomplete order data.') display_price = limit_price if limit_price is not None else stop_price log.debug( diff --git a/catalyst/exchange/exchange_errors.py b/catalyst/exchange/exchange_errors.py index 955c33b8..dbbb254e 100644 --- a/catalyst/exchange/exchange_errors.py +++ b/catalyst/exchange/exchange_errors.py @@ -69,9 +69,9 @@ class InvalidSymbolError(ZiplineError): ).strip() -class InvalidOrderType(ZiplineError): +class InvalidOrderStyle(ZiplineError): msg = ( - 'Order type not found.' + 'Order style {style} not supported by exchange {exchange}.' ).strip() @@ -79,3 +79,10 @@ class SidHashError(ZiplineError): msg = ( 'Unable to hash sid from symbol {symbol}.' ).strip() + + +class MismatchingBaseCurrencies(ZiplineError): + msg = ( + 'Unable to trade with base currency {base_currency} when the ' + 'algorithm uses {algo_currency}.' + ).strip() diff --git a/catalyst/exchange/exchange_execution.py b/catalyst/exchange/exchange_execution.py new file mode 100644 index 00000000..6527678e --- /dev/null +++ b/catalyst/exchange/exchange_execution.py @@ -0,0 +1,39 @@ +from catalyst.finance.execution import LimitOrder, StopOrder, StopLimitOrder + + +class ExchangeLimitOrder(LimitOrder): + def get_limit_price(self, is_buy): + """ + We may be trading Satoshis with 8 decimals, we cannot round numbers + :param is_buy: + :return: + """ + return self.limit_price + + +class ExchangeStopOrder(StopOrder): + def get_stop_price(self, is_buy): + """ + We may be trading Satoshis with 8 decimals, we cannot round numbers + :param is_buy: + :return: + """ + return self.stop_price + + +class ExchangeStopLimitOrder(StopLimitOrder): + def get_limit_price(self, is_buy): + """ + We may be trading Satoshis with 8 decimals, we cannot round numbers + :param is_buy: + :return: + """ + return self.limit_price + + def get_stop_price(self, is_buy): + """ + We may be trading Satoshis with 8 decimals, we cannot round numbers + :param is_buy: + :return: + """ + return self.stop_price diff --git a/tests/exchange/test_bittrex.py b/tests/exchange/test_bittrex.py index 31d454e8..9ca2d398 100644 --- a/tests/exchange/test_bittrex.py +++ b/tests/exchange/test_bittrex.py @@ -19,18 +19,16 @@ class BittrexTestCase(BaseExchangeTestCase): self.exchange = Bittrex( key=auth['key'], secret=auth['secret'], - base_currency='usd' + base_currency='btc' ) def test_order(self): log.info('creating order') - asset = self.exchange.get_asset('neo_eth') + asset = self.exchange.get_asset('neo_btc') order_id = self.exchange.order( asset=asset, - style=LimitOrder(limit_price=200), - limit_price=200, - amount=0.5, - stop_price=None + limit_price=0.00055, + amount=1, ) log.info('order created {}'.format(order_id)) pass