From f60abcd6363f6425173abb99446b778ce7ab7429 Mon Sep 17 00:00:00 2001 From: Victor Grau Serrat Date: Mon, 25 Sep 2017 11:28:06 -0600 Subject: [PATCH] WIP: Poloniex exchange class --- catalyst/exchange/poloniex/__init__.py | 0 catalyst/exchange/poloniex/poloniex.py | 508 +++++++++++++++++++++ catalyst/exchange/poloniex/poloniex_api.py | 126 +++++ catalyst/utils/run_algo.py | 10 +- 4 files changed, 643 insertions(+), 1 deletion(-) create mode 100644 catalyst/exchange/poloniex/__init__.py create mode 100644 catalyst/exchange/poloniex/poloniex.py create mode 100644 catalyst/exchange/poloniex/poloniex_api.py diff --git a/catalyst/exchange/poloniex/__init__.py b/catalyst/exchange/poloniex/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/catalyst/exchange/poloniex/poloniex.py b/catalyst/exchange/poloniex/poloniex.py new file mode 100644 index 00000000..bf8069b8 --- /dev/null +++ b/catalyst/exchange/poloniex/poloniex.py @@ -0,0 +1,508 @@ +import base64 +import hashlib +import hmac +import json +import re +import time + +import numpy as np +import pandas as pd +import pytz +import requests +import six +from catalyst.assets._assets import TradingPair +from logbook import Logger + +from catalyst.exchange.poloniex.poloniex_api import Poloniex_api + + +# from websocket import create_connection +from catalyst.exchange.exchange import Exchange +from catalyst.exchange.exchange_errors import ( + ExchangeRequestError, + InvalidHistoryFrequencyError, + InvalidOrderStyle, OrderCancelError) +from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \ + ExchangeStopLimitOrder, ExchangeStopOrder +from catalyst.finance.order import Order, ORDER_STATUS +from catalyst.protocol import Account +from catalyst.exchange.exchange_utils import get_exchange_symbols_filename + + +log = Logger('Poloniex') + + +class Poloniex(Exchange): + def __init__(self, key, secret, base_currency, portfolio=None): + self.api = Poloniex_api(key=key, secret=secret.encode('UTF-8')) + self.name = 'poloniex' + self.assets = {} + #self.load_assets() + self.base_currency = base_currency + self._portfolio = portfolio + self.minute_writer = None + self.minute_reader = None + + def sanitize_curency_symbol(self, exchange_symbol): + """ + Helper method used to build the universal pair. + Include any symbol mapping here if appropriate. + + :param exchange_symbol: + :return universal_symbol: + """ + return exchange_symbol.lower() + + ''' + def _create_order(self, order_status): + """ + Create a Catalyst order object from a Bitfinex order dictionary + :param order_status: + :return: Order + """ + if order_status['is_cancelled']: + status = ORDER_STATUS.CANCELLED + elif not order_status['is_live']: + log.info('found executed order {}'.format(order_status)) + status = ORDER_STATUS.FILLED + else: + status = ORDER_STATUS.OPEN + + amount = float(order_status['original_amount']) + filled = float(order_status['executed_amount']) + + if order_status['side'] == 'sell': + amount = -amount + filled = -filled + + price = float(order_status['price']) + order_type = order_status['type'] + + stop_price = None + limit_price = None + + # TODO: is this comprehensive enough? + if order_type.endswith('limit'): + limit_price = price + elif order_type.endswith('stop'): + stop_price = price + + executed_price = float(order_status['avg_execution_price']) + + # TODO: bitfinex does not specify comission. I could calculate it but not sure if it's worth it. + commission = None + + date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp'])) + date = pytz.utc.localize(date) + order = Order( + dt=date, + asset=self.assets[order_status['symbol']], + amount=amount, + stop=stop_price, + limit=limit_price, + filled=filled, + id=str(order_status['id']), + commission=commission + ) + order.status = status + + return order, executed_price + ''' + + def get_balances(self): + pass + ''' + log.debug('retrieving wallets balances') + try: + response = self._request('balances', None) + balances = response.json() + except Exception as e: + raise ExchangeRequestError(error=e) + + if 'message' in balances: + raise ExchangeRequestError( + error='unable to fetch balance {}'.format(balances['message']) + ) + + std_balances = dict() + for balance in balances: + currency = balance['currency'].lower() + std_balances[currency] = float(balance['available']) + + return std_balances + ''' + + @property + def account(self): + account = Account() + + account.settled_cash = None + account.accrued_interest = None + account.buying_power = None + account.equity_with_loan = None + account.total_positions_value = None + account.total_positions_exposure = None + account.regt_equity = None + account.regt_margin = None + account.initial_margin_requirement = None + account.maintenance_margin_requirement = None + account.available_funds = None + account.excess_liquidity = None + account.cushion = None + account.day_trades_remaining = None + account.leverage = None + account.net_leverage = None + account.net_liquidation = None + + return account + + @property + def time_skew(self): + # TODO: research the time skew conditions + return pd.Timedelta('0s') + + def get_account(self): + # TODO: fetch account data and keep in cache + return None + + def get_candles(self, data_frequency, assets, bar_count=None): + pass + ''' + """ + Retrieve OHLVC candles from Bitfinex + + :param data_frequency: + :param assets: + :param bar_count: + :return: + + Available Frequencies + --------------------- + '1m', '5m', '15m', '30m', '1h', '3h', '6h', '12h', '1D', '7D', '14D', + '1M' + """ + + # TODO: use BcolzMinuteBarReader to read from cache + freq_match = re.match(r'([0-9].*)(m|h|d)', data_frequency, re.M | re.I) + if freq_match: + number = int(freq_match.group(1)) + unit = freq_match.group(2) + + if unit == 'd': + converted_unit = 'D' + else: + converted_unit = unit + + frequency = '{}{}'.format(number, converted_unit) + allowed_frequencies = ['1m', '5m', '15m', '30m', '1h', '3h', '6h', + '12h', '1D', '7D', '14D', '1M'] + + if frequency not in allowed_frequencies: + raise InvalidHistoryFrequencyError( + frequency=data_frequency + ) + elif data_frequency == 'minute': + frequency = '1m' + elif data_frequency == 'daily': + frequency = '1D' + else: + raise InvalidHistoryFrequencyError( + frequency=data_frequency + ) + + # Making sure that assets are iterable + asset_list = [assets] if isinstance(assets, TradingPair) else assets + ohlc_map = dict() + for asset in asset_list: + symbol = self._get_v2_symbol(asset) + url = '{url}/v2/candles/trade:{frequency}:{symbol}'.format( + url=self.url, + frequency=frequency, + symbol=symbol + ) + + if bar_count: + is_list = True + url += '/hist?limit={}'.format(int(bar_count)) + else: + is_list = False + url += '/last' + + try: + response = requests.get(url) + except Exception as e: + raise ExchangeRequestError(error=e) + + if 'error' in response.content: + raise ExchangeRequestError( + error='Unable to retrieve candles: {}'.format( + response.content) + ) + + candles = response.json() + + def ohlc_from_candle(candle): + ohlc = dict( + open=np.float64(candle[1]), + high=np.float64(candle[3]), + low=np.float64(candle[4]), + close=np.float64(candle[2]), + volume=np.float64(candle[5]), + price=np.float64(candle[2]), + last_traded=pd.Timestamp.utcfromtimestamp( + candle[0] / 1000.0) + ) + return ohlc + + if is_list: + ohlc_bars = [] + # We can to list candles from old to new + for candle in reversed(candles): + ohlc = ohlc_from_candle(candle) + ohlc_bars.append(ohlc) + + ohlc_map[asset] = ohlc_bars + + else: + ohlc = ohlc_from_candle(candles) + ohlc_map[asset] = ohlc + + return ohlc_map[assets] \ + if isinstance(assets, TradingPair) else ohlc_map + ''' + + def create_order(self, asset, amount, is_buy, style): + pass + ''' + """ + Creating order on the exchange. + + :param asset: + :param amount: + :param is_buy: + :param style: + :return: + """ + exchange_symbol = self.get_symbol(asset) + if isinstance(style, ExchangeLimitOrder) \ + or isinstance(style, ExchangeStopLimitOrder): + price = style.get_limit_price(is_buy) + order_type = 'limit' + + elif isinstance(style, ExchangeStopOrder): + price = style.get_stop_price(is_buy) + order_type = 'stop' + + else: + raise InvalidOrderStyle(exchange=self.name, + style=style.__class__.__name__) + + req = dict( + symbol=exchange_symbol, + amount=str(float(abs(amount))), + price="{:.20f}".format(float(price)), + side='buy' if is_buy else 'sell', + type='exchange ' + order_type, # TODO: support margin trades + exchange=self.name, + is_hidden=False, + is_postonly=False, + use_all_available=0, + ocoorder=False, + buy_price_oco=0, + sell_price_oco=0 + ) + + date = pd.Timestamp.utcnow() + try: + response = self._request('order/new', req) + order_status = response.json() + except Exception as e: + raise ExchangeRequestError(error=e) + + if 'message' in order_status: + raise ExchangeRequestError( + error='unable to create Bitfinex order {}'.format( + order_status['message']) + ) + + order_id = str(order_status['id']) + order = Order( + dt=date, + asset=asset, + amount=amount, + stop=style.get_stop_price(is_buy), + limit=style.get_limit_price(is_buy), + id=order_id + ) + + return order + ''' + + def get_open_orders(self, asset=None): + """Retrieve all of the current open orders. + + Parameters + ---------- + asset : Asset + If passed and not None, return only the open orders for the given + asset instead of all open orders. + + Returns + ------- + open_orders : dict[list[Order]] or list[Order] + If no asset is passed this will return a dict mapping Assets + to a list containing all the open orders for the asset. + If an asset is passed then this will return a list of the open + orders for this asset. + """ + pass + ''' + try: + response = self._request('orders', None) + order_statuses = response.json() + except Exception as e: + raise ExchangeRequestError(error=e) + + if 'message' in order_statuses: + raise ExchangeRequestError( + error='Unable to retrieve open orders: {}'.format( + order_statuses['message']) + ) + + orders = list() + for order_status in order_statuses: + order, executed_price = self._create_order(order_status) + if asset is None or asset == order.sid: + orders.append(order) + + return orders + ''' + + def get_order(self, order_id): + """Lookup an order based on the order id returned from one of the + order functions. + + Parameters + ---------- + order_id : str + The unique identifier for the order. + + Returns + ------- + order : Order + The order object. + """ + pass + ''' + try: + response = self._request( + 'order/status', {'order_id': int(order_id)}) + order_status = response.json() + except Exception as e: + raise ExchangeRequestError(error=e) + + if 'message' in order_status: + raise ExchangeRequestError( + error='Unable to retrieve order status: {}'.format( + order_status['message']) + ) + return self._create_order(order_status) + ''' + + def cancel_order(self, order_param): + """Cancel an open order. + + Parameters + ---------- + order_param : str or Order + The order_id or order object to cancel. + """ + pass + ''' + order_id = order_param.id \ + if isinstance(order_param, Order) else order_param + + try: + response = self._request('order/cancel', {'order_id': order_id}) + status = response.json() + except Exception as e: + raise ExchangeRequestError(error=e) + + if 'message' in status: + raise OrderCancelError( + order_id=order_id, + exchange=self.name, + error=status['message'] + ) + ''' + + def tickers(self, assets): + """ + Fetch ticket data for assets + https://docs.bitfinex.com/v2/reference#rest-public-tickers + + :param assets: + :return: + """ + pass + + ''' + symbols = self._get_v2_symbols(assets) + log.debug('fetching tickers {}'.format(symbols)) + + try: + response = requests.get( + '{url}/v2/tickers?symbols={symbols}'.format( + url=self.url, + symbols=','.join(symbols), + ) + ) + except Exception as e: + raise ExchangeRequestError(error=e) + + if 'error' in response.content: + raise ExchangeRequestError( + error='Unable to retrieve tickers: {}'.format( + response.content) + ) + + tickers = response.json() + + ticks = dict() + for index, ticker in enumerate(tickers): + if not len(ticker) == 11: + raise ExchangeRequestError( + error='Invalid ticker in response: {}'.format(ticker) + ) + + ticks[assets[index]] = dict( + timestamp=pd.Timestamp.utcnow(), + bid=ticker[1], + ask=ticker[3], + last_price=ticker[7], + low=ticker[10], + high=ticker[9], + volume=ticker[8], + ) + + log.debug('got tickers {}'.format(ticks)) + return ticks + ''' + + def generate_symbols_json(self, filename=None): + symbol_map = {} + response = self.api.returnticker() + for exchange_symbol in response: + base, market = self.sanitize_curency_symbol(exchange_symbol).split('_') + symbol = '{market}_{base}'.format( market=market, base=base ) + symbol_map[exchange_symbol] = dict( + symbol = symbol, + start_date = '2010-01-01' + ) + + if(filename is None): + filename = get_exchange_symbols_filename(self.name) + + with open(filename,'w') as f: + json.dump(symbol_map, f, sort_keys=True, indent=2, separators=(',',':')) + diff --git a/catalyst/exchange/poloniex/poloniex_api.py b/catalyst/exchange/poloniex/poloniex_api.py new file mode 100644 index 00000000..5094dd9f --- /dev/null +++ b/catalyst/exchange/poloniex/poloniex_api.py @@ -0,0 +1,126 @@ +#!/usr/bin/env python +import json +import time +import hmac +import hashlib + +from six.moves import urllib + +# Workaround for backwards compatibility +# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7 +urlopen = urllib.request.urlopen + + +class Poloniex_api(object): + def __init__(self, key, secret): + self.key = key + self.secret = secret + self.public = ['returnTicker', 'return24Volume', 'returnOrderBook', + 'returnTradeHistory', 'returnChartData', + 'returnCurrencies', 'returnLoanOrders'] + self.trading = ['returnBalances','returnCompleteBalances','returnDepositAddresses', + 'generateNewAddress','returnDepositsWithdrawals','returnOpenOrders', + 'returnTradeHistory','returnOrderTrades', + 'buy', 'sell', 'cancelOrder', 'moveOrder', + 'withdraw', 'returnFeeInfo','returnAvailableAccountBalances', + 'returnTradableBalances', 'transferBalance', + 'returnMarginAccountSummary','marginBuy','marginSell', + 'getMarginPosition', 'closeMarginPosition','createLoanOffer', + 'cancelLoanOffer','returnOpenLoanOffers','returnActiveLoans', + 'returnLendingHistory','toggleAutoRenew'] + + def query(self, method, values={}): + + if method in self.public: + url = 'https://poloniex.com/public?command=' + method + urllib.parse.urlencode(values) + headers = {} + post_data = None + elif method in self.trading: + url = 'https://poloniex.com/tradingApi' + req['command'] = method + req['nonce'] = int(time.time()*1000) + post_data = urllib.urlencode(req) + signature = hmac.new(self.secret, post_data, hashlib.sha512).hexdigest() + headers = { 'Sign': signature, 'Key': self.key} + + req = urllib.request.Request(url, data=post_data, headers=headers) + return json.loads(urlopen(req).read()) + + def returnticker(self): + return self.query('returnTicker') + + def return24volume(self): + return self.query('return24Volume') + + def returnOrderBook(self, market='all'): + return self.query('returnOrderBook', {'currencyPair': market}) + + def returntradehistory(self, market, start=None, end=None): + if(start is not None and end is not None): + return self.query('returntradehistory', + {'currencyPair': market, 'start': start, 'end': end }) + else: + return self.query('returntradehistory', {'currencyPair': market }) + + def returnchartdata(self, market, period, start, end): + return self.query('returnChartData', {'currencyPair': market, 'period': period, + 'start': start, 'end': end}) + + def returncurrencies(self): + return self.query('returnCurrencies') + + def returnloadorders(self, market): + return self.query('returnLoanOrders', {'market': market}) + + ''' + def buylimit(self, market, quantity, rate): + return self.query('buylimit', {'market': market, 'quantity': quantity, + 'rate': rate}) + + def buymarket(self, market, quantity): + return self.query('buymarket', + {'market': market, 'quantity': quantity}) + + def selllimit(self, market, quantity, rate): + return self.query('selllimit', {'market': market, 'quantity': quantity, + 'rate': rate}) + + def sellmarket(self, market, quantity): + return self.query('sellmarket', + {'market': market, 'quantity': quantity}) + + def cancel(self, uuid): + return self.query('cancel', {'uuid': uuid}) + + def getopenorders(self, market): + return self.query('getopenorders', {'market': market}) + + def getbalances(self): + return self.query('getbalances') + + def getbalance(self, currency): + return self.query('getbalance', {'currency': currency}) + + def getdepositaddress(self, currency): + return self.query('getdepositaddress', {'currency': currency}) + + def withdraw(self, currency, quantity, address): + return self.query('withdraw', + {'currency': currency, 'quantity': quantity, + 'address': address}) + + def getorder(self, uuid): + return self.query('getorder', {'uuid': uuid}) + + def getorderhistory(self, market, count): + return self.query('getorderhistory', + {'market': market, 'count': count}) + + def getwithdrawalhistory(self, currency, count): + return self.query('getwithdrawalhistory', + {'currency': currency, 'count': count}) + + def getdeposithistory(self, currency, count): + return self.query('getdeposithistory', + {'currency': currency, 'count': count}) + ''' diff --git a/catalyst/utils/run_algo.py b/catalyst/utils/run_algo.py index fc9f6354..63063c55 100644 --- a/catalyst/utils/run_algo.py +++ b/catalyst/utils/run_algo.py @@ -11,6 +11,8 @@ import pandas as pd import click from catalyst.exchange.bittrex.bittrex import Bittrex +from catalyst.exchange.bitfinex.bitfinex import Bitfinex +from catalyst.exchange.poloniex.poloniex import Poloniex try: from pygments import highlight @@ -39,7 +41,6 @@ import catalyst.utils.paths as pth from catalyst.exchange.algorithm_exchange import ExchangeTradingAlgorithm from catalyst.exchange.data_portal_exchange import DataPortalExchange -from catalyst.exchange.bitfinex.bitfinex import Bitfinex from catalyst.exchange.asset_finder_exchange import AssetFinderExchange from catalyst.exchange.exchange_portfolio import ExchangePortfolio from catalyst.exchange.exchange_errors import ( @@ -178,6 +179,13 @@ def _run(handle_data, base_currency=base_currency, portfolio=portfolio ) + elif exchange_name == 'poloniex': + exchange = Poloniex( + key=exchange_auth['key'], + secret=exchange_auth['secret'], + base_currency=base_currency, + portfolio=portfolio + ) else: raise NotImplementedError( 'exchange not supported: %s' % exchange_name)