From 556533f160bae66fa89717454c6f577f9b64f7ea Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Wed, 21 Mar 2018 19:02:04 +0200 Subject: [PATCH 01/22] requirements add ccxt, remove bittrex --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 29361ecdb..e11ff8680 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -python-bittrex==0.3.0 +ccxt==1.11.149 SQLAlchemy==1.2.5 python-telegram-bot==10.0.1 arrow==0.12.1 From 14d16d573cb8369df54bc80171b1431cae818cf3 Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Wed, 21 Mar 2018 19:31:15 +0200 Subject: [PATCH 02/22] Remove bittrex related interface code and tests --- freqtrade/exchange/bittrex.py | 211 ----------- freqtrade/exchange/interface.py | 172 --------- .../tests/exchange/test_exchange_bittrex.py | 349 ------------------ 3 files changed, 732 deletions(-) delete mode 100644 freqtrade/exchange/bittrex.py delete mode 100644 freqtrade/exchange/interface.py delete mode 100644 freqtrade/tests/exchange/test_exchange_bittrex.py diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py deleted file mode 100644 index 0cba621af..000000000 --- a/freqtrade/exchange/bittrex.py +++ /dev/null @@ -1,211 +0,0 @@ -import logging -from typing import Dict, List, Optional - -from bittrex.bittrex import API_V1_1, API_V2_0 -from bittrex.bittrex import Bittrex as _Bittrex -from requests.exceptions import ContentDecodingError - -from freqtrade import OperationalException -from freqtrade.exchange.interface import Exchange - -logger = logging.getLogger(__name__) - -_API: _Bittrex = None -_API_V2: _Bittrex = None -_EXCHANGE_CONF: dict = {} - - -class Bittrex(Exchange): - """ - Bittrex API wrapper. - """ - # Base URL and API endpoints - BASE_URL: str = 'https://www.bittrex.com' - PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index' - - def __init__(self, config: dict) -> None: - global _API, _API_V2, _EXCHANGE_CONF - - _EXCHANGE_CONF.update(config) - _API = _Bittrex( - api_key=_EXCHANGE_CONF['key'], - api_secret=_EXCHANGE_CONF['secret'], - calls_per_second=1, - api_version=API_V1_1, - ) - _API_V2 = _Bittrex( - api_key=_EXCHANGE_CONF['key'], - api_secret=_EXCHANGE_CONF['secret'], - calls_per_second=1, - api_version=API_V2_0, - ) - self.cached_ticker = {} - - @staticmethod - def _validate_response(response) -> None: - """ - Validates the given bittrex response - and raises a ContentDecodingError if a non-fatal issue happened. - """ - temp_error_messages = [ - 'NO_API_RESPONSE', - 'MIN_TRADE_REQUIREMENT_NOT_MET', - ] - if response['message'] in temp_error_messages: - raise ContentDecodingError(response['message']) - - @property - def fee(self) -> float: - # 0.25 %: See https://bittrex.com/fees - return 0.0025 - - def buy(self, pair: str, rate: float, amount: float) -> str: - data = _API.buy_limit(pair.replace('_', '-'), amount, rate) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format( - message=data['message'], - pair=pair, - rate=rate, - amount=amount)) - return data['result']['uuid'] - - def sell(self, pair: str, rate: float, amount: float) -> str: - data = _API.sell_limit(pair.replace('_', '-'), amount, rate) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({pair}, {rate}, {amount})'.format( - message=data['message'], - pair=pair, - rate=rate, - amount=amount)) - return data['result']['uuid'] - - def get_balance(self, currency: str) -> float: - data = _API.get_balance(currency) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({currency})'.format( - message=data['message'], - currency=currency)) - return float(data['result']['Balance'] or 0.0) - - def get_balances(self): - data = _API.get_balances() - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message}'.format(message=data['message'])) - return data['result'] - - def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: - if refresh or pair not in self.cached_ticker.keys(): - data = _API.get_ticker(pair.replace('_', '-')) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({pair})'.format( - message=data['message'], - pair=pair)) - keys = ['Bid', 'Ask', 'Last'] - if not data.get('result') or\ - not all(key in data.get('result', {}) for key in keys) or\ - not all(data.get('result', {})[key] is not None for key in keys): - raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format( - pair=pair)) - # Update the pair - self.cached_ticker[pair] = { - 'bid': float(data['result']['Bid']), - 'ask': float(data['result']['Ask']), - 'last': float(data['result']['Last']), - } - return self.cached_ticker[pair] - - def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]: - if tick_interval == 1: - interval = 'oneMin' - elif tick_interval == 5: - interval = 'fiveMin' - elif tick_interval == 30: - interval = 'thirtyMin' - elif tick_interval == 60: - interval = 'hour' - elif tick_interval == 1440: - interval = 'Day' - else: - raise ValueError('Unknown tick_interval: {}'.format(tick_interval)) - - data = _API_V2.get_candles(pair.replace('_', '-'), interval) - - # These sanity check are necessary because bittrex cannot keep their API stable. - if not data.get('result'): - raise ContentDecodingError('Invalid response from Bittrex params=({pair})'.format( - pair=pair)) - - for prop in ['C', 'V', 'O', 'H', 'L', 'T']: - for tick in data['result']: - if prop not in tick.keys(): - raise ContentDecodingError('Required property {} not present ' - 'in response params=({})'.format(prop, pair)) - - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({pair})'.format( - message=data['message'], - pair=pair)) - - return data['result'] - - def get_order(self, order_id: str) -> Dict: - data = _API.get_order(order_id) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({order_id})'.format( - message=data['message'], - order_id=order_id)) - data = data['result'] - return { - 'id': data['OrderUuid'], - 'type': data['Type'], - 'pair': data['Exchange'].replace('-', '_'), - 'opened': data['Opened'], - 'rate': data['PricePerUnit'], - 'amount': data['Quantity'], - 'remaining': data['QuantityRemaining'], - 'closed': data['Closed'], - } - - def cancel_order(self, order_id: str) -> None: - data = _API.cancel(order_id) - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException('{message} params=({order_id})'.format( - message=data['message'], - order_id=order_id)) - - def get_pair_detail_url(self, pair: str) -> str: - return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-')) - - def get_markets(self) -> List[str]: - data = _API.get_markets() - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException(data['message']) - return [m['MarketName'].replace('-', '_') for m in data['result']] - - def get_market_summaries(self) -> List[Dict]: - data = _API.get_market_summaries() - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException(data['message']) - return data['result'] - - def get_wallet_health(self) -> List[Dict]: - data = _API_V2.get_wallet_health() - if not data['success']: - Bittrex._validate_response(data) - raise OperationalException(data['message']) - return [{ - 'Currency': entry['Health']['Currency'], - 'IsActive': entry['Health']['IsActive'], - 'LastChecked': entry['Health']['LastChecked'], - 'Notice': entry['Currency'].get('Notice'), - } for entry in data['result']] diff --git a/freqtrade/exchange/interface.py b/freqtrade/exchange/interface.py deleted file mode 100644 index 6121a98b3..000000000 --- a/freqtrade/exchange/interface.py +++ /dev/null @@ -1,172 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Dict, List, Optional - - -class Exchange(ABC): - @property - def name(self) -> str: - """ - Name of the exchange. - :return: str representation of the class name - """ - return self.__class__.__name__ - - @property - def fee(self) -> float: - """ - Fee for placing an order - :return: percentage in float - """ - - @abstractmethod - def buy(self, pair: str, rate: float, amount: float) -> str: - """ - Places a limit buy order. - :param pair: Pair as str, format: BTC_ETH - :param rate: Rate limit for order - :param amount: The amount to purchase - :return: order_id of the placed buy order - """ - - @abstractmethod - def sell(self, pair: str, rate: float, amount: float) -> str: - """ - Places a limit sell order. - :param pair: Pair as str, format: BTC_ETH - :param rate: Rate limit for order - :param amount: The amount to sell - :return: order_id of the placed sell order - """ - - @abstractmethod - def get_balance(self, currency: str) -> float: - """ - Gets account balance. - :param currency: Currency as str, format: BTC - :return: float - """ - - @abstractmethod - def get_balances(self) -> List[dict]: - """ - Gets account balances across currencies - :return: List of dicts, format: [ - { - 'Currency': str, - 'Balance': float, - 'Available': float, - 'Pending': float, - } - ... - ] - """ - - @abstractmethod - def get_ticker(self, pair: str, refresh: Optional[bool] = True) -> dict: - """ - Gets ticker for given pair. - :param pair: Pair as str, format: BTC_ETC - :param refresh: Shall we query a new value or a cached value is enough - :return: dict, format: { - 'bid': float, - 'ask': float, - 'last': float - } - """ - - @abstractmethod - def get_ticker_history(self, pair: str, tick_interval: int) -> List[Dict]: - """ - Gets ticker history for given pair. - :param pair: Pair as str, format: BTC_ETC - :param tick_interval: ticker interval in minutes - :return: list, format: [ - { - 'O': float, (Open) - 'H': float, (High) - 'L': float, (Low) - 'C': float, (Close) - 'V': float, (Volume) - 'T': datetime, (Time) - 'BV': float, (Base Volume) - }, - ... - ] - """ - - def get_order(self, order_id: str) -> Dict: - """ - Get order details for the given order_id. - :param order_id: ID as str - :return: dict, format: { - 'id': str, - 'type': str, - 'pair': str, - 'opened': str ISO 8601 datetime, - 'closed': str ISO 8601 datetime, - 'rate': float, - 'amount': float, - 'remaining': int - } - """ - - @abstractmethod - def cancel_order(self, order_id: str) -> None: - """ - Cancels order for given order_id. - :param order_id: ID as str - :return: None - """ - - @abstractmethod - def get_pair_detail_url(self, pair: str) -> str: - """ - Returns the market detail url for the given pair. - :param pair: Pair as str, format: BTC_ETC - :return: URL as str - """ - - @abstractmethod - def get_markets(self) -> List[str]: - """ - Returns all available markets. - :return: List of all available pairs - """ - - @abstractmethod - def get_market_summaries(self) -> List[Dict]: - """ - Returns a 24h market summary for all available markets - :return: list, format: [ - { - 'MarketName': str, - 'High': float, - 'Low': float, - 'Volume': float, - 'Last': float, - 'TimeStamp': datetime, - 'BaseVolume': float, - 'Bid': float, - 'Ask': float, - 'OpenBuyOrders': int, - 'OpenSellOrders': int, - 'PrevDay': float, - 'Created': datetime - }, - ... - ] - """ - - @abstractmethod - def get_wallet_health(self) -> List[Dict]: - """ - Returns a list of all wallet health information - :return: list, format: [ - { - 'Currency': str, - 'IsActive': bool, - 'LastChecked': str, - 'Notice': str - }, - ... - """ diff --git a/freqtrade/tests/exchange/test_exchange_bittrex.py b/freqtrade/tests/exchange/test_exchange_bittrex.py deleted file mode 100644 index 2c66215c2..000000000 --- a/freqtrade/tests/exchange/test_exchange_bittrex.py +++ /dev/null @@ -1,349 +0,0 @@ -# pragma pylint: disable=missing-docstring, C0103, protected-access, unused-argument - -from unittest.mock import MagicMock - -import pytest -from requests.exceptions import ContentDecodingError - -import freqtrade.exchange.bittrex as btx -from freqtrade.exchange.bittrex import Bittrex - - -# Eat this flake8 -# +------------------+ -# | bittrex.Bittrex | -# +------------------+ -# | -# (mock Fake_bittrex) -# | -# +-----------------------------+ -# | freqtrade.exchange.Bittrex | -# +-----------------------------+ -# Call into Bittrex will flow up to the -# external package bittrex.Bittrex. -# By inserting a mock, we redirect those -# calls. -# The faked bittrex API is called just 'fb' -# The freqtrade.exchange.Bittrex is a -# wrapper, and is called 'wb' - - -def _stub_config(): - return {'key': '', - 'secret': ''} - - -class FakeBittrex(): - def __init__(self, success=True): - self.success = True # Believe in yourself - self.result = None - self.get_ticker_call_count = 0 - # This is really ugly, doing side-effect during instance creation - # But we're allowed to in testing-code - btx._API = MagicMock() - btx._API.buy_limit = self.fake_buysell_limit - btx._API.sell_limit = self.fake_buysell_limit - btx._API.get_balance = self.fake_get_balance - btx._API.get_balances = self.fake_get_balances - btx._API.get_ticker = self.fake_get_ticker - btx._API.get_order = self.fake_get_order - btx._API.cancel = self.fake_cancel_order - btx._API.get_markets = self.fake_get_markets - btx._API.get_market_summaries = self.fake_get_market_summaries - btx._API_V2 = MagicMock() - btx._API_V2.get_candles = self.fake_get_candles - btx._API_V2.get_wallet_health = self.fake_get_wallet_health - - def fake_buysell_limit(self, pair, amount, limit): - return {'success': self.success, - 'result': {'uuid': '1234'}, - 'message': 'barter'} - - def fake_get_balance(self, cur): - return {'success': self.success, - 'result': {'Balance': 1234}, - 'message': 'unbalanced'} - - def fake_get_balances(self): - return {'success': self.success, - 'result': [{'BTC_ETH': 1234}], - 'message': 'no balances'} - - def fake_get_ticker(self, pair): - self.get_ticker_call_count += 1 - return self.result or {'success': self.success, - 'result': {'Bid': 1, 'Ask': 1, 'Last': 1}, - 'message': 'NO_API_RESPONSE'} - - def fake_get_candles(self, pair, interval): - return self.result or {'success': self.success, - 'result': [{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}], - 'message': 'candles lit'} - - def fake_get_order(self, uuid): - return {'success': self.success, - 'result': {'OrderUuid': 'ABC123', - 'Type': 'Type', - 'Exchange': 'BTC_ETH', - 'Opened': True, - 'PricePerUnit': 1, - 'Quantity': 1, - 'QuantityRemaining': 1, - 'Closed': True}, - 'message': 'lost'} - - def fake_cancel_order(self, uuid): - return self.result or {'success': self.success, - 'message': 'no such order'} - - def fake_get_markets(self): - return self.result or {'success': self.success, - 'message': 'market gone', - 'result': [{'MarketName': '-_'}]} - - def fake_get_market_summaries(self): - return self.result or {'success': self.success, - 'message': 'no summary', - 'result': ['sum']} - - def fake_get_wallet_health(self): - return self.result or {'success': self.success, - 'message': 'bad health', - 'result': [{'Health': {'Currency': 'BTC_ETH', - 'IsActive': True, - 'LastChecked': 0}, - 'Currency': {'Notice': True}}]} - - -# The freqtrade.exchange.bittrex is called wrap_bittrex -# to not confuse naming with bittrex.bittrex -def make_wrap_bittrex(): - conf = _stub_config() - wb = btx.Bittrex(conf) - return wb - - -def test_exchange_bittrex_class(): - conf = _stub_config() - b = Bittrex(conf) - assert isinstance(b, Bittrex) - slots = dir(b) - for name in ['fee', 'buy', 'sell', 'get_balance', 'get_balances', - 'get_ticker', 'get_ticker_history', 'get_order', - 'cancel_order', 'get_pair_detail_url', 'get_markets', - 'get_market_summaries', 'get_wallet_health']: - assert name in slots - # FIX: ensure that the slot is also a method in the class - # getattr(b, name) => bound method Bittrex.buy - # type(getattr(b, name)) => class 'method' - - -def test_exchange_bittrex_fee(): - fee = Bittrex.fee.__get__(Bittrex) - assert fee >= 0 and fee < 0.1 # Fee is 0-10 % - - -def test_exchange_bittrex_buy_good(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - uuid = wb.buy('BTC_ETH', 1, 1) - assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid'] - - fb.success = False - with pytest.raises(btx.OperationalException, match=r'barter.*'): - wb.buy('BAD', 1, 1) - - -def test_exchange_bittrex_sell_good(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - uuid = wb.sell('BTC_ETH', 1, 1) - assert uuid == fb.fake_buysell_limit(1, 2, 3)['result']['uuid'] - - fb.success = False - with pytest.raises(btx.OperationalException, match=r'barter.*'): - uuid = wb.sell('BAD', 1, 1) - - -def test_exchange_bittrex_get_balance(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - bal = wb.get_balance('BTC_ETH') - assert bal == fb.fake_get_balance(1)['result']['Balance'] - - fb.success = False - with pytest.raises(btx.OperationalException, match=r'unbalanced'): - wb.get_balance('BTC_ETH') - - -def test_exchange_bittrex_get_balances(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - bals = wb.get_balances() - assert bals == fb.fake_get_balances()['result'] - - fb.success = False - with pytest.raises(btx.OperationalException, match=r'no balances'): - wb.get_balances() - - -def test_exchange_bittrex_get_ticker(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - - # Poll ticker, which updates the cache - tick = wb.get_ticker('BTC_ETH') - for x in ['bid', 'ask', 'last']: - assert x in tick - # Ensure the side-effect was made (update the ticker cache) - assert 'BTC_ETH' in wb.cached_ticker.keys() - - # taint the cache, so we can recognize the cache wall utilized - wb.cached_ticker['BTC_ETH']['bid'] = 1234 - # Poll again, getting the cached result - fb.get_ticker_call_count = 0 - tick = wb.get_ticker('BTC_ETH', False) - # Ensure the result was from the cache, and that we didn't call exchange - assert wb.cached_ticker['BTC_ETH']['bid'] == 1234 - assert fb.get_ticker_call_count == 0 - - -def test_exchange_bittrex_get_ticker_bad(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - fb.result = {'success': True, 'result': {'Bid': 1, 'Ask': 0}} # incomplete result - - with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'): - wb.get_ticker('BTC_ETH') - fb.result = {'success': False, 'message': 'gone bad'} - with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'): - wb.get_ticker('BTC_ETH') - - fb.result = {'success': True, 'result': {}} # incomplete result - with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'): - wb.get_ticker('BTC_ETH') - fb.result = {'success': False, 'message': 'gone bad'} - with pytest.raises(btx.OperationalException, match=r'.*gone bad.*'): - wb.get_ticker('BTC_ETH') - - fb.result = {'success': True, - 'result': {'Bid': 1, 'Ask': 0, 'Last': None}} # incomplete result - with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex params.*'): - wb.get_ticker('BTC_ETH') - - -def test_exchange_bittrex_get_ticker_history_intervals(): - wb = make_wrap_bittrex() - FakeBittrex() - for tick_interval in [1, 5, 30, 60, 1440]: - assert ([{'C': 0, 'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}] == - wb.get_ticker_history('BTC_ETH', tick_interval)) - - -def test_exchange_bittrex_get_ticker_history(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - assert wb.get_ticker_history('BTC_ETH', 5) - with pytest.raises(ValueError, match=r'.*Unknown tick_interval.*'): - wb.get_ticker_history('BTC_ETH', 2) - - fb.success = False - with pytest.raises(btx.OperationalException, match=r'candles lit.*'): - wb.get_ticker_history('BTC_ETH', 5) - - fb.success = True - with pytest.raises(ContentDecodingError, match=r'.*Invalid response from Bittrex.*'): - fb.result = {'bad': 0} - wb.get_ticker_history('BTC_ETH', 5) - - with pytest.raises(ContentDecodingError, match=r'.*Required property C not present.*'): - fb.result = {'success': True, - 'result': [{'V': 0, 'O': 0, 'H': 0, 'L': 0, 'T': 0}], # close is missing - 'message': 'candles lit'} - wb.get_ticker_history('BTC_ETH', 5) - - -def test_exchange_bittrex_get_order(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - order = wb.get_order('someUUID') - assert order['id'] == 'ABC123' - fb.success = False - with pytest.raises(btx.OperationalException, match=r'lost'): - wb.get_order('someUUID') - - -def test_exchange_bittrex_cancel_order(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - wb.cancel_order('someUUID') - with pytest.raises(btx.OperationalException, match=r'no such order'): - fb.success = False - wb.cancel_order('someUUID') - # Note: this can be a bug in exchange.bittrex._validate_response - with pytest.raises(KeyError): - fb.result = {'success': False} # message is missing! - wb.cancel_order('someUUID') - with pytest.raises(btx.OperationalException, match=r'foo'): - fb.result = {'success': False, 'message': 'foo'} - wb.cancel_order('someUUID') - - -def test_exchange_get_pair_detail_url(): - wb = make_wrap_bittrex() - assert wb.get_pair_detail_url('BTC_ETH') - - -def test_exchange_get_markets(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - x = wb.get_markets() - assert x == ['__'] - with pytest.raises(btx.OperationalException, match=r'market gone'): - fb.success = False - wb.get_markets() - - -def test_exchange_get_market_summaries(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - assert ['sum'] == wb.get_market_summaries() - with pytest.raises(btx.OperationalException, match=r'no summary'): - fb.success = False - wb.get_market_summaries() - - -def test_exchange_get_wallet_health(): - wb = make_wrap_bittrex() - fb = FakeBittrex() - x = wb.get_wallet_health() - assert x[0]['Currency'] == 'BTC_ETH' - with pytest.raises(btx.OperationalException, match=r'bad health'): - fb.success = False - wb.get_wallet_health() - - -def test_validate_response_success(): - response = { - 'message': '', - 'result': [], - } - Bittrex._validate_response(response) - - -def test_validate_response_no_api_response(): - response = { - 'message': 'NO_API_RESPONSE', - 'result': None, - } - with pytest.raises(ContentDecodingError, match=r'.*NO_API_RESPONSE.*'): - Bittrex._validate_response(response) - - -def test_validate_response_min_trade_requirement_not_met(): - response = { - 'message': 'MIN_TRADE_REQUIREMENT_NOT_MET', - 'result': None, - } - with pytest.raises(ContentDecodingError, match=r'.*MIN_TRADE_REQUIREMENT_NOT_MET.*'): - Bittrex._validate_response(response) From 40a0689183423d5eb3158703fb4bc6166253064f Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Wed, 21 Mar 2018 19:40:16 +0200 Subject: [PATCH 03/22] exhcange now uses ccxt in dry_run, update config --- freqtrade/constants.py | 4 +- freqtrade/exchange/__init__.py | 107 ++++++++++++++++++++++++++------- 2 files changed, 86 insertions(+), 25 deletions(-) diff --git a/freqtrade/constants.py b/freqtrade/constants.py index a3f91d774..af48ac4f9 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -91,7 +91,7 @@ class Constants(object): 'type': 'array', 'items': { 'type': 'string', - 'pattern': '^[0-9A-Z]+_[0-9A-Z]+$' + 'pattern': '^[0-9A-Z]+/[0-9A-Z]+$' }, 'uniqueItems': True }, @@ -99,7 +99,7 @@ class Constants(object): 'type': 'array', 'items': { 'type': 'string', - 'pattern': '^[0-9A-Z]+_[0-9A-Z]+$' + 'pattern': '^[0-9A-Z]+/[0-9A-Z]+$' }, 'uniqueItems': True } diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index dc85bfedb..cdc393d35 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -2,32 +2,56 @@ """ Cryptocurrency Exchanges support """ import enum import logging +import ccxt from random import randint from typing import List, Dict, Any, Optional +from cachetools import cached, TTLCache import arrow import requests -from cachetools import cached, TTLCache from freqtrade import OperationalException -from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.interface import Exchange logger = logging.getLogger(__name__) # Current selected exchange -_API: Exchange = None +_API = None _CONF: dict = {} +API_RETRY_COUNT = 4 # Holds all open sell orders for dry_run _DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {} +def retrier(f): + def wrapper(*args, **kwargs): + count = kwargs.pop('count', API_RETRY_COUNT) + try: + return f(*args, **kwargs) + # TODO dont be a gotta-catch-them-all pokemon collector + except Exception as ex: + logger.warn('%s returned exception: "%s"', f, ex) + if count > 0: + count -= 1 + kwargs.update({'count': count}) + logger.warn('retrying %s still for %s times', f, count) + return wrapper(*args, **kwargs) + else: + raise OperationalException('Giving up retrying: %s', f) + return wrapper -class Exchanges(enum.Enum): - """ - Maps supported exchange names to correspondent classes. - """ - BITTREX = Bittrex + +def _get_market_url(exchange): + "get market url for exchange" + # TODO: PR to ccxt + base = exchange.urls.get('www') + market = "" + if 'bittrex' in get_name(): + market = base + '/Market/Index?MarketName={}' + if 'binance' in get_name(): + market = base + '/trade.html?symbol={}' + + return market def init(config: dict) -> None: @@ -49,12 +73,21 @@ def init(config: dict) -> None: # Find matching class for the given exchange name name = exchange_config['name'] + + # TODO add check for a list of supported exchanges + try: - exchange_class = Exchanges[name.upper()].value + # exchange_class = Exchanges[name.upper()].value + _API = getattr(ccxt, name.lower())({ + 'apiKey': exchange_config.get('key'), + 'secret': exchange_config.get('secret'), + }) + logger.info('Using Exchange %s', name.capitalize()) except KeyError: raise OperationalException('Exchange {} is not supported'.format(name)) - _API = exchange_class(exchange_config) + # we need load api markets + _API.load_markets() # Check if all pairs are available validate_pairs(config['exchange']['pair_whitelist']) @@ -67,15 +100,22 @@ def validate_pairs(pairs: List[str]) -> None: :param pairs: list of pairs :return: None """ + + if not _API.markets: + _API.load_markets() + try: - markets = _API.get_markets() + markets = _API.markets except requests.exceptions.RequestException as e: logger.warning('Unable to validate pairs (assuming they are correct). Reason: %s', e) return stake_cur = _CONF['stake_currency'] for pair in pairs: - if not pair.startswith(stake_cur): + # Note: ccxt has BaseCurrency/QuoteCurrency format for pairs + pair = pair.replace('_', '/') + # TODO: add a support for having coins in BTC/USDT format + if not pair.endswith(stake_cur): raise OperationalException( 'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur) ) @@ -124,23 +164,31 @@ def get_balance(currency: str) -> float: if _CONF['dry_run']: return 999.9 - return _API.get_balance(currency) + return _API.fetch_balance()[currency] def get_balances(): if _CONF['dry_run']: return [] - return _API.get_balances() - + return _API.fetch_balance() +# @cached(TTLCache(maxsize=100, ttl=30)) +@retrier def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict: - return _API.get_ticker(pair, refresh) + return _API.fetch_ticker(pair) -@cached(TTLCache(maxsize=100, ttl=30)) +# @cached(TTLCache(maxsize=100, ttl=30)) +@retrier def get_ticker_history(pair: str, tick_interval) -> List[Dict]: - return _API.get_ticker_history(pair, tick_interval) + # TODO: tickers need to be in format 1m,5m + # fetch_ohlcv returns an [[datetime,o,h,l,c,v]] + if not _API.markets: + _API.load_markets() + ohlcv = _API.fetch_ohlcv(pair, str(tick_interval)+'m') + + return ohlcv def cancel_order(order_id: str) -> None: @@ -162,7 +210,9 @@ def get_order(order_id: str) -> Dict: def get_pair_detail_url(pair: str) -> str: - return _API.get_pair_detail_url(pair) + return _get_market_url(_API).format( + _API.markets[pair]['id'] + ) def get_markets() -> List[str]: @@ -170,16 +220,27 @@ def get_markets() -> List[str]: def get_market_summaries() -> List[Dict]: - return _API.get_market_summaries() + return _API.fetch_tickers() def get_name() -> str: - return _API.name + return _API.__class__.__name__ + + +def get_fee_maker() -> float: + return _API.fees['trading']['maker'] + + +def get_fee_taker() -> float: + return _API.fees['trading']['taker'] def get_fee() -> float: - return _API.fee + return _API.fees['trading'] def get_wallet_health() -> List[Dict]: - return _API.get_wallet_health() + if not _API.markets: + _API.load_markets() + + return _API.markets From d20e3f79be2ed7f3a8f4df270b165fe8b6c2e3b1 Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Wed, 21 Mar 2018 19:57:58 +0200 Subject: [PATCH 04/22] analyze to use the ccxt OHLCV format setup: remove bittrex and add requirement to ccxt freqtradebot: update market summaries to ccxt format --- freqtrade/analyze.py | 13 +++++++------ freqtrade/freqtradebot.py | 15 +++++++-------- scripts/plot_dataframe.py | 2 +- setup.py | 2 +- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 8bc552d74..decb73628 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -44,12 +44,13 @@ class Analyze(object): :param ticker: See exchange.get_ticker_history :return: DataFrame """ - columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'} - frame = DataFrame(ticker) \ - .rename(columns=columns) - if 'BV' in frame: - frame.drop('BV', 1, inplace=True) - frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True) + cols = ['date', 'open', 'high', 'low', 'close', 'volume'] + frame = DataFrame(ticker, columns=cols) + + frame['date'] = to_datetime(frame['date'], + unit='ms', + utc=True, + infer_datetime_format=True) frame.sort_values('date', inplace=True) return frame diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index e57f177e9..ef9bd593b 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -208,13 +208,12 @@ class FreqtradeBot(object): :return: List of pairs """ summaries = sorted( - (s for s in exchange.get_market_summaries() if - s['MarketName'].startswith(base_currency)), - key=lambda s: s.get(key) or 0.0, + (v for s, v in exchange.get_market_summaries().items() if v['symbol'].endswith(base_currency)), + key=lambda v: v.get('info').get(key) or 0.0, reverse=True ) - return [s['MarketName'].replace('-', '_') for s in summaries] + return [s['symbol'] for s in summaries] def _refresh_whitelist(self, whitelist: List[str]) -> List[str]: """ @@ -227,15 +226,15 @@ class FreqtradeBot(object): sanitized_whitelist = whitelist health = exchange.get_wallet_health() known_pairs = set() - for status in health: - pair = '{}_{}'.format(self.config['stake_currency'], status['Currency']) + for symbol, status in health.items(): + pair = f"{status['base']}/{self.config['stake_currency']}" # pair is not int the generated dynamic market, or in the blacklist ... ignore it if pair not in whitelist or pair in self.config['exchange'].get('pair_blacklist', []): continue # else the pair is valid known_pairs.add(pair) # Market is not active - if not status['IsActive']: + if not status['active']: sanitized_whitelist.remove(pair) self.logger.info( 'Ignoring %s from whitelist (reason: %s).', @@ -328,7 +327,7 @@ class FreqtradeBot(object): pair=pair, stake_amount=stake_amount, amount=amount, - fee=exchange.get_fee(), + fee=exchange.get_fee_maker(), open_rate=buy_limit, open_date=datetime.utcnow(), exchange=exchange.get_name().upper(), diff --git a/scripts/plot_dataframe.py b/scripts/plot_dataframe.py index 285ba6d97..5c5e17661 100755 --- a/scripts/plot_dataframe.py +++ b/scripts/plot_dataframe.py @@ -55,7 +55,7 @@ def plot_analyzed_dataframe(args: Namespace) -> None: if args.live: logger.info('Downloading pair.') # Init Bittrex to use public API - exchange._API = exchange.Bittrex({'key': '', 'secret': ''}) + exchange.init({'key': '', 'secret': ''}) tickers[pair] = exchange.get_ticker_history(pair, tick_interval) else: tickers = optimize.load_data( diff --git a/setup.py b/setup.py index 4b0635efa..856a47181 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ setup(name='freqtrade', setup_requires=['pytest-runner'], tests_require=['pytest', 'pytest-mock', 'pytest-cov'], install_requires=[ - 'python-bittrex', + 'ccxt', 'SQLAlchemy', 'python-telegram-bot', 'arrow', From eb4ac73b78971fd08dc99a5a8f629749f909b903 Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Thu, 22 Mar 2018 08:29:52 +0200 Subject: [PATCH 05/22] remove last bittrex references so that bot is runnable --- freqtrade/exchange/__init__.py | 1 - freqtrade/optimize/backtesting.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index cdc393d35..1b1d2f7aa 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -11,7 +11,6 @@ import arrow import requests from freqtrade import OperationalException -from freqtrade.exchange.interface import Exchange logger = logging.getLogger(__name__) diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index d8af47326..6202edead 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -11,11 +11,12 @@ from pandas import DataFrame, Series from tabulate import tabulate import freqtrade.optimize as optimize +import freqtrade.exchange as exchange from freqtrade import exchange from freqtrade.analyze import Analyze from freqtrade.arguments import Arguments from freqtrade.configuration import Configuration -from freqtrade.exchange import Bittrex + from freqtrade.logger import Logger from freqtrade.misc import file_dump_json from freqtrade.persistence import Trade @@ -52,7 +53,7 @@ class Backtesting(object): self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe self.populate_buy_trend = self.analyze.populate_buy_trend self.populate_sell_trend = self.analyze.populate_sell_trend - exchange._API = Bittrex({'key': '', 'secret': ''}) + exchange.init({'key': '', 'secret': ''}) @staticmethod def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: From 85af68d8075ea4df19ec6ea4e653da13d81228ef Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sat, 24 Mar 2018 19:45:23 +0100 Subject: [PATCH 06/22] ccxt - make backtesting work --- freqtrade/optimize/__init__.py | 4 ++-- freqtrade/optimize/backtesting.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index a26744691..61694d884 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -35,7 +35,7 @@ def load_tickerdata_file( """ path = make_testdata_path(datadir) file = os.path.join(path, '{pair}-{ticker_interval}.json'.format( - pair=pair, + pair=pair.replace('/', '_'), ticker_interval=ticker_interval, )) gzipfile = file + '.gz' @@ -126,7 +126,7 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> interval ) - filepair = pair.replace("-", "_") + filepair = pair.replace("/", "_") filename = os.path.join(path, '{pair}-{interval}.json'.format( pair=filepair, interval=interval, diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 6202edead..2b583da3c 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -53,7 +53,10 @@ class Backtesting(object): self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe self.populate_buy_trend = self.analyze.populate_buy_trend self.populate_sell_trend = self.analyze.populate_sell_trend - exchange.init({'key': '', 'secret': ''}) + # Reest keys for backtesting + self.config['exchange']['key'] = '' + self.config['exchange']['secret'] = '' + exchange.init(self.config) @staticmethod def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: From ab6e32f6bbbbf681a24d4a9ee44d5e4242999943 Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sat, 24 Mar 2018 19:51:40 +0100 Subject: [PATCH 07/22] have backtest and dry-mode working partially revert d20e3f79be2ed7f3a8f4df270b165fe8b6c2e3b1 - Changing the OHLVC format should not be done at this time --- freqtrade/__init__.py | 8 ++++++++ freqtrade/analyze.py | 12 ++++++------ freqtrade/exchange/__init__.py | 36 ++++++++++++++++++++++++++++------ 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index cd4515a3b..16f8b3b4a 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -14,3 +14,11 @@ class OperationalException(BaseException): Requires manual intervention. This happens when an exchange returns an unexpected error during runtime. """ + + +class NetworkException(BaseException): + """ + Network related error. + This could happen when an exchange is congested, unavailable, or the user + has networking problems. Usually resolves itself after a time. + """ diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index decb73628..f3e44b57b 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -44,13 +44,13 @@ class Analyze(object): :param ticker: See exchange.get_ticker_history :return: DataFrame """ - cols = ['date', 'open', 'high', 'low', 'close', 'volume'] - frame = DataFrame(ticker, columns=cols) + columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'} + frame = DataFrame(ticker) \ + .rename(columns=columns) + if 'BV' in frame: + frame.drop('BV', 1, inplace=True) + frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True) - frame['date'] = to_datetime(frame['date'], - unit='ms', - utc=True, - infer_datetime_format=True) frame.sort_values('date', inplace=True) return frame diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 1b1d2f7aa..127bc7b69 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -6,11 +6,12 @@ import ccxt from random import randint from typing import List, Dict, Any, Optional from cachetools import cached, TTLCache +from datetime import datetime import arrow import requests -from freqtrade import OperationalException +from freqtrade import OperationalException, NetworkException logger = logging.getLogger(__name__) @@ -183,11 +184,34 @@ def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict: def get_ticker_history(pair: str, tick_interval) -> List[Dict]: # TODO: tickers need to be in format 1m,5m # fetch_ohlcv returns an [[datetime,o,h,l,c,v]] - if not _API.markets: - _API.load_markets() - ohlcv = _API.fetch_ohlcv(pair, str(tick_interval)+'m') + if 'fetchOHLCV' not in _API.has or not _API.has['fetchOHLCV']: + raise OperationalException( + 'Exhange {} does not support fetching historical candlestick data.'.format( + _API.name) + ) - return ohlcv + try: + history = _API.fetch_ohlcv(pair, timeframe=str(tick_interval)+"m") + history_json = [] + for candlestick in history: + history_json.append({ + 'T': datetime.fromtimestamp(candlestick[0]/1000.0).strftime('%Y-%m-%dT%H:%M:%S.%f'), + 'O': candlestick[1], + 'H': candlestick[2], + 'L': candlestick[3], + 'C': candlestick[4], + 'V': candlestick[5], + }) + return history_json + except IndexError as e: + logger.warning('Empty ticker history. Msg %s', str(e)) + return [] + except ccxt.NetworkError as e: + raise NetworkException( + 'Could not load ticker history due to networking error. Message: {}'.format(e) + ) + except ccxt.BaseError as e: + raise OperationalException('Could not fetch ticker data. Msg: {}'.format(e)) def cancel_order(order_id: str) -> None: @@ -235,7 +259,7 @@ def get_fee_taker() -> float: def get_fee() -> float: - return _API.fees['trading'] + return get_fee_taker() def get_wallet_health() -> List[Dict]: From 609c1eee5514a4c210cd789d7bd571eeed3ef64a Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sat, 24 Mar 2018 20:02:13 +0100 Subject: [PATCH 08/22] fix persistance tests --- freqtrade/tests/conftest.py | 16 +++++----- freqtrade/tests/test_persistence.py | 48 ++++++++++++++--------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 07dc45a3e..5e290a27c 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -73,11 +73,11 @@ def default_conf(): "key": "key", "secret": "secret", "pair_whitelist": [ - "BTC_ETH", - "BTC_TKN", - "BTC_TRST", - "BTC_SWT", - "BTC_BCC" + "ETH/BTC", + "TKN/BTC", + "TRST/BTC", + "SWT/BTC", + "BCC/BTC" ] }, "telegram": { @@ -175,7 +175,7 @@ def limit_buy_order_old(): return { 'id': 'mocked_limit_buy_old', 'type': 'LIMIT_BUY', - 'pair': 'BTC_ETH', + 'pair': 'ETH/BTC', 'opened': str(arrow.utcnow().shift(minutes=-601).datetime), 'rate': 0.00001099, 'amount': 90.99181073, @@ -188,7 +188,7 @@ def limit_sell_order_old(): return { 'id': 'mocked_limit_sell_old', 'type': 'LIMIT_SELL', - 'pair': 'BTC_ETH', + 'pair': 'ETH/BTC', 'opened': str(arrow.utcnow().shift(minutes=-601).datetime), 'rate': 0.00001099, 'amount': 90.99181073, @@ -201,7 +201,7 @@ def limit_buy_order_old_partial(): return { 'id': 'mocked_limit_buy_old_partial', 'type': 'LIMIT_BUY', - 'pair': 'BTC_ETH', + 'pair': 'ETH/BTC', 'opened': str(arrow.utcnow().shift(minutes=-601).datetime), 'rate': 0.00001099, 'amount': 90.99181073, diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 70199b12a..7d3ae3efc 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -4,7 +4,7 @@ import os import pytest from sqlalchemy import create_engine -from freqtrade.exchange import Exchanges +from freqtrade import exchange from freqtrade.persistence import Trade, init, clean_dry_run_db @@ -122,7 +122,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order): pair='BTC_ETH', stake_amount=0.001, fee=0.0025, - exchange=Exchanges.BITTREX, + exchange='binance', ) assert trade.open_order_id is None assert trade.open_rate is None @@ -146,10 +146,10 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order): def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order): trade = Trade( - pair='BTC_ETH', + pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange=Exchanges.BITTREX, + exchange='binance', ) trade.open_order_id = 'something' @@ -168,10 +168,10 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order): def test_calc_close_trade_price_exception(limit_buy_order): trade = Trade( - pair='BTC_ETH', + pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange=Exchanges.BITTREX, + exchange='binance', ) trade.open_order_id = 'something' @@ -181,10 +181,10 @@ def test_calc_close_trade_price_exception(limit_buy_order): def test_update_open_order(limit_buy_order): trade = Trade( - pair='BTC_ETH', + pair='ETH/BTC', stake_amount=1.00, fee=0.1, - exchange=Exchanges.BITTREX, + exchange='binance', ) assert trade.open_order_id is None @@ -203,10 +203,10 @@ def test_update_open_order(limit_buy_order): def test_update_invalid_order(limit_buy_order): trade = Trade( - pair='BTC_ETH', + pair='ETH/BTC', stake_amount=1.00, fee=0.1, - exchange=Exchanges.BITTREX, + exchange='binance', ) limit_buy_order['type'] = 'invalid' with pytest.raises(ValueError, match=r'Unknown order type'): @@ -215,10 +215,10 @@ def test_update_invalid_order(limit_buy_order): def test_calc_open_trade_price(limit_buy_order): trade = Trade( - pair='BTC_ETH', + pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange=Exchanges.BITTREX, + exchange='binance', ) trade.open_order_id = 'open_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -232,10 +232,10 @@ def test_calc_open_trade_price(limit_buy_order): def test_calc_close_trade_price(limit_buy_order, limit_sell_order): trade = Trade( - pair='BTC_ETH', + pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange=Exchanges.BITTREX, + exchange='binance', ) trade.open_order_id = 'close_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -253,10 +253,10 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order): def test_calc_profit(limit_buy_order, limit_sell_order): trade = Trade( - pair='BTC_ETH', + pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange=Exchanges.BITTREX, + exchange='binance', ) trade.open_order_id = 'profit_percent' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -283,10 +283,10 @@ def test_calc_profit(limit_buy_order, limit_sell_order): def test_calc_profit_percent(limit_buy_order, limit_sell_order): trade = Trade( - pair='BTC_ETH', + pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange=Exchanges.BITTREX, + exchange='binance', ) trade.open_order_id = 'profit_percent' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -310,35 +310,35 @@ def test_clean_dry_run_db(default_conf): # Simulate dry_run entries trade = Trade( - pair='BTC_ETH', + pair='ETH/BTC', stake_amount=0.001, amount=123.0, fee=0.0025, open_rate=0.123, - exchange='BITTREX', + exchange='binance', open_order_id='dry_run_buy_12345' ) Trade.session.add(trade) trade = Trade( - pair='BTC_ETC', + pair='ETC/BTC', stake_amount=0.001, amount=123.0, fee=0.0025, open_rate=0.123, - exchange='BITTREX', + exchange='binance', open_order_id='dry_run_sell_12345' ) Trade.session.add(trade) # Simulate prod entry trade = Trade( - pair='BTC_ETC', + pair='ETC/BTC', stake_amount=0.001, amount=123.0, fee=0.0025, open_rate=0.123, - exchange='BITTREX', + exchange='binance', open_order_id='prod_buy_12345' ) Trade.session.add(trade) From 4dc1d7538e2bdb20cabb9c5ea0f2a65fe1694a56 Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sat, 24 Mar 2018 20:07:04 +0100 Subject: [PATCH 09/22] switch currencies to new format --- config.json.example | 22 +++++++++++----------- config_full.json.example | 22 +++++++++++----------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/config.json.example b/config.json.example index 2c464a925..6a4f20cd6 100644 --- a/config.json.example +++ b/config.json.example @@ -13,19 +13,19 @@ "key": "your_exchange_key", "secret": "your_exchange_secret", "pair_whitelist": [ - "BTC_ETH", - "BTC_LTC", - "BTC_ETC", - "BTC_DASH", - "BTC_ZEC", - "BTC_XLM", - "BTC_NXT", - "BTC_POWR", - "BTC_ADA", - "BTC_XMR" + "ETH/BTC", + "LTC/BTC", + "ETC/BTC", + "DASH/BTC", + "ZEC/BTC", + "XLM/BTC", + "NXT/BTC", + "POWR/BTC", + "ADA/BTC", + "XMR/BTC" ], "pair_blacklist": [ - "BTC_DOGE" + "DOGE/BTC" ] }, "experimental": { diff --git a/config_full.json.example b/config_full.json.example index c74b59660..d0f9d1260 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -21,19 +21,19 @@ "key": "your_exchange_key", "secret": "your_exchange_secret", "pair_whitelist": [ - "BTC_ETH", - "BTC_LTC", - "BTC_ETC", - "BTC_DASH", - "BTC_ZEC", - "BTC_XLM", - "BTC_NXT", - "BTC_POWR", - "BTC_ADA", - "BTC_XMR" + "ETH/BTC", + "LTC/BTC", + "ETC/BTC", + "DASH/BTC", + "ZEC/BTC", + "XLM/BTC", + "NXT/BTC", + "POWR/BTC", + "ADA/BTC", + "XMR/BTC" ], "pair_blacklist": [ - "BTC_DOGE" + "DOGE/BTC" ] }, "experimental": { From a6587b209f217386f2b1ee8a815e71d9010ba5b6 Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sat, 24 Mar 2018 20:11:42 +0100 Subject: [PATCH 10/22] freqtradebot_tests - change currency to new format --- freqtrade/tests/test_freqtradebot.py | 29 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index d58b428da..4c99a4d6a 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -17,7 +17,6 @@ import requests from sqlalchemy import create_engine from freqtrade import DependencyException, OperationalException -from freqtrade.exchange import Exchanges from freqtrade.freqtradebot import FreqtradeBot from freqtrade.persistence import Trade from freqtrade.state import State @@ -263,7 +262,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, mocker) -> None: assert trade.stake_amount == 0.001 assert trade.is_open assert trade.open_date is not None - assert trade.exchange == Exchanges.BITTREX.name + assert trade.exchange == 'BITTREX' # Simulate fulfilled LIMIT_BUY order for trade trade.update(limit_buy_order) @@ -333,8 +332,8 @@ def test_create_trade_no_pairs(default_conf, ticker, mocker) -> None: ) conf = deepcopy(default_conf) - conf['exchange']['pair_whitelist'] = ["BTC_ETH"] - conf['exchange']['pair_blacklist'] = ["BTC_ETH"] + conf['exchange']['pair_whitelist'] = ["ETH/BTC"] + conf['exchange']['pair_blacklist'] = ["ETH/BTC"] freqtrade = FreqtradeBot(conf, create_engine('sqlite://')) freqtrade.create_trade() @@ -358,8 +357,8 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker, mocker) -> ) conf = deepcopy(default_conf) - conf['exchange']['pair_whitelist'] = ["BTC_ETH"] - conf['exchange']['pair_blacklist'] = ["BTC_ETH"] + conf['exchange']['pair_whitelist'] = ["ETH/BTC"] + conf['exchange']['pair_blacklist'] = ["ETH/BTC"] freqtrade = FreqtradeBot(conf, create_engine('sqlite://')) freqtrade.create_trade() @@ -793,7 +792,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, mo freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) trade_buy = Trade( - pair='BTC_ETH', + pair='ETH/BTC', open_rate=0.00001099, exchange='BITTREX', open_order_id='123456789', @@ -832,7 +831,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) trade_sell = Trade( - pair='BTC_ETH', + pair='ETH/BTC', open_rate=0.00001099, exchange='BITTREX', open_order_id='123456789', @@ -871,7 +870,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) trade_buy = Trade( - pair='BTC_ETH', + pair='ETH/BTC', open_rate=0.00001099, exchange='BITTREX', open_order_id='123456789', @@ -918,7 +917,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) - freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://')) trade_buy = Trade( - pair='BTC_ETH', + pair='ETH/BTC', open_rate=0.00001099, exchange='BITTREX', open_order_id='123456789', @@ -931,7 +930,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) - Trade.session.add(trade_buy) regexp = re.compile( - 'Cannot query order for Trade(id=1, pair=BTC_ETH, amount=90.99181073, ' + 'Cannot query order for Trade(id=1, pair=ETH/BTC, amount=90.99181073, ' 'open_rate=0.00001099, open_since=10 hours ago) due to Traceback (most ' 'recent call last):\n.*' ) @@ -1024,7 +1023,7 @@ def test_execute_sell_up(default_conf, ticker, ticker_sell_up, mocker) -> None: assert rpc_mock.call_count == 2 assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] assert 'Amount' in rpc_mock.call_args_list[-1][0][0] assert 'Profit' in rpc_mock.call_args_list[-1][0][0] assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] @@ -1064,7 +1063,7 @@ def test_execute_sell_down(default_conf, ticker, ticker_sell_down, mocker) -> No assert rpc_mock.call_count == 2 assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] assert 'Amount' in rpc_mock.call_args_list[-1][0][0] assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] @@ -1103,7 +1102,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, ticker_sell_up, assert rpc_mock.call_count == 2 assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] assert 'Amount' in rpc_mock.call_args_list[-1][0][0] assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] assert '(profit: 6.11%, 0.00006126)' in rpc_mock.call_args_list[-1][0][0] @@ -1143,7 +1142,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, assert rpc_mock.call_count == 2 assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] From 22ef8603122f9b0eea44c71e390817db5b23c919 Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sat, 24 Mar 2018 20:32:15 +0100 Subject: [PATCH 11/22] Change freqbottest currencies --- freqtrade/tests/test_freqtradebot.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index 4c99a4d6a..02edf3322 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -215,15 +215,15 @@ def test_gen_pair_whitelist(mocker, default_conf, get_market_summaries_data) -> # Test to retrieved BTC sorted on BaseVolume whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC') - assert whitelist == ['BTC_ZCL', 'BTC_ZEC', 'BTC_XZC', 'BTC_XWC'] + assert whitelist == ['ZCL/BTC', 'ZEC/BTC', 'XZC/BTC', 'XWC/BTC'] # Test to retrieved BTC sorted on OpenBuyOrders whitelist = freqtrade._gen_pair_whitelist(base_currency='BTC', key='OpenBuyOrders') - assert whitelist == ['BTC_XWC', 'BTC_ZCL', 'BTC_ZEC', 'BTC_XZC'] + assert whitelist == ['XWC/BTC', 'ZCL/BTC', 'ZEC/BTC', 'XZC/BTC'] # Test with USDT sorted on BaseVolume whitelist = freqtrade._gen_pair_whitelist(base_currency='USDT') - assert whitelist == ['USDT_XRP', 'USDT_XVG', 'USDT_XMR', 'USDT_ZEC'] + assert whitelist == ['XRP/USDT', 'XVG/USDT', 'XMR/USDT', 'ZEC/USDT'] # Test with ETH (our fixture does not have ETH, but Bittrex returns them) whitelist = freqtrade._gen_pair_whitelist(base_currency='ETH') @@ -424,7 +424,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order, assert trade.stake_amount == default_conf['stake_amount'] assert trade.is_open assert trade.open_date is not None - assert trade.exchange == Exchanges.BITTREX.name + assert trade.exchange == "BITTREX" assert trade.open_rate == 0.00001099 assert trade.amount == 90.99181073703367 From 82a21442964995645c5b50f30cd1f7159e0d7480 Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sat, 24 Mar 2018 20:32:42 +0100 Subject: [PATCH 12/22] change format of health fixture and get_market_summaries fixture --- freqtrade/tests/conftest.py | 303 +++++++++++++++++++----------------- 1 file changed, 163 insertions(+), 140 deletions(-) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index 5e290a27c..d3d2b4d9a 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -128,32 +128,31 @@ def ticker_sell_down(): @pytest.fixture def health(): - return MagicMock(return_value=[{ - 'Currency': 'BTC', - 'IsActive': True, - 'LastChecked': '2017-11-13T20:15:00.00', - 'Notice': None - }, { - 'Currency': 'ETH', - 'IsActive': True, - 'LastChecked': '2017-11-13T20:15:00.00', - 'Notice': None - }, { - 'Currency': 'TRST', - 'IsActive': True, - 'LastChecked': '2017-11-13T20:15:00.00', - 'Notice': None - }, { - 'Currency': 'SWT', - 'IsActive': True, - 'LastChecked': '2017-11-13T20:15:00.00', - 'Notice': None - }, { - 'Currency': 'BCC', - 'IsActive': False, - 'LastChecked': '2017-11-13T20:15:00.00', - 'Notice': None - }]) + return MagicMock(return_value={ + "ETH/BTC": { + 'base': 'ETH', + 'active': True, + 'LastChecked': '2017-11-13T20:15:00.00', + 'Notice': None + }, + "TRST/BTC": { + 'base': 'TRST', + 'active': True, + 'LastChecked': '2017-11-13T20:15:00.00', + 'Notice': None + }, + "SWT/BTC": { + 'base': 'SWT', + 'active': True, + 'LastChecked': '2017-11-13T20:15:00.00', + 'Notice': None + }, + "BCC/BTC": { + 'base': 'BCC', + 'active': False, + 'LastChecked': '2017-11-13T20:15:00.00', + 'Notice': None + }}) @pytest.fixture @@ -307,125 +306,149 @@ def get_market_summaries_data(): 8 entries. 4 BTC, 4 USTD :return: JSON market summaries """ - return [ - { - 'Ask': 1.316e-05, - 'BaseVolume': 5.72599471, - 'Bid': 1.3e-05, - 'Created': '2014-04-14T00:00:00', - 'High': 1.414e-05, - 'Last': 1.298e-05, - 'Low': 1.282e-05, - 'MarketName': 'BTC-XWC', - 'OpenBuyOrders': 2000, - 'OpenSellOrders': 1484, - 'PrevDay': 1.376e-05, - 'TimeStamp': '2018-02-05T01:32:40.493', - 'Volume': 424041.21418375 + return { + 'XWC/BTC': { + 'symbol': 'XWC/BTC', + 'info': { + 'Ask': 1.316e-05, + 'BaseVolume': 5.72599471, + 'Bid': 1.3e-05, + 'Created': '2014-04-14T00:00:00', + 'High': 1.414e-05, + 'Last': 1.298e-05, + 'Low': 1.282e-05, + 'MarketName': 'BTC-XWC', + 'OpenBuyOrders': 2000, + 'OpenSellOrders': 1484, + 'PrevDay': 1.376e-05, + 'TimeStamp': '2018-02-05T01:32:40.493', + 'Volume': 424041.21418375 + } }, - { - 'Ask': 0.00627051, - 'BaseVolume': 93.23302388, - 'Bid': 0.00618192, - 'Created': '2016-10-20T04:48:30.387', - 'High': 0.00669897, - 'Last': 0.00618192, - 'Low': 0.006, - 'MarketName': 'BTC-XZC', - 'OpenBuyOrders': 343, - 'OpenSellOrders': 2037, - 'PrevDay': 0.00668229, - 'TimeStamp': '2018-02-05T01:32:43.383', - 'Volume': 14863.60730702 + 'XZC/BTC': { + 'symbol': 'XZC/BTC', + 'info': { + 'Ask': 0.00627051, + 'BaseVolume': 93.23302388, + 'Bid': 0.00618192, + 'Created': '2016-10-20T04:48:30.387', + 'High': 0.00669897, + 'Last': 0.00618192, + 'Low': 0.006, + 'MarketName': 'BTC-XZC', + 'OpenBuyOrders': 343, + 'OpenSellOrders': 2037, + 'PrevDay': 0.00668229, + 'TimeStamp': '2018-02-05T01:32:43.383', + 'Volume': 14863.60730702 + } }, - { - 'Ask': 0.01137247, - 'BaseVolume': 383.55922657, - 'Bid': 0.01136006, - 'Created': '2016-11-15T20:29:59.73', - 'High': 0.012, - 'Last': 0.01137247, - 'Low': 0.01119883, - 'MarketName': 'BTC-ZCL', - 'OpenBuyOrders': 1332, - 'OpenSellOrders': 5317, - 'PrevDay': 0.01179603, - 'TimeStamp': '2018-02-05T01:32:42.773', - 'Volume': 33308.07358285 + 'ZCL/BTC': { + 'symbol': 'ZCL/BTC', + 'info': { + 'Ask': 0.01137247, + 'BaseVolume': 383.55922657, + 'Bid': 0.01136006, + 'Created': '2016-11-15T20:29:59.73', + 'High': 0.012, + 'Last': 0.01137247, + 'Low': 0.01119883, + 'MarketName': 'BTC-ZCL', + 'OpenBuyOrders': 1332, + 'OpenSellOrders': 5317, + 'PrevDay': 0.01179603, + 'TimeStamp': '2018-02-05T01:32:42.773', + 'Volume': 33308.07358285 + } }, - { - 'Ask': 0.04155821, - 'BaseVolume': 274.75369074, - 'Bid': 0.04130002, - 'Created': '2016-10-28T17:13:10.833', - 'High': 0.04354429, - 'Last': 0.041585, - 'Low': 0.0413, - 'MarketName': 'BTC-ZEC', - 'OpenBuyOrders': 863, - 'OpenSellOrders': 5579, - 'PrevDay': 0.0429, - 'TimeStamp': '2018-02-05T01:32:43.21', - 'Volume': 6479.84033259 + 'ZEC/BTC': { + 'symbol': 'ZEC/BTC', + 'info': { + 'Ask': 0.04155821, + 'BaseVolume': 274.75369074, + 'Bid': 0.04130002, + 'Created': '2016-10-28T17:13:10.833', + 'High': 0.04354429, + 'Last': 0.041585, + 'Low': 0.0413, + 'MarketName': 'BTC-ZEC', + 'OpenBuyOrders': 863, + 'OpenSellOrders': 5579, + 'PrevDay': 0.0429, + 'TimeStamp': '2018-02-05T01:32:43.21', + 'Volume': 6479.84033259 + } }, - { - 'Ask': 210.99999999, - 'BaseVolume': 615132.70989532, - 'Bid': 210.05503736, - 'Created': '2017-07-21T01:08:49.397', - 'High': 257.396, - 'Last': 211.0, - 'Low': 209.05333589, - 'MarketName': 'USDT-XMR', - 'OpenBuyOrders': 180, - 'OpenSellOrders': 1203, - 'PrevDay': 247.93528899, - 'TimeStamp': '2018-02-05T01:32:43.117', - 'Volume': 2688.17410793 + 'XMR/USDT': { + 'symbol': 'XMR/USDT', + 'info': { + 'Ask': 210.99999999, + 'BaseVolume': 615132.70989532, + 'Bid': 210.05503736, + 'Created': '2017-07-21T01:08:49.397', + 'High': 257.396, + 'Last': 211.0, + 'Low': 209.05333589, + 'MarketName': 'USDT-XMR', + 'OpenBuyOrders': 180, + 'OpenSellOrders': 1203, + 'PrevDay': 247.93528899, + 'TimeStamp': '2018-02-05T01:32:43.117', + 'Volume': 2688.17410793 + } }, - { - 'Ask': 0.79589979, - 'BaseVolume': 9349557.01853031, - 'Bid': 0.789226, - 'Created': '2017-07-14T17:10:10.737', - 'High': 0.977, - 'Last': 0.79589979, - 'Low': 0.781, - 'MarketName': 'USDT-XRP', - 'OpenBuyOrders': 1075, - 'OpenSellOrders': 6508, - 'PrevDay': 0.93300218, - 'TimeStamp': '2018-02-05T01:32:42.383', - 'Volume': 10801663.00788851 + 'XRP/USDT': { + 'symbol': 'XRP/USDT', + 'info': { + 'Ask': 0.79589979, + 'BaseVolume': 9349557.01853031, + 'Bid': 0.789226, + 'Created': '2017-07-14T17:10:10.737', + 'High': 0.977, + 'Last': 0.79589979, + 'Low': 0.781, + 'MarketName': 'USDT-XRP', + 'OpenBuyOrders': 1075, + 'OpenSellOrders': 6508, + 'PrevDay': 0.93300218, + 'TimeStamp': '2018-02-05T01:32:42.383', + 'Volume': 10801663.00788851 + } }, - { - 'Ask': 0.05154982, - 'BaseVolume': 2311087.71232136, - 'Bid': 0.05040107, - 'Created': '2017-12-29T19:29:18.357', - 'High': 0.06668561, - 'Last': 0.0508, - 'Low': 0.05006731, - 'MarketName': 'USDT-XVG', - 'OpenBuyOrders': 655, - 'OpenSellOrders': 5544, - 'PrevDay': 0.0627, - 'TimeStamp': '2018-02-05T01:32:41.507', - 'Volume': 40031424.2152716 + 'XVG/USDT': { + 'symbol': 'XVG/USDT', + 'info': { + 'Ask': 0.05154982, + 'BaseVolume': 2311087.71232136, + 'Bid': 0.05040107, + 'Created': '2017-12-29T19:29:18.357', + 'High': 0.06668561, + 'Last': 0.0508, + 'Low': 0.05006731, + 'MarketName': 'USDT-XVG', + 'OpenBuyOrders': 655, + 'OpenSellOrders': 5544, + 'PrevDay': 0.0627, + 'TimeStamp': '2018-02-05T01:32:41.507', + 'Volume': 40031424.2152716 + } }, - { - 'Ask': 332.65500022, - 'BaseVolume': 562911.87455665, - 'Bid': 330.00000001, - 'Created': '2017-07-14T17:10:10.673', - 'High': 401.59999999, - 'Last': 332.65500019, - 'Low': 330.0, - 'MarketName': 'USDT-ZEC', - 'OpenBuyOrders': 161, - 'OpenSellOrders': 1731, - 'PrevDay': 391.42, - 'TimeStamp': '2018-02-05T01:32:42.947', - 'Volume': 1571.09647946 + 'ZEC/USDT': { + 'symbol': 'ZEC/USDT', + 'info': { + 'Ask': 332.65500022, + 'BaseVolume': 562911.87455665, + 'Bid': 330.00000001, + 'Created': '2017-07-14T17:10:10.673', + 'High': 401.59999999, + 'Last': 332.65500019, + 'Low': 330.0, + 'MarketName': 'USDT-ZEC', + 'OpenBuyOrders': 161, + 'OpenSellOrders': 1731, + 'PrevDay': 391.42, + 'TimeStamp': '2018-02-05T01:32:42.947', + 'Volume': 1571.09647946 + } } - ] + } From 32222ae6ef6fa2f7d6d0d5bf39a0e29358991889 Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sat, 24 Mar 2018 20:42:51 +0100 Subject: [PATCH 13/22] Fix tests in acl_pair --- freqtrade/tests/test_acl_pair.py | 130 +++++++++++++++++-------------- 1 file changed, 70 insertions(+), 60 deletions(-) diff --git a/freqtrade/tests/test_acl_pair.py b/freqtrade/tests/test_acl_pair.py index b5f52774d..174f1fde9 100644 --- a/freqtrade/tests/test_acl_pair.py +++ b/freqtrade/tests/test_acl_pair.py @@ -12,77 +12,87 @@ def whitelist_conf(): config['stake_currency'] = 'BTC' config['exchange']['pair_whitelist'] = [ - 'BTC_ETH', - 'BTC_TKN', - 'BTC_TRST', - 'BTC_SWT', - 'BTC_BCC' + 'ETH/BTC', + 'TKN/BTC', + 'TRST/BTC', + 'SWT/BTC', + 'BCC/BTC' ] config['exchange']['pair_blacklist'] = [ - 'BTC_BLK' + 'BLK/BTC' ] return config def get_market_summaries(): - return [{ - 'MarketName': 'BTC-TKN', - 'High': 0.00000919, - 'Low': 0.00000820, - 'Volume': 74339.61396015, - 'Last': 0.00000820, - 'BaseVolume': 1664, - 'TimeStamp': '2014-07-09T07:19:30.15', - 'Bid': 0.00000820, - 'Ask': 0.00000831, - 'OpenBuyOrders': 15, - 'OpenSellOrders': 15, - 'PrevDay': 0.00000821, - 'Created': '2014-03-20T06:00:00', - 'DisplayMarketName': '' - }, { - 'MarketName': 'BTC-ETH', - 'High': 0.00000072, - 'Low': 0.00000001, - 'Volume': 166340678.42280999, - 'Last': 0.00000005, - 'BaseVolume': 42, - 'TimeStamp': '2014-07-09T07:21:40.51', - 'Bid': 0.00000004, - 'Ask': 0.00000005, - 'OpenBuyOrders': 18, - 'OpenSellOrders': 18, - 'PrevDay': 0.00000002, - 'Created': '2014-05-30T07:57:49.637', - 'DisplayMarketName': '' - }, { - 'MarketName': 'BTC-BLK', - 'High': 0.00000072, - 'Low': 0.00000001, - 'Volume': 166340678.42280999, - 'Last': 0.00000005, - 'BaseVolume': 3, - 'TimeStamp': '2014-07-09T07:21:40.51', - 'Bid': 0.00000004, - 'Ask': 0.00000005, - 'OpenBuyOrders': 18, - 'OpenSellOrders': 18, - 'PrevDay': 0.00000002, - 'Created': '2014-05-30T07:57:49.637', - 'DisplayMarketName': '' - }] + return { + 'TKN/BTC': { + 'symbol': 'TKN/BTC', + 'info': { + 'High': 0.00000919, + 'Low': 0.00000820, + 'Volume': 74339.61396015, + 'Last': 0.00000820, + 'BaseVolume': 1664, + 'TimeStamp': '2014-07-09T07:19:30.15', + 'Bid': 0.00000820, + 'Ask': 0.00000831, + 'OpenBuyOrders': 15, + 'OpenSellOrders': 15, + 'PrevDay': 0.00000821, + 'Created': '2014-03-20T06:00:00', + 'DisplayMarketName': '' + } + }, + 'ETH/BTC': { + 'symbol': 'ETH/BTC', + 'info': { + 'High': 0.00000072, + 'Low': 0.00000001, + 'Volume': 166340678.42280999, + 'Last': 0.00000005, + 'BaseVolume': 42, + 'TimeStamp': '2014-07-09T07:21:40.51', + 'Bid': 0.00000004, + 'Ask': 0.00000005, + 'OpenBuyOrders': 18, + 'OpenSellOrders': 18, + 'PrevDay': 0.00000002, + 'Created': '2014-05-30T07:57:49.637', + 'DisplayMarketName': '' + } + }, + 'BLK/BTC': { + 'symbol': 'BLK/BTC', + 'info': { + 'High': 0.00000072, + 'Low': 0.00000001, + 'Volume': 166340678.42280999, + 'Last': 0.00000005, + 'BaseVolume': 3, + 'TimeStamp': '2014-07-09T07:21:40.51', + 'Bid': 0.00000004, + 'Ask': 0.00000005, + 'OpenBuyOrders': 18, + 'OpenSellOrders': 18, + 'PrevDay': 0.00000002, + 'Created': '2014-05-30T07:57:49.637', + 'DisplayMarketName': '' + }} + } def get_health(): - return [{'Currency': 'ETH', 'IsActive': True}, - {'Currency': 'TKN', 'IsActive': True}, - {'Currency': 'BLK', 'IsActive': True}] + return { + 'ETH/BTC': {'base': 'ETH', 'active': True}, + 'TKN/BTC': {'base': 'TKN', 'active': True}, + 'BLK/BTC': {'base': 'BLK', 'active': True}} def get_health_empty(): - return [] + return {} def test_refresh_market_pair_not_in_whitelist(mocker): @@ -92,10 +102,10 @@ def test_refresh_market_pair_not_in_whitelist(mocker): mocker.patch('freqtrade.freqtradebot.exchange.get_wallet_health', get_health) refreshedwhitelist = freqtradebot._refresh_whitelist( - conf['exchange']['pair_whitelist'] + ['BTC_XXX'] + conf['exchange']['pair_whitelist'] + ['XXX/BTC'] ) # List ordered by BaseVolume - whitelist = ['BTC_ETH', 'BTC_TKN'] + whitelist = ['ETH/BTC', 'TKN/BTC'] # Ensure all except those in whitelist are removed assert whitelist == refreshedwhitelist @@ -108,7 +118,7 @@ def test_refresh_whitelist(mocker): refreshedwhitelist = freqtradebot._refresh_whitelist(conf['exchange']['pair_whitelist']) # List ordered by BaseVolume - whitelist = ['BTC_ETH', 'BTC_TKN'] + whitelist = ['ETH/BTC', 'TKN/BTC'] # Ensure all except those in whitelist are removed assert whitelist == refreshedwhitelist @@ -123,7 +133,7 @@ def test_refresh_whitelist_dynamic(mocker): ) # argument: use the whitelist dynamically by exchange-volume - whitelist = ['BTC_TKN', 'BTC_ETH'] + whitelist = ['TKN/BTC', 'ETH/BTC'] refreshedwhitelist = freqtradebot._refresh_whitelist( freqtradebot._gen_pair_whitelist(conf['stake_currency']) From 0a068db28548f7a9abc39516bba272622cd8839a Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sat, 24 Mar 2018 20:59:09 +0100 Subject: [PATCH 14/22] Switch rpc_test to new currency style --- freqtrade/tests/rpc/test_rpc.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc.py b/freqtrade/tests/rpc/test_rpc.py index 50943b1bc..191916488 100644 --- a/freqtrade/tests/rpc/test_rpc.py +++ b/freqtrade/tests/rpc/test_rpc.py @@ -59,7 +59,7 @@ def test_rpc_trade_status(default_conf, ticker, mocker) -> None: result_message = [ '*Trade ID:* `1`\n' '*Current Pair:* ' - '[BTC_ETH](https://www.bittrex.com/Market/Index?MarketName=BTC-ETH)\n' + '[ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' '*Open Since:* `just now`\n' '*Amount:* `90.99181074`\n' '*Open Rate:* `0.00001099`\n' @@ -70,7 +70,7 @@ def test_rpc_trade_status(default_conf, ticker, mocker) -> None: '*Open Order:* `(LIMIT_BUY rem=0.00000000)`' ] assert result == result_message - assert trade.find('[BTC_ETH]') >= 0 + assert trade.find('[ETH/BTC]') >= 0 def test_rpc_status_table(default_conf, ticker, mocker) -> None: @@ -102,7 +102,7 @@ def test_rpc_status_table(default_conf, ticker, mocker) -> None: freqtradebot.create_trade() (error, result) = rpc.rpc_status_table() assert 'just now' in result['Since'].all() - assert 'BTC_ETH' in result['Pair'].all() + assert 'ETH/BTC' in result['Pair'].all() assert '-0.59%' in result['Profit'].all() @@ -214,7 +214,7 @@ def test_rpc_trade_statistics( assert stats['first_trade_date'] == 'just now' assert stats['latest_trade_date'] == 'just now' assert stats['avg_duration'] == '0:00:00' - assert stats['best_pair'] == 'BTC_ETH' + assert stats['best_pair'] == 'ETH/BTC' assert prec_satoshi(stats['best_rate'], 6.2) @@ -274,7 +274,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, ticker_sell_u assert stats['first_trade_date'] == 'just now' assert stats['latest_trade_date'] == 'just now' assert stats['avg_duration'] == '0:00:00' - assert stats['best_pair'] == 'BTC_ETH' + assert stats['best_pair'] == 'ETH/BTC' assert prec_satoshi(stats['best_rate'], 6.2) @@ -509,7 +509,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, (error, res) = rpc.rpc_performance() assert not error assert len(res) == 1 - assert res[0]['pair'] == 'BTC_ETH' + assert res[0]['pair'] == 'ETH/BTC' assert res[0]['count'] == 1 assert prec_satoshi(res[0]['profit'], 6.2) From ae803474f97e6eff20379dc5ac50ad33b0874ce4 Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sat, 24 Mar 2018 20:59:25 +0100 Subject: [PATCH 15/22] switch rpc_telgram to new style and make it pass --- freqtrade/tests/rpc/test_rpc_telegram.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/freqtrade/tests/rpc/test_rpc_telegram.py b/freqtrade/tests/rpc/test_rpc_telegram.py index 4796b077e..2df43f0fc 100644 --- a/freqtrade/tests/rpc/test_rpc_telegram.py +++ b/freqtrade/tests/rpc/test_rpc_telegram.py @@ -248,7 +248,8 @@ def test_status(default_conf, update, mocker, ticker) -> None: mocker.patch.multiple( 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), - get_ticker=ticker + get_ticker=ticker, + get_pair_detail_url=MagicMock() ) msg_mock = MagicMock() status_table = MagicMock() @@ -319,7 +320,7 @@ def test_status_handle(default_conf, update, ticker, mocker) -> None: telegram._status(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 - assert '[BTC_ETH]' in msg_mock.call_args_list[0][0][0] + assert '[ETH/BTC]' in msg_mock.call_args_list[0][0][0] def test_status_table_handle(default_conf, update, ticker, mocker) -> None: @@ -369,7 +370,7 @@ def test_status_table_handle(default_conf, update, ticker, mocker) -> None: fields = re.sub('[ ]+', ' ', line[2].strip()).split(' ') assert int(fields[0]) == 1 - assert fields[1] == 'BTC_ETH' + assert fields[1] == 'ETH/BTC' assert msg_mock.call_count == 1 @@ -387,7 +388,8 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, mocker.patch.multiple( 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), - get_ticker=ticker + get_ticker=ticker, + get_pair_detail_url=MagicMock() ) msg_mock = MagicMock() mocker.patch.multiple( @@ -541,7 +543,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, assert '∙ `0.00006217 BTC (6.20%)`' in msg_mock.call_args_list[-1][0][0] assert '∙ `0.933 USD`' in msg_mock.call_args_list[-1][0][0] - assert '*Best Performing:* `BTC_ETH: 6.20%`' in msg_mock.call_args_list[-1][0][0] + assert '*Best Performing:* `ETH/BTC: 6.20%`' in msg_mock.call_args_list[-1][0][0] def test_telegram_balance_handle(default_conf, update, mocker) -> None: @@ -779,7 +781,7 @@ def test_forcesell_handle(default_conf, update, ticker, ticker_sell_up, mocker) assert rpc_mock.call_count == 2 assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] assert 'Amount' in rpc_mock.call_args_list[-1][0][0] assert '0.00001172' in rpc_mock.call_args_list[-1][0][0] assert 'profit: 6.11%, 0.00006126' in rpc_mock.call_args_list[-1][0][0] @@ -822,7 +824,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, ticker_sell_down, m assert rpc_mock.call_count == 2 assert 'Selling' in rpc_mock.call_args_list[-1][0][0] - assert '[BTC_ETH]' in rpc_mock.call_args_list[-1][0][0] + assert '[ETH/BTC]' in rpc_mock.call_args_list[-1][0][0] assert 'Amount' in rpc_mock.call_args_list[-1][0][0] assert '0.00001044' in rpc_mock.call_args_list[-1][0][0] assert 'loss: -5.48%, -0.00005492' in rpc_mock.call_args_list[-1][0][0] @@ -838,6 +840,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, mocker) -> None: mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) + mocker.patch('freqtrade.exchange.get_pair_detail_url', MagicMock()) mocker.patch.multiple( 'freqtrade.freqtradebot.exchange', validate_pairs=MagicMock(), @@ -942,7 +945,7 @@ def test_performance_handle(default_conf, update, ticker, limit_buy_order, telegram._performance(bot=MagicMock(), update=update) assert msg_mock.call_count == 1 assert 'Performance' in msg_mock.call_args_list[0][0][0] - assert 'BTC_ETH\t6.20% (1)' in msg_mock.call_args_list[0][0][0] + assert 'ETH/BTC\t6.20% (1)' in msg_mock.call_args_list[0][0][0] def test_performance_handle_invalid(default_conf, update, mocker) -> None: From b07ee26e0869c18d471f8d833cdcc36423cff75d Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sun, 25 Mar 2018 12:57:59 +0200 Subject: [PATCH 16/22] Revert testing exchange to bittrex --- freqtrade/tests/test_persistence.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 7d3ae3efc..c09774a37 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -122,7 +122,7 @@ def test_update_with_bittrex(limit_buy_order, limit_sell_order): pair='BTC_ETH', stake_amount=0.001, fee=0.0025, - exchange='binance', + exchange='bittrex', ) assert trade.open_order_id is None assert trade.open_rate is None @@ -149,7 +149,7 @@ def test_calc_open_close_trade_price(limit_buy_order, limit_sell_order): pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange='binance', + exchange='bittrex', ) trade.open_order_id = 'something' @@ -171,7 +171,7 @@ def test_calc_close_trade_price_exception(limit_buy_order): pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange='binance', + exchange='bittrex', ) trade.open_order_id = 'something' @@ -184,7 +184,7 @@ def test_update_open_order(limit_buy_order): pair='ETH/BTC', stake_amount=1.00, fee=0.1, - exchange='binance', + exchange='bittrex', ) assert trade.open_order_id is None @@ -206,7 +206,7 @@ def test_update_invalid_order(limit_buy_order): pair='ETH/BTC', stake_amount=1.00, fee=0.1, - exchange='binance', + exchange='bittrex', ) limit_buy_order['type'] = 'invalid' with pytest.raises(ValueError, match=r'Unknown order type'): @@ -218,7 +218,7 @@ def test_calc_open_trade_price(limit_buy_order): pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange='binance', + exchange='bittrex', ) trade.open_order_id = 'open_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -235,7 +235,7 @@ def test_calc_close_trade_price(limit_buy_order, limit_sell_order): pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange='binance', + exchange='bittrex', ) trade.open_order_id = 'close_trade' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -256,7 +256,7 @@ def test_calc_profit(limit_buy_order, limit_sell_order): pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange='binance', + exchange='bittrex', ) trade.open_order_id = 'profit_percent' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -286,7 +286,7 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order): pair='ETH/BTC', stake_amount=0.001, fee=0.0025, - exchange='binance', + exchange='bittrex', ) trade.open_order_id = 'profit_percent' trade.update(limit_buy_order) # Buy @ 0.00001099 @@ -315,7 +315,7 @@ def test_clean_dry_run_db(default_conf): amount=123.0, fee=0.0025, open_rate=0.123, - exchange='binance', + exchange='bittrex', open_order_id='dry_run_buy_12345' ) Trade.session.add(trade) @@ -326,7 +326,7 @@ def test_clean_dry_run_db(default_conf): amount=123.0, fee=0.0025, open_rate=0.123, - exchange='binance', + exchange='bittrex', open_order_id='dry_run_sell_12345' ) Trade.session.add(trade) @@ -338,7 +338,7 @@ def test_clean_dry_run_db(default_conf): amount=123.0, fee=0.0025, open_rate=0.123, - exchange='binance', + exchange='bittrex', open_order_id='prod_buy_12345' ) Trade.session.add(trade) From dbb0a6261f6e8dfa748a0883945896b72ef53657 Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sun, 25 Mar 2018 13:01:33 +0200 Subject: [PATCH 17/22] don't raise exceptions from get_ticker_history --- freqtrade/exchange/__init__.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 127bc7b69..2a9222141 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -185,10 +185,8 @@ def get_ticker_history(pair: str, tick_interval) -> List[Dict]: # TODO: tickers need to be in format 1m,5m # fetch_ohlcv returns an [[datetime,o,h,l,c,v]] if 'fetchOHLCV' not in _API.has or not _API.has['fetchOHLCV']: - raise OperationalException( - 'Exhange {} does not support fetching historical candlestick data.'.format( - _API.name) - ) + logger.warning('Exhange %s does not support fetching historical candlestick data.', _API.name) + return [] try: history = _API.fetch_ohlcv(pair, timeframe=str(tick_interval)+"m") @@ -205,13 +203,11 @@ def get_ticker_history(pair: str, tick_interval) -> List[Dict]: return history_json except IndexError as e: logger.warning('Empty ticker history. Msg %s', str(e)) - return [] except ccxt.NetworkError as e: - raise NetworkException( - 'Could not load ticker history due to networking error. Message: {}'.format(e) - ) + logger.warning('Could not load ticker history due to networking error. Message: %s', str(e)) except ccxt.BaseError as e: - raise OperationalException('Could not fetch ticker data. Msg: {}'.format(e)) + logger.warning('Could not fetch ticker data. Msg: %s', str(e)) + return [] def cancel_order(order_id: str) -> None: From 016232a8e97cc13e3cf292096d57d95609397c5e Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sun, 25 Mar 2018 13:32:46 +0200 Subject: [PATCH 18/22] Revert OHLVC dataformat to ccxt format * Also fixes backtesting - but data must be refreshed for now as no conversation is happening yet --- freqtrade/analyze.py | 13 +++++++------ freqtrade/exchange/__init__.py | 20 ++++++-------------- freqtrade/optimize/__init__.py | 15 ++++++++++----- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index f3e44b57b..e6e8023f0 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -44,12 +44,13 @@ class Analyze(object): :param ticker: See exchange.get_ticker_history :return: DataFrame """ - columns = {'C': 'close', 'V': 'volume', 'O': 'open', 'H': 'high', 'L': 'low', 'T': 'date'} - frame = DataFrame(ticker) \ - .rename(columns=columns) - if 'BV' in frame: - frame.drop('BV', 1, inplace=True) - frame['date'] = to_datetime(frame['date'], utc=True, infer_datetime_format=True) + cols = ['date', 'open', 'high', 'low', 'close', 'volume'] + frame = DataFrame(ticker, columns=cols) + + frame['date'] = to_datetime(frame['date'], + unit='ms', + utc=True, + infer_datetime_format=True) frame.sort_values('date', inplace=True) return frame diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 2a9222141..e2e22a358 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -181,26 +181,18 @@ def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict: # @cached(TTLCache(maxsize=100, ttl=30)) @retrier -def get_ticker_history(pair: str, tick_interval) -> List[Dict]: +def get_ticker_history(pair: str, tick_interval) -> List[List]: # TODO: tickers need to be in format 1m,5m # fetch_ohlcv returns an [[datetime,o,h,l,c,v]] if 'fetchOHLCV' not in _API.has or not _API.has['fetchOHLCV']: - logger.warning('Exhange %s does not support fetching historical candlestick data.', _API.name) + logger.warning('Exhange %s does not support fetching historical candlestick data.', + _API.name) return [] try: - history = _API.fetch_ohlcv(pair, timeframe=str(tick_interval)+"m") - history_json = [] - for candlestick in history: - history_json.append({ - 'T': datetime.fromtimestamp(candlestick[0]/1000.0).strftime('%Y-%m-%dT%H:%M:%S.%f'), - 'O': candlestick[1], - 'H': candlestick[2], - 'L': candlestick[3], - 'C': candlestick[4], - 'V': candlestick[5], - }) - return history_json + ohlcv = _API.fetch_ohlcv(pair, timeframe=str(tick_interval)+"m") + + return ohlcv except IndexError as e: logger.warning('Empty ticker history. Msg %s', str(e)) except ccxt.NetworkError as e: diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index 61694d884..be2300de7 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -4,6 +4,7 @@ import gzip import json import os from typing import Optional, List, Dict, Tuple +from datetime import datetime from freqtrade import misc from freqtrade.exchange import get_ticker_history @@ -135,8 +136,8 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> if os.path.isfile(filename): with open(filename, "rt") as file: data = json.load(file) - logger.debug("Current Start: %s", data[1]['T']) - logger.debug("Current End: %s", data[-1:][0]['T']) + logger.debug("Current Start: %s", format_ms_time(data[1][0])) + logger.debug("Current End: %s", format_ms_time(data[-1:][0][0])) else: data = [] logger.debug("Current Start: None") @@ -146,10 +147,14 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> for row in new_data: if row not in data: data.append(row) - logger.debug("New Start: %s", data[1]['T']) - logger.debug("New End: %s", data[-1:][0]['T']) - data = sorted(data, key=lambda data: data['T']) + logger.debug("New Start: %s", format_ms_time(data[0][0])) + logger.debug("New End: %s", format_ms_time(data[-1:][0][0])) + data = sorted(data, key=lambda data: data[0]) misc.file_dump_json(filename, data) return True + + +def format_ms_time(date: str) -> str: + return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S') From f51ef1a7915611459657d44234e2258548621338 Mon Sep 17 00:00:00 2001 From: Matthias Voppichler Date: Sun, 25 Mar 2018 13:38:17 +0200 Subject: [PATCH 19/22] refactor format_ms_time to misc.py --- freqtrade/misc.py | 8 ++++++++ freqtrade/optimize/__init__.py | 13 ++++--------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/freqtrade/misc.py b/freqtrade/misc.py index f5d045c44..bc04d6b88 100644 --- a/freqtrade/misc.py +++ b/freqtrade/misc.py @@ -72,3 +72,11 @@ def file_dump_json(filename, data) -> None: """ with open(filename, 'w') as fp: json.dump(data, fp, default=str) + + +def format_ms_time(date: str) -> str: + """ + convert MS date to readable format. + : epoch-string in ms + """ + return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S') diff --git a/freqtrade/optimize/__init__.py b/freqtrade/optimize/__init__.py index be2300de7..30be5dc33 100644 --- a/freqtrade/optimize/__init__.py +++ b/freqtrade/optimize/__init__.py @@ -4,7 +4,6 @@ import gzip import json import os from typing import Optional, List, Dict, Tuple -from datetime import datetime from freqtrade import misc from freqtrade.exchange import get_ticker_history @@ -136,8 +135,8 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> if os.path.isfile(filename): with open(filename, "rt") as file: data = json.load(file) - logger.debug("Current Start: %s", format_ms_time(data[1][0])) - logger.debug("Current End: %s", format_ms_time(data[-1:][0][0])) + logger.debug("Current Start: %s", misc.format_ms_time(data[1][0])) + logger.debug("Current End: %s", misc.format_ms_time(data[-1:][0][0])) else: data = [] logger.debug("Current Start: None") @@ -147,14 +146,10 @@ def download_backtesting_testdata(datadir: str, pair: str, interval: int = 5) -> for row in new_data: if row not in data: data.append(row) - logger.debug("New Start: %s", format_ms_time(data[0][0])) - logger.debug("New End: %s", format_ms_time(data[-1:][0][0])) + logger.debug("New Start: %s", misc.format_ms_time(data[0][0])) + logger.debug("New End: %s", misc.format_ms_time(data[-1:][0][0])) data = sorted(data, key=lambda data: data[0]) misc.file_dump_json(filename, data) return True - - -def format_ms_time(date: str) -> str: - return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S') From 1b4c1980c260eced456ad3412273ba304c34f08d Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Mon, 26 Mar 2018 09:23:42 +0300 Subject: [PATCH 20/22] exchange: capitalize class name --- freqtrade/exchange/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index e2e22a358..908820466 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -191,7 +191,6 @@ def get_ticker_history(pair: str, tick_interval) -> List[List]: try: ohlcv = _API.fetch_ohlcv(pair, timeframe=str(tick_interval)+"m") - return ohlcv except IndexError as e: logger.warning('Empty ticker history. Msg %s', str(e)) @@ -235,7 +234,7 @@ def get_market_summaries() -> List[Dict]: def get_name() -> str: - return _API.__class__.__name__ + return _API.__class__.__name__.capitalize() def get_fee_maker() -> float: From 3069a422e91a80106b951d5897e92c8b3696bd69 Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Mon, 26 Mar 2018 09:24:22 +0300 Subject: [PATCH 21/22] Conftest: use coins that we know are in bittrex, added a new conf for ccxt unittest --- freqtrade/tests/conftest.py | 45 +++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index d3d2b4d9a..be21ef9f6 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -72,6 +72,51 @@ def default_conf(): "enabled": True, "key": "key", "secret": "secret", + "pair_whitelist": [ + "ETH/BTC", + "NEO/BTC", + "LTC/BTC", + "XRP/BTC" + ] + }, + "telegram": { + "enabled": True, + "token": "token", + "chat_id": "0" + }, + "initial_state": "running", + "loglevel": logging.DEBUG + } + validate(configuration, Constants.CONF_SCHEMA) + return configuration + + +@pytest.fixture(scope="module") +def default_conf_ccxt(): + """ Returns validated configuration suitable for most tests """ + configuration = { + "max_open_trades": 1, + "stake_currency": "BTC", + "stake_amount": 0.001, + "fiat_display_currency": "USD", + "ticker_interval": 5, + "dry_run": True, + "minimal_roi": { + "40": 0.0, + "30": 0.01, + "20": 0.02, + "0": 0.04 + }, + "stoploss": -0.10, + "unfilledtimeout": 600, + "bid_strategy": { + "ask_last_balance": 0.0 + }, + "exchange": { + "name": "ccxt-unittest", + "enabled": True, + "key": "key", + "secret": "secret", "pair_whitelist": [ "ETH/BTC", "TKN/BTC", From 0a32d38ad97935fc24adc507a8237d146428e765 Mon Sep 17 00:00:00 2001 From: Samuel Husso Date: Mon, 26 Mar 2018 09:24:50 +0300 Subject: [PATCH 22/22] exchange: fix get_ticker_history test --- freqtrade/tests/exchange/test_exchange.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/freqtrade/tests/exchange/test_exchange.py b/freqtrade/tests/exchange/test_exchange.py index f2874f2da..6a423cefd 100644 --- a/freqtrade/tests/exchange/test_exchange.py +++ b/freqtrade/tests/exchange/test_exchange.py @@ -205,9 +205,10 @@ def test_get_ticker_history(default_conf, mocker): tick = 123 api_mock.get_ticker_history = MagicMock(return_value=tick) mocker.patch('freqtrade.exchange._API', api_mock) - + mocker.patch('freqtrade.exchange._API.has', {'fetchOHLCV': True}) + mocker.patch('freqtrade.exchange._API.fetch_ohlcv', return_value=tick) # retrieve original ticker - ticks = get_ticker_history('BTC_ETH', int(default_conf['ticker_interval'])) + ticks = get_ticker_history('ETH/BTC', int(default_conf['ticker_interval'])) assert ticks == 123 # change the ticker