diff --git a/.travis.yml b/.travis.yml index ac45ea3..d72ee88 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,4 @@ python: install: - pip install -r requirements.txt script: - - python -m unittest bittrex.test.bittrex_tests.TestBittrexPublicAPI + - python -m unittest bittrex.test.bittrex_tests diff --git a/README.md b/README.md index 40645bc..ab325e5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ python-bittrex ============== -[![Build Status](https://travis-ci.org/ericsomdahl/python-bittrex.svg?branch=travis-setup)](https://travis-ci.org/ericsomdahl/python-bittrex) +[![Build Status](https://travis-ci.org/ericsomdahl/python-bittrex.svg?branch=master)](https://travis-ci.org/ericsomdahl/python-bittrex) Python bindings for bittrex. I am Not associated -- use at your own risk, etc. @@ -10,17 +10,19 @@ Tips are appreciated: * LTC: LaasG9TRa9p32noN2oKUVVqrDFp4Ja1NK3 -Example Usage +Example Usage for Bittrex API ------------- ```python from bittrex import Bittrex -my_bittrex = Bittrex(None, None) +my_bittrex = Bittrex(None, None, api_version=API_V2_0) # or defaulting to v1.1 as Bittrex(None, None) my_bittrex.get_markets() {'success': True, 'message': '', 'result': [{'MarketCurrency': 'LTC', ... ``` +API_V2_0 and API_V1_1 are constants that can be imported from Bittrex. + To access account methods, an API key for your account is required and can be generated on the `Settings` then `API Keys` page. Make sure you save the secret, as it will not be visible @@ -29,7 +31,7 @@ after navigating away from the page. ```python from bittrex import Bittrex -my_bittrex = Bittrex("", "") +my_bittrex = Bittrex("", "", api_version=" or ") my_bittrex.get_balance('ETH') {'success': True, @@ -39,6 +41,39 @@ my_bittrex.get_balance('ETH') } ``` +v1.1 constants of interest: +--- +``` +BUY_ORDERBOOK = 'buy' +SELL_ORDERBOOK = 'sell' +BOTH_ORDERBOOK = 'both' +``` + +v2.0 constants of interest +--- +These are used by get_candles() +``` +TICKINTERVAL_ONEMIN = 'oneMin' +TICKINTERVAL_FIVEMIN = 'fiveMin' +TICKINTERVAL_HOUR = 'hour' +TICKINTERVAL_THIRTYMIN = 'thirtyMin' +TICKINTERVAL_DAY = 'Day' +``` +these are used by trade_sell() and trade_buy() +``` +ORDERTYPE_LIMIT = 'LIMIT' +ORDERTYPE_MARKET = 'MARKET' + +TIMEINEFFECT_GOOD_TIL_CANCELLED = 'GOOD_TIL_CANCELLED' +TIMEINEFFECT_IMMEDIATE_OR_CANCEL = 'IMMEDIATE_OR_CANCEL' +TIMEINEFFECT_FILL_OR_KILL = 'FILL_OR_KILL' + +CONDITIONTYPE_NONE = 'NONE' +CONDITIONTYPE_GREATER_THAN = 'GREATER_THAN' +CONDITIONTYPE_LESS_THAN = 'LESS_THAN' +CONDITIONTYPE_STOP_LOSS_FIXED = 'STOP_LOSS_FIXED' +CONDITIONTYPE_STOP_LOSS_PERCENTAGE = 'STOP_LOSS_PERCENTAGE' +``` Testing ------- diff --git a/bittrex/bittrex.py b/bittrex/bittrex.py index 6ad6a4b..798515b 100644 --- a/bittrex/bittrex.py +++ b/bittrex/bittrex.py @@ -5,6 +5,7 @@ import time import hmac import hashlib + try: from urllib import urlencode from urlparse import urljoin @@ -20,36 +21,42 @@ import getpass import ast import json + encrypted = True import requests - BUY_ORDERBOOK = 'buy' SELL_ORDERBOOK = 'sell' BOTH_ORDERBOOK = 'both' -BASE_URL = 'https://bittrex.com/api/v1.1/{method_set}/{method}?' - -MARKET_SET = { - 'getopenorders', - 'cancel', - 'sellmarket', - 'selllimit', - 'buymarket', - 'buylimit' -} - -ACCOUNT_SET = { - 'getbalances', - 'getbalance', - 'getdepositaddress', - 'withdraw', - 'getorderhistory', - 'getorder', - 'getdeposithistory', - 'getwithdrawalhistory' -} +TICKINTERVAL_ONEMIN = 'oneMin' +TICKINTERVAL_FIVEMIN = 'fiveMin' +TICKINTERVAL_HOUR = 'hour' +TICKINTERVAL_THIRTYMIN = 'thirtyMin' +TICKINTERVAL_DAY = 'Day' + +ORDERTYPE_LIMIT = 'LIMIT' +ORDERTYPE_MARKET = 'MARKET' + +TIMEINEFFECT_GOOD_TIL_CANCELLED = 'GOOD_TIL_CANCELLED' +TIMEINEFFECT_IMMEDIATE_OR_CANCEL = 'IMMEDIATE_OR_CANCEL' +TIMEINEFFECT_FILL_OR_KILL = 'FILL_OR_KILL' + +CONDITIONTYPE_NONE = 'NONE' +CONDITIONTYPE_GREATER_THAN = 'GREATER_THAN' +CONDITIONTYPE_LESS_THAN = 'LESS_THAN' +CONDITIONTYPE_STOP_LOSS_FIXED = 'STOP_LOSS_FIXED' +CONDITIONTYPE_STOP_LOSS_PERCENTAGE = 'STOP_LOSS_PERCENTAGE' + +API_V1_1 = 'v1.1' +API_V2_0 = 'v2.0' + +BASE_URL_V1_1 = 'https://bittrex.com/api/v1.1{path}?' +BASE_URL_V2_0 = 'https://bittrex.com/api/v2.0{path}?' + +PROTECTION_PUB = 'pub' # public methods +PROTECTION_PRV = 'prv' # authenticated methods def encrypt(api_key, api_secret, export=True, export_fn='secrets.json'): @@ -66,21 +73,23 @@ def encrypt(api_key, api_secret, export=True, export_fn='secrets.json'): def using_requests(request_url, apisign): return requests.get( - request_url, - headers={"apisign": apisign} - ).json() + request_url, + headers={"apisign": apisign} + ).json() class Bittrex(object): """ Used for requesting Bittrex with API key and API secret """ - def __init__(self, api_key, api_secret, calls_per_second=1, dispatch=using_requests): + + def __init__(self, api_key, api_secret, calls_per_second=1, dispatch=using_requests, api_version=API_V1_1): self.api_key = str(api_key) if api_key is not None else '' self.api_secret = str(api_secret) if api_secret is not None else '' self.dispatch = dispatch - self.call_rate = 1.0/calls_per_second + self.call_rate = 1.0 / calls_per_second self.last_call = None + self.api_version = api_version def decrypt(self): if encrypted: @@ -105,37 +114,34 @@ def wait(self): now = time.time() passed = now - self.last_call if passed < self.call_rate: - #print("sleep") + # print("sleep") time.sleep(1.0 - passed) self.last_call = time.time() - def api_query(self, method, options=None): + def _api_query(self, protection=None, path_dict=None, options=None): """ - Queries Bittrex with given method and options. + Queries Bittrex - :param method: Query method for getting info - :type method: str - :param options: Extra options for query + :param request_url: fully-formed URL to request :type options: dict :return: JSON response from Bittrex :rtype : dict """ + if not options: options = {} - nonce = str(int(time.time() * 1000)) - method_set = 'public' - if method in MARKET_SET: - method_set = 'market' - elif method in ACCOUNT_SET: - method_set = 'account' + if self.api_version not in path_dict: + raise Exception('method call not available under API version {}'.format(self.api_version)) - request_url = BASE_URL.format(method_set=method_set, method=method) + request_url = BASE_URL_V2_0 if self.api_version == API_V2_0 else BASE_URL_V1_1 + request_url = request_url.format(path=path_dict[self.api_version]) - if method_set != 'public': - request_url = "{0}apikey={1}&nonce={2}&".format( - request_url, self.api_key, nonce) + nonce = str(int(time.time() * 1000)) + + if protection != PROTECTION_PUB: + request_url = "{0}apikey={1}&nonce={2}&".format(request_url, self.api_key, nonce) request_url += urlencode(options) @@ -152,7 +158,8 @@ def get_markets(self): Used to get the open and available trading markets at Bittrex along with other meta data. - Endpoint: /public/getmarkets + 1.1 Endpoint: /public/getmarkets + 2.0 Endpoint: /pub/Markets/GetMarkets Example :: {'success': True, @@ -175,63 +182,89 @@ def get_markets(self): :return: Available market info in JSON :rtype : dict """ - return self.api_query('getmarkets') + return self._api_query(path_dict={ + API_V1_1: '/public/getmarkets', + API_V2_0: '/pub/Markets/GetMarkets' + }, protection=PROTECTION_PUB) def get_currencies(self): """ Used to get all supported currencies at Bittrex along with other meta data. - Endpoint: /public/getcurrencies + Endpoint: + 1.1 /public/getcurrencies + 2.0 /pub/Currencies/GetCurrencies :return: Supported currencies info in JSON :rtype : dict """ - return self.api_query('getcurrencies') + return self._api_query(path_dict={ + API_V1_1: '/public/getcurrencies', + API_V2_0: '/pub/Currencies/GetCurrencies' + }, protection=PROTECTION_PUB) def get_ticker(self, market): """ Used to get the current tick values for a market. - Endpoint: /public/getticker + Endpoints: + 1.1 /public/getticker + 2.0 NO EQUIVALENT -- but get_candlesticks gives comparable data :param market: String literal for the market (ex: BTC-LTC) :type market: str :return: Current values for given market in JSON :rtype : dict """ - return self.api_query('getticker', {'market': market}) + return self._api_query(path_dict={ + API_V1_1: '/public/getticker', + }, options={'market': market}, protection=PROTECTION_PUB) def get_market_summaries(self): """ Used to get the last 24 hour summary of all active exchanges - Endpoint: /public/getmarketsummary + Endpoint: + 1.1 /public/getmarketsummaries + 2.0 /pub/Markets/GetMarketSummaries :return: Summaries of active exchanges in JSON :rtype : dict """ - return self.api_query('getmarketsummaries') + return self._api_query(path_dict={ + API_V1_1: '/public/getmarketsummaries', + API_V2_0: '/pub/Markets/GetMarketSummaries' + }, protection=PROTECTION_PUB) def get_marketsummary(self, market): """ Used to get the last 24 hour summary of all active exchanges in specific coin - Endpoint: /public/getmarketsummary + Endpoint: + 1.1 /public/getmarketsummary + 2.0 /pub/Market/GetMarketSummary :param market: String literal for the market(ex: BTC-XRP) :type market: str :return: Summaries of active exchanges of a coin in JSON :rtype : dict """ - return self.api_query('getmarketsummary', {'market': market}) + return self._api_query(path_dict={ + API_V1_1: '/public/getmarketsummary', + API_V2_0: '/pub/Market/GetMarketSummary' + }, options={'market': market, 'marketname': market}, protection=PROTECTION_PUB) - def get_orderbook(self, market, depth_type, depth=20): + def get_orderbook(self, market, depth_type=BOTH_ORDERBOOK): """ - Used to get retrieve the orderbook for a given market + Used to get retrieve the orderbook for a given market. - Endpoint: /public/getorderbook + The depth_type parameter is IGNORED under v2.0 and both orderbooks are aleways returned + + Endpoint: + 1.1 /public/getorderbook + 2.0 /pub/Market/GetMarketOrderBook :param market: String literal for the market (ex: BTC-LTC) :type market: str @@ -239,23 +272,22 @@ def get_orderbook(self, market, depth_type, depth=20): orderbook to return. Use constants BUY_ORDERBOOK, SELL_ORDERBOOK, BOTH_ORDERBOOK :type depth_type: str - :param depth: how deep of an order book to retrieve. - Max is 100, default is 20 - :type depth: int :return: Orderbook of market in JSON :rtype : dict """ - return self.api_query('getorderbook', - {'market': market, - 'type': depth_type, - 'depth': depth}) + return self._api_query(path_dict={ + API_V1_1: '/public/getorderbook', + API_V2_0: '/pub/Market/GetMarketOrderBook' + }, options={'market': market, 'marketname': market, 'type': depth_type}, protection=PROTECTION_PUB) - def get_market_history(self, market, count=20): + def get_market_history(self, market): """ Used to retrieve the latest trades that have occurred for a specific market. - Endpoint: /market/getmarkethistory + Endpoint: + 1.1 /market/getmarkethistory + 2.0 /pub/Market/GetMarketHistory Example :: {'success': True, @@ -273,14 +305,13 @@ def get_market_history(self, market, count=20): :param market: String literal for the market (ex: BTC-LTC) :type market: str - :param count: Number between 1-100 for the number - of entries to return (default = 20) - :type count: int :return: Market history in JSON :rtype : dict """ - return self.api_query('getmarkethistory', - {'market': market, 'count': count}) + return self._api_query(path_dict={ + API_V1_1: '/public/getmarkethistory', + API_V2_0: '/pub/Market/GetMarketHistory' + }, options={'market': market, 'marketname': market}, protection=PROTECTION_PUB) def buy_limit(self, market, quantity, rate): """ @@ -288,7 +319,9 @@ def buy_limit(self, market, quantity, rate): limit orders Make sure you have the proper permissions set on your API keys for this call to work - Endpoint: /market/buylimit + Endpoint: + 1.1 /market/buylimit + 2.0 NO Direct equivalent. Use trade_buy for LIMIT and MARKET buys :param market: String literal for the market (ex: BTC-LTC) :type market: str @@ -300,10 +333,11 @@ def buy_limit(self, market, quantity, rate): :return: :rtype : dict """ - return self.api_query('buylimit', - {'market': market, - 'quantity': quantity, - 'rate': rate}) + return self._api_query(path_dict={ + API_V1_1: '/market/buylimit', + }, options={'market': market, + 'quantity': quantity, + 'rate': rate}, protection=PROTECTION_PRV) def sell_limit(self, market, quantity, rate): """ @@ -311,7 +345,9 @@ def sell_limit(self, market, quantity, rate): limit orders Make sure you have the proper permissions set on your API keys for this call to work - Endpoint: /market/selllimit + Endpoint: + 1.1 /market/selllimit + 2.0 NO Direct equivalent. Use trade_sell for LIMIT and MARKET sells :param market: String literal for the market (ex: BTC-LTC) :type market: str @@ -323,44 +359,56 @@ def sell_limit(self, market, quantity, rate): :return: :rtype : dict """ - return self.api_query('selllimit', - {'market': market, - 'quantity': quantity, - 'rate': rate}) + return self._api_query(path_dict={ + API_V1_1: '/market/selllimit', + }, options={'market': market, + 'quantity': quantity, + 'rate': rate}, protection=PROTECTION_PRV) def cancel(self, uuid): """ Used to cancel a buy or sell order - Endpoint: /market/cancel + Endpoint: + 1.1 /market/cancel + 2.0 /key/market/cancel :param uuid: uuid of buy or sell order :type uuid: str :return: :rtype : dict """ - return self.api_query('cancel', {'uuid': uuid}) + return self._api_query(path_dict={ + API_V1_1: '/market/cancel', + API_V2_0: '/key/market/cancel' + }, options={'uuid': uuid, 'orderid': uuid}, protection=PROTECTION_PRV) def get_open_orders(self, market=None): """ Get all orders that you currently have opened. A specific market can be requested. - Endpoint: /market/getopenorders + Endpoint: + 1.1 /market/getopenorders + 2.0 /key/market/getopenorders :param market: String literal for the market (ie. BTC-LTC) :type market: str :return: Open orders info in JSON :rtype : dict """ - return self.api_query('getopenorders', - {'market': market} if market else None) + return self._api_query(path_dict={ + API_V1_1: '/market/getopenorders', + API_V2_0: '/key/market/getopenorders' + }, options={'market': market, 'marketname': market} if market else None, protection=PROTECTION_PRV) def get_balances(self): """ Used to retrieve all balances from your account. - Endpoint: /account/getbalances + Endpoint: + 1.1 /account/getbalances + 2.0 /key/balance/getbalances Example :: {'success': True, @@ -378,13 +426,18 @@ def get_balances(self): :return: Balances info in JSON :rtype : dict """ - return self.api_query('getbalances', {}) + return self._api_query(path_dict={ + API_V1_1: '/account/getbalances', + API_V2_0: '/key/balance/getbalances' + }, protection=PROTECTION_PRV) def get_balance(self, currency): """ Used to retrieve the balance from your account for a specific currency - Endpoint: /account/getbalance + Endpoint: + 1.1 /account/getbalance + 2.0 /key/balance/getbalance Example :: {'success': True, @@ -402,26 +455,36 @@ def get_balance(self, currency): :return: Balance info in JSON :rtype : dict """ - return self.api_query('getbalance', {'currency': currency}) + return self._api_query(path_dict={ + API_V1_1: '/account/getbalance', + API_V2_0: '/key/balance/getbalance' + }, options={'currency': currency, 'currencyname': currency}, protection=PROTECTION_PRV) def get_deposit_address(self, currency): """ Used to generate or retrieve an address for a specific currency - Endpoint: /account/getdepositaddress + Endpoint: + 1.1 /account/getdepositaddress + 2.0 /key/balance/getdepositaddress :param currency: String literal for the currency (ie. BTC) :type currency: str :return: Address info in JSON :rtype : dict """ - return self.api_query('getdepositaddress', {'currency': currency}) + return self._api_query(path_dict={ + API_V1_1: '/account/getdepositaddress', + API_V2_0: '/key/balance/getdepositaddress' + }, options={'currency': currency, 'currencyname': currency}, protection=PROTECTION_PRV) def withdraw(self, currency, quantity, address): """ Used to withdraw funds from your account - Endpoint: /account/withdraw + Endpoint: + 1.1 /account/withdraw + 2.0 /key/balance/withdrawcurrency :param currency: String literal for the currency (ie. BTC) :type currency: str @@ -432,16 +495,18 @@ def withdraw(self, currency, quantity, address): :return: :rtype : dict """ - return self.api_query('withdraw', - {'currency': currency, - 'quantity': quantity, - 'address': address}) + return self._api_query(path_dict={ + API_V1_1: '/account/withdraw', + API_V2_0: '/key/balance/withdrawcurrency' + }, options={'currency': currency, 'quantity': quantity, 'address': address}, protection=PROTECTION_PRV) def get_order_history(self, market=None): """ Used to retrieve order trade history of account - Endpoint: /account/getorderhistory + Endpoint: + 1.1 /account/getorderhistory + 2.0 /key/orders/getorderhistory :param market: optional a string literal for the market (ie. BTC-LTC). If omitted, will return for all markets @@ -449,27 +514,36 @@ def get_order_history(self, market=None): :return: order history in JSON :rtype : dict """ - return self.api_query('getorderhistory', - {'market': market} if market else None) + return self._api_query(path_dict={ + API_V1_1: '/account/getorderhistory', + API_V2_0: '/key/orders/getorderhistory' + }, options={'market': market, 'marketname': market} if market else None, protection=PROTECTION_PRV) def get_order(self, uuid): """ Used to get details of buy or sell order - Endpoint: /account/getorder + Endpoint: + 1.1 /account/getorder + 2.0 /key/orders/getorder :param uuid: uuid of buy or sell order :type uuid: str :return: :rtype : dict """ - return self.api_query('getorder', {'uuid': uuid}) + return self._api_query(path_dict={ + API_V1_1: '/account/getorder', + API_V2_0: '/key/orders/getorder' + }, options={'uuid': uuid, 'orderid': uuid}, protection=PROTECTION_PRV) def get_withdrawal_history(self, currency=None): """ Used to view your history of withdrawals - Endpoint: /account/getwithdrawalhistory + Endpoint: + 1.1 /account/getwithdrawalhistory + 2.0 /key/balance/getwithdrawalhistory :param currency: String literal for the currency (ie. BTC) :type currency: str @@ -477,22 +551,30 @@ def get_withdrawal_history(self, currency=None): :rtype : dict """ - return self.api_query('getwithdrawalhistory', - {'currency': currency} if currency else None) + return self._api_query(path_dict={ + API_V1_1: '/account/getwithdrawalhistory', + API_V2_0: '/key/balance/getwithdrawalhistory' + }, options={'currency': currency, 'currencyname': currency} if currency else None, + protection=PROTECTION_PRV) def get_deposit_history(self, currency=None): """ Used to view your history of deposits - Endpoint: /account/getdeposithistory + Endpoint: + 1.1 /account/getdeposithistory + 2.0 /key/balance/getdeposithistory :param currency: String literal for the currency (ie. BTC) :type currency: str :return: deposit history in JSON :rtype : dict """ - return self.api_query('getdeposithistory', - {'currency': currency} if currency else None) + return self._api_query(path_dict={ + API_V1_1: '/account/getdeposithistory', + API_V2_0: '/key/balance/getdeposithistory' + }, options={'currency': currency, 'currencyname': currency} if currency else None, + protection=PROTECTION_PRV) def list_markets_by_currency(self, currency): """ @@ -511,3 +593,202 @@ def list_markets_by_currency(self, currency): """ return [market['MarketName'] for market in self.get_markets()['result'] if market['MarketName'].lower().endswith(currency.lower())] + + def get_wallet_health(self): + """ + Used to view wallet health + + Endpoints: + 1.1 NO Equivalent + 2.0 /pub/Currencies/GetWalletHealth + + :return: + """ + return self._api_query(path_dict={ + API_V2_0: '/pub/Currencies/GetWalletHealth' + }, protection=PROTECTION_PUB) + + def get_balance_distribution(self): + """ + Used to view balance distibution + + Endpoints: + 1.1 NO Equivalent + 2.0 /pub/Currency/GetBalanceDistribution + + :return: + """ + return self._api_query(path_dict={ + API_V2_0: '/pub/Currency/GetBalanceDistribution' + }, protection=PROTECTION_PUB) + + def get_pending_withdrawls(self, currency=None): + """ + Used to view your pending withdrawls + + Endpoint: + 1.1 NO EQUIVALENT + 2.0 /key/balance/getpendingwithdrawals + + :param currency: String literal for the currency (ie. BTC) + :type currency: str + :return: pending widthdrawls in JSON + :rtype : list + """ + return self._api_query(path_dict={ + API_V2_0: '/key/balance/getpendingwithdrawals' + }, options={'currencyname': currency} if currency else None, + protection=PROTECTION_PRV) + + def get_pending_deposits(self, currency=None): + """ + Used to view your pending deposits + + Endpoint: + 1.1 NO EQUIVALENT + 2.0 /key/balance/getpendingdeposits + + :param currency: String literal for the currency (ie. BTC) + :type currency: str + :return: pending deposits in JSON + :rtype : list + """ + return self._api_query(path_dict={ + API_V2_0: '/key/balance/getpendingdeposits' + }, options={'currencyname': currency} if currency else None, + protection=PROTECTION_PRV) + + def generate_deposit_address(self, currency): + """ + Generate a deposit address for the specified currency + + Endpoint: + 1.1 NO EQUIVALENT + 2.0 /key/balance/generatedepositaddress + + :param currency: String literal for the currency (ie. BTC) + :type currency: str + :return: result of creation operation + :rtype : dict + """ + return self._api_query(path_dict={ + API_V2_0: '/key/balance/getpendingdeposits' + }, options={'currencyname': currency}, protection=PROTECTION_PRV) + + def trade_sell(self, market=None, order_type=None, quantity=None, rate=None, time_in_effect=None, + condition_type=None, target=0.0): + """ + Enter a sell order into the book + Endpoint + 1.1 NO EQUIVALENT -- see sell_market or sell_limit + 2.0 /key/market/tradesell + + :param market: String literal for the market (ex: BTC-LTC) + :type market: str + :param order_type: ORDERTYPE_LIMIT = 'LIMIT' or ORDERTYPE_MARKET = 'MARKET' + :type order_type: str + :param quantity: The amount to purchase + :type quantity: float + :param rate: The rate at which to place the order. + This is not needed for market orders + :type rate: float + :param time_in_effect: TIMEINEFFECT_GOOD_TIL_CANCELLED = 'GOOD_TIL_CANCELLED', + TIMEINEFFECT_IMMEDIATE_OR_CANCEL = 'IMMEDIATE_OR_CANCEL', or TIMEINEFFECT_FILL_OR_KILL = 'FILL_OR_KILL' + :type time_in_effect: str + :param condition_type: CONDITIONTYPE_NONE = 'NONE', CONDITIONTYPE_GREATER_THAN = 'GREATER_THAN', + CONDITIONTYPE_LESS_THAN = 'LESS_THAN', CONDITIONTYPE_STOP_LOSS_FIXED = 'STOP_LOSS_FIXED', + CONDITIONTYPE_STOP_LOSS_PERCENTAGE = 'STOP_LOSS_PERCENTAGE' + :type condition_type: str + :param target: used in conjunction with condition_type + :type target: float + :return: + """ + return self._api_query(path_dict={ + API_V2_0: '/key/market/tradesell' + }, options={ + 'marketname': market, + 'ordertype': order_type, + 'quantity': quantity, + 'rate': rate, + 'timeInEffect': time_in_effect, + 'conditiontype': condition_type, + 'target': target + }, protection=PROTECTION_PRV) + + def trade_buy(self, market=None, order_type=None, quantity=None, rate=None, time_in_effect=None, + condition_type=None, target=0.0): + """ + Enter a buy order into the book + Endpoint + 1.1 NO EQUIVALENT -- see buy_market or buy_limit + 2.0 /key/market/tradebuy + + :param market: String literal for the market (ex: BTC-LTC) + :type market: str + :param order_type: ORDERTYPE_LIMIT = 'LIMIT' or ORDERTYPE_MARKET = 'MARKET' + :type order_type: str + :param quantity: The amount to purchase + :type quantity: float + :param rate: The rate at which to place the order. + This is not needed for market orders + :type rate: float + :param time_in_effect: TIMEINEFFECT_GOOD_TIL_CANCELLED = 'GOOD_TIL_CANCELLED', + TIMEINEFFECT_IMMEDIATE_OR_CANCEL = 'IMMEDIATE_OR_CANCEL', or TIMEINEFFECT_FILL_OR_KILL = 'FILL_OR_KILL' + :type time_in_effect: str + :param condition_type: CONDITIONTYPE_NONE = 'NONE', CONDITIONTYPE_GREATER_THAN = 'GREATER_THAN', + CONDITIONTYPE_LESS_THAN = 'LESS_THAN', CONDITIONTYPE_STOP_LOSS_FIXED = 'STOP_LOSS_FIXED', + CONDITIONTYPE_STOP_LOSS_PERCENTAGE = 'STOP_LOSS_PERCENTAGE' + :type condition_type: str + :param target: used in conjunction with condition_type + :type target: float + :return: + """ + return self._api_query(path_dict={ + API_V2_0: '/key/market/tradebuy' + }, options={ + 'marketname': market, + 'ordertype': order_type, + 'quantity': quantity, + 'rate': rate, + 'timeInEffect': time_in_effect, + 'conditiontype': condition_type, + 'target': target + }, protection=PROTECTION_PRV) + + def get_candles(self, market, tick_interval): + """ + Used to get all tick candle for a market. + + Endpoint: + 1.1 NO EQUIVALENT + 2.0 /pub/market/GetTicks + + Example :: + { success: true, + message: '', + result: + [ { O: 421.20630125, + H: 424.03951276, + L: 421.20630125, + C: 421.20630125, + V: 0.05187504, + T: '2016-04-08T00:00:00', + BV: 21.87921187 }, + { O: 420.206, + H: 420.206, + L: 416.78743422, + C: 416.78743422, + V: 2.42281573, + T: '2016-04-09T00:00:00', + BV: 1012.63286332 }] + } + + :return: Available tick candle in JSON + :rtype: dict + """ + + return self._api_query(path_dict={ + API_V2_0: '/pub/market/GetTicks' + }, options={ + 'marketName': market, 'tickInterval': tick_interval + }, protection=PROTECTION_PUB) diff --git a/bittrex/test/bittrex_tests.py b/bittrex/test/bittrex_tests.py index 5aced2d..62a56fe 100644 --- a/bittrex/test/bittrex_tests.py +++ b/bittrex/test/bittrex_tests.py @@ -1,8 +1,9 @@ -__author__ = 'eric' - import unittest import json -from bittrex.bittrex import Bittrex +import os +from bittrex.bittrex import Bittrex, API_V2_0, API_V1_1, BUY_ORDERBOOK, TICKINTERVAL_ONEMIN + +IS_CI_ENV = True if 'IN_CI' in os.environ else False def test_basic_response(unit_test, result, method_name): @@ -17,13 +18,14 @@ def test_auth_basic_failures(unit_test, result, test_type): unit_test.assertIsNone(result['result'], "{0:s} failed response result not None".format(test_type)) -class TestBittrexPublicAPI(unittest.TestCase): +class TestBittrexV11PublicAPI(unittest.TestCase): """ Integration tests for the Bittrex public API. These will fail in the absence of an internet connection or if bittrex API goes down """ + def setUp(self): - self.bittrex = Bittrex(None, None) + self.bittrex = Bittrex(None, None, api_version=API_V1_1) def test_handles_none_key_or_secret(self): self.bittrex = Bittrex(None, None) @@ -36,6 +38,7 @@ def test_handles_none_key_or_secret(self): self.assertTrue(actual['success'], "failed with None secret") self.bittrex = Bittrex(None, "123") + actual = self.bittrex.get_markets() self.assertTrue(actual['success'], "failed with None key") def test_get_markets(self): @@ -47,10 +50,114 @@ def test_get_markets(self): def test_get_currencies(self): actual = self.bittrex.get_currencies() test_basic_response(self, actual, "get_currencies") - pass + def test_get_ticker(self): + actual = self.bittrex.get_ticker(market='BTC-LTC') + test_basic_response(self, actual, "get_ticker") + + def test_get_market_summaries(self): + actual = self.bittrex.get_market_summaries() + test_basic_response(self, actual, "get_market_summaries") + + def test_get_orderbook(self): + actual = self.bittrex.get_orderbook('BTC-LTC', depth_type=BUY_ORDERBOOK) + test_basic_response(self, actual, "get_orderbook") + + def test_get_market_history(self): + actual = self.bittrex.get_market_history('BTC-LTC') + test_basic_response(self, actual, "get_market_history") -class TestBittrexAccountAPI(unittest.TestCase): + def test_list_markets_by_currency(self): + actual = self.bittrex.list_markets_by_currency('LTC') + self.assertListEqual(['BTC-LTC', 'ETH-LTC', 'USDT-LTC'], actual) + + def test_get_wallet_health(self): + self.assertRaisesRegexp(Exception, 'method call not available', self.bittrex.get_wallet_health) + + def test_get_balance_distribution(self): + self.assertRaisesRegexp(Exception, 'method call not available', self.bittrex.get_balance_distribution) + + def test_get_candles(self): + self.assertRaisesRegexp(Exception, 'method call not available', self.bittrex.get_candles, market='BTC-LTC', + tick_interval=TICKINTERVAL_ONEMIN) + + +class TestBittrexV20PublicAPI(unittest.TestCase): + """ + Integration tests for the Bittrex public API. + These will fail in the absence of an internet connection or if bittrex API goes down + """ + + def setUp(self): + self.bittrex = Bittrex(None, None, api_version=API_V2_0) + + def test_handles_none_key_or_secret(self): + self.bittrex = Bittrex(None, None, api_version=API_V2_0) + # could call any public method here + actual = self.bittrex.get_markets() + self.assertTrue(actual['success'], "failed with None key and None secret") + + self.bittrex = Bittrex("123", None, api_version=API_V2_0) + actual = self.bittrex.get_markets() + self.assertTrue(actual['success'], "failed with None secret") + + self.bittrex = Bittrex(None, "123", api_version=API_V2_0) + actual = self.bittrex.get_markets() + self.assertTrue(actual['success'], "failed with None key") + + def test_get_markets(self): + actual = self.bittrex.get_markets() + test_basic_response(self, actual, "get_markets") + self.assertTrue(isinstance(actual['result'], list), "result is not a list") + self.assertTrue(len(actual['result']) > 0, "result list is 0-length") + + def test_get_currencies(self): + actual = self.bittrex.get_currencies() + test_basic_response(self, actual, "get_currencies") + + def test_get_ticker(self): + self.assertRaisesRegexp(Exception, 'method call not available', self.bittrex.get_ticker, + market='BTC-LTC') + + def test_get_market_summaries(self): + actual = self.bittrex.get_market_summaries() + test_basic_response(self, actual, "get_market_summaries") + + def test_get_market_summary(self): + actual = self.bittrex.get_marketsummary(market='BTC-LTC') + test_basic_response(self, actual, "get_marketsummary") + + def test_get_orderbook(self): + actual = self.bittrex.get_orderbook('BTC-LTC') + test_basic_response(self, actual, "get_orderbook") + + def test_get_market_history(self): + actual = self.bittrex.get_market_history('BTC-LTC') + test_basic_response(self, actual, "get_market_history") + + def test_list_markets_by_currency(self): + actual = self.bittrex.list_markets_by_currency('LTC') + self.assertListEqual(['BTC-LTC', 'ETH-LTC', 'USDT-LTC'], actual) + + def test_get_wallet_health(self): + actual = self.bittrex.get_wallet_health() + test_basic_response(self, actual, "get_wallet_health") + self.assertIsInstance(actual['result'], list) + + @unittest.skip("Endpoint 404s. Is this still a valid 2.0 API?") + def test_get_balance_distribution(self): + actual = self.bittrex.get_balance_distribution() + test_basic_response(self, actual, "get_balance_distribution") + self.assertIsInstance(actual['result'], list) + + def test_get_candles(self): + actual = self.bittrex.get_candles('BTC-LTC', tick_interval=TICKINTERVAL_ONEMIN) + test_basic_response(self, actual, "test_get_candles") + self.assertIsInstance(actual['result'], list) + + +@unittest.skipIf(IS_CI_ENV, 'no account secrets uploaded in CI envieonment, TODO') +class TestBittrexV11AccountAPI(unittest.TestCase): """ Integration tests for the Bittrex Account API. * These will fail in the absence of an internet connection or if bittrex API goes down. @@ -62,6 +169,7 @@ class TestBittrexAccountAPI(unittest.TestCase): "secret": "3345745634234534" } """ + def setUp(self): with open("secrets.json") as secrets_file: self.secrets = json.load(secrets_file) @@ -88,17 +196,194 @@ def test_handles_invalid_key_or_secret(self): self.bittrex = Bittrex('invalidkey', 'invalidsecret') actual = self.bittrex.get_balance('BTC') test_auth_basic_failures(self, actual, 'invalid key, invalid secret') - pass + def test_get_openorders(self): + actual = self.bittrex.get_open_orders('BTC-LTC') + test_basic_response(self, actual, "get_openorders") + self.assertTrue(isinstance(actual['result'], list), "result is not a list") + + def test_get_balances(self): + actual = self.bittrex.get_balances() + test_basic_response(self, actual, "get_balances") + self.assertTrue(isinstance(actual['result'], list), "result is not a list") + + def test_get_balance(self): + actual = self.bittrex.get_balance('BTC') + test_basic_response(self, actual, "get_balance") + self.assertTrue(isinstance(actual['result'], dict), "result is not a dict") + self.assertEqual(actual['result']['Currency'], + "BTC", + "requested currency {0:s} does not match returned currency {1:s}" + .format("BTC", actual['result']['Currency'])) + + def test_get_depositaddress(self): + actual = self.bittrex.get_deposit_address('BTC') + if not actual['success']: + self.assertTrue(actual['message'], 'ADDRESS_GENERATING') + else: + test_basic_response(self, actual, "get_deposit_address") + + def test_get_order_history_all_markets(self): + actual = self.bittrex.get_order_history() + test_basic_response(self, actual, "get_order_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_order_history_one_market(self): + actual = self.bittrex.get_order_history(market='BTC-LTC') + test_basic_response(self, actual, "get_order_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_withdrawlhistory_all_currencies(self): + actual = self.bittrex.get_withdrawal_history() + test_basic_response(self, actual, "get_withdrawal_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_withdrawlhistory_one_currency(self): + actual = self.bittrex.get_withdrawal_history('BTC') + test_basic_response(self, actual, "get_withdrawal_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_deposithistory_all_currencies(self): + actual = self.bittrex.get_deposit_history() + test_basic_response(self, actual, "get_deposit_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_deposithistory_one_currency(self): + actual = self.bittrex.get_deposit_history('BTC') + test_basic_response(self, actual, "get_deposit_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_pending_withdrawls(self): + self.assertRaisesRegexp(Exception, 'method call not available', self.bittrex.get_pending_withdrawls) + + def test_get_pending_deposits(self): + self.assertRaisesRegexp(Exception, 'method call not available', self.bittrex.get_pending_deposits) + + def test_generate_deposit_address(self): + self.assertRaisesRegexp(Exception, 'method call not available', self.bittrex.generate_deposit_address, currency='BTC') + + +@unittest.skipIf(IS_CI_ENV, 'no account secrets uploaded in CI envieonment, TODO') +class TestBittrexV20AccountAPI(unittest.TestCase): + """ + Integration tests for the Bittrex Account API. + * These will fail in the absence of an internet connection or if bittrex API goes down. + * They require a valid API key and secret issued by Bittrex. + * They also require the presence of a JSON file called secrets.json. + It is structured as such: + { + "key": "12341253456345", + "secret": "3345745634234534" + } + """ + + def setUp(self): + with open("secrets.json") as secrets_file: + self.secrets = json.load(secrets_file) + secrets_file.close() + self.bittrex = Bittrex(self.secrets['key'], self.secrets['secret'], api_version=API_V2_0) + + def test_handles_invalid_key_or_secret(self): + self.bittrex = Bittrex('invalidkey', self.secrets['secret'], api_version=API_V2_0) + actual = self.bittrex.get_balance('BTC') + test_auth_basic_failures(self, actual, 'Invalid key, valid secret') + + self.bittrex = Bittrex(None, self.secrets['secret'], api_version=API_V2_0) + actual = self.bittrex.get_balance('BTC') + test_auth_basic_failures(self, actual, 'None key, valid secret') + + self.bittrex = Bittrex(self.secrets['key'], 'invalidsecret', api_version=API_V2_0) + actual = self.bittrex.get_balance('BTC') + test_auth_basic_failures(self, actual, 'valid key, invalid secret') + + self.bittrex = Bittrex(self.secrets['key'], None, api_version=API_V2_0) + actual = self.bittrex.get_balance('BTC') + test_auth_basic_failures(self, actual, 'valid key, None secret') + + self.bittrex = Bittrex('invalidkey', 'invalidsecret', api_version=API_V2_0) + actual = self.bittrex.get_balance('BTC') + test_auth_basic_failures(self, actual, 'invalid key, invalid secret') + + def test_get_openorders(self): + actual = self.bittrex.get_open_orders('BTC-LTC') + test_basic_response(self, actual, "get_openorders") + self.assertTrue(isinstance(actual['result'], list), "result is not a list") + + def test_get_balances(self): + actual = self.bittrex.get_balances() + test_basic_response(self, actual, "get_balances") + self.assertTrue(isinstance(actual['result'], list), "result is not a list") + + @unittest.skip("the return result is an empty dict. API bug? the 2.0 get_balances works as expected") def test_get_balance(self): actual = self.bittrex.get_balance('BTC') - test_basic_response(self, actual, "getbalance") + test_basic_response(self, actual, "get_balance") self.assertTrue(isinstance(actual['result'], dict), "result is not a dict") self.assertEqual(actual['result']['Currency'], "BTC", "requested currency {0:s} does not match returned currency {1:s}" .format("BTC", actual['result']['Currency'])) + @unittest.skip("my testing account is acting funny this should work") + def test_get_depositaddress(self): + actual = self.bittrex.get_deposit_address('BTC') + test_basic_response(self, actual, "get_deposit_address") + + def test_get_order_history_all_markets(self): + actual = self.bittrex.get_order_history() + test_basic_response(self, actual, "get_order_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_order_history_one_market(self): + actual = self.bittrex.get_order_history(market='BTC-LTC') + test_basic_response(self, actual, "get_order_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_withdrawlhistory_all_currencies(self): + actual = self.bittrex.get_withdrawal_history() + test_basic_response(self, actual, "get_withdrawal_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_withdrawlhistory_one_currency(self): + actual = self.bittrex.get_withdrawal_history('BTC') + test_basic_response(self, actual, "get_withdrawal_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_deposithistory_all_currencies(self): + actual = self.bittrex.get_deposit_history() + test_basic_response(self, actual, "get_deposit_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_deposithistory_one_currency(self): + actual = self.bittrex.get_deposit_history('BTC') + test_basic_response(self, actual, "get_deposit_history") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_pending_withdrawls_all_currencies(self): + actual = self.bittrex.get_pending_withdrawls() + test_basic_response(self, actual, "get_pending_withdrawls") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_pending_withdrawls_one_currency(self): + actual = self.bittrex.get_pending_withdrawls('BTC') + test_basic_response(self, actual, "get_pending_withdrawls") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_pending_deposits_all_currencies(self): + actual = self.bittrex.get_pending_deposits() + test_basic_response(self, actual, "get_pending_deposits") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_get_pending_deposits_one_currency(self): + actual = self.bittrex.get_pending_deposits('BTC') + test_basic_response(self, actual, "get_pending_deposits") + self.assertIsInstance(actual['result'], list, "result is not a list") + + def test_generate_deposit_address(self): + actual = self.bittrex.generate_deposit_address(currency='BTC') + test_basic_response(self, actual, "generate_deposit_address") + self.assertIsInstance(actual['result'], list, "result is not a list") + + if __name__ == '__main__': unittest.main() - diff --git a/setup.py b/setup.py index 716dd22..8bdea0f 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ setup(name='python-bittrex', - version='0.1.3', + version='0.2.0', packages=['bittrex'], modules=['bittrex'], description='Python bindings for bittrex API.',