From 157a43f2bae8a33103cbfda2d7831259a8705d20 Mon Sep 17 00:00:00 2001 From: kecheon Date: Sun, 11 Apr 2021 19:23:12 +0900 Subject: [PATCH] add upbit --- freqtrade/constants.py | 2 +- freqtrade/exchange/__init__.py | 1 + freqtrade/exchange/upbit.py | 90 +++++++++++++++++++++++++++ tests/exchange/test_exchange.py | 14 ++++- tests/exchange/test_upbit.py | 107 ++++++++++++++++++++++++++++++++ 5 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 freqtrade/exchange/upbit.py create mode 100644 tests/exchange/test_upbit.py diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 7b955c37d..f7721dd30 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -29,7 +29,7 @@ AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList', 'SpreadFilter', 'VolatilityFilter'] AVAILABLE_PROTECTIONS = ['CooldownPeriod', 'LowProfitPairs', 'MaxDrawdown', 'StoplossGuard'] AVAILABLE_DATAHANDLERS = ['json', 'jsongz', 'hdf5'] -DRY_RUN_WALLET = 1000 +DRY_RUN_WALLET = 1 # BTC DATETIME_PRINT_FORMAT = '%Y-%m-%d %H:%M:%S' MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume'] diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 8a5563623..629370c9e 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -7,6 +7,7 @@ from freqtrade.exchange.bibox import Bibox from freqtrade.exchange.binance import Binance from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bybit import Bybit +from freqtrade.exchange.upbit import Upbit from freqtrade.exchange.exchange import (available_exchanges, ccxt_exchanges, is_exchange_known_ccxt, is_exchange_officially_supported, market_is_active, timeframe_to_minutes, timeframe_to_msecs, diff --git a/freqtrade/exchange/upbit.py b/freqtrade/exchange/upbit.py new file mode 100644 index 000000000..22f4464cf --- /dev/null +++ b/freqtrade/exchange/upbit.py @@ -0,0 +1,90 @@ +""" Binance exchange subclass """ +import logging +from typing import Dict + +import ccxt + +from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException, + OperationalException, TemporaryError) +from freqtrade.exchange import Exchange +from freqtrade.exchange.common import retrier + + +logger = logging.getLogger(__name__) + + +class Upbit(Exchange): + + _ft_has: Dict = { + "stoploss_on_exchange": True, + "order_time_in_force": ['gtc', 'fok', 'ioc'], + "ohlcv_candle_limit": 1000, + "trades_pagination": "id", + "trades_pagination_arg": "fromId", + "l2_limit_range": [5, 10, 20, 50, 100, 500, 1000], + } + + def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool: + """ + Verify stop_loss against stoploss-order value (limit or price) + Returns True if adjustment is necessary. + """ + return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice']) + + @retrier(retries=0) + def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict: + """ + creates a stoploss limit order. + this stoploss-limit is binance-specific. + It may work with a limited number of other exchanges, but this has not been tested yet. + """ + # Limit price threshold: As limit price should always be below stop-price + limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99) + rate = stop_price * limit_price_pct + + ordertype = "stop_loss_limit" + + stop_price = self.price_to_precision(pair, stop_price) + + # Ensure rate is less than stop price + if stop_price <= rate: + raise OperationalException( + 'In stoploss limit order, stop price should be more than limit price') + + if self._config['dry_run']: + dry_order = self.create_dry_run_order( + pair, ordertype, "sell", amount, stop_price) + return dry_order + + try: + params = self._params.copy() + params.update({'stopPrice': stop_price}) + + amount = self.amount_to_precision(pair, amount) + + rate = self.price_to_precision(pair, rate) + + order = self._api.create_order(symbol=pair, type=ordertype, side='sell', + amount=amount, price=rate, params=params) + logger.info('stoploss limit order added for %s. ' + 'stop price: %s. limit: %s', pair, stop_price, rate) + return order + except ccxt.InsufficientFunds as e: + raise InsufficientFundsError( + f'Insufficient funds to create {ordertype} sell order on market {pair}. ' + f'Tried to sell amount {amount} at rate {rate}. ' + f'Message: {e}') from e + except ccxt.InvalidOrder as e: + # Errors: + # `binance Order would trigger immediately.` + raise InvalidOrderException( + f'Could not create {ordertype} sell order on market {pair}. ' + f'Tried to sell amount {amount} at rate {rate}. ' + f'Message: {e}') from e + except ccxt.DDoSProtection as e: + raise DDosProtection(e) from e + except (ccxt.NetworkError, ccxt.ExchangeError) as e: + raise TemporaryError( + f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e + except ccxt.BaseError as e: + raise OperationalException(e) from e diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 4ceba6eba..bcb41bf97 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -12,7 +12,7 @@ from pandas import DataFrame from freqtrade.exceptions import (DDosProtection, DependencyException, InvalidOrderException, OperationalException, TemporaryError) -from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken +from freqtrade.exchange import Binance, Bittrex, Exchange, Kraken, Upbit from freqtrade.exchange.common import (API_FETCH_ORDER_RETRY_COUNT, API_RETRY_COUNT, calculate_backoff) from freqtrade.exchange.exchange import (market_is_active, timeframe_to_minutes, timeframe_to_msecs, @@ -157,7 +157,7 @@ def test_exchange_resolver(default_conf, mocker, caplog): exchange = ExchangeResolver.load_exchange('kraken', default_conf) assert isinstance(exchange, Exchange) assert isinstance(exchange, Kraken) - assert not isinstance(exchange, Binance) + assert not isinstance(exchange, Upbit) assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) @@ -166,6 +166,14 @@ def test_exchange_resolver(default_conf, mocker, caplog): assert isinstance(exchange, Binance) assert not isinstance(exchange, Kraken) + assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", + caplog) + + exchange = ExchangeResolver.load_exchange('upbit', default_conf) + assert isinstance(exchange, Exchange) + assert isinstance(exchange, Upbit) + assert not isinstance(exchange, Kraken) + assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.", caplog) @@ -1998,7 +2006,7 @@ def test_cancel_order_dry_run(default_conf, mocker, exchange_name): ({'status': 'canceled', 'filled': 10.0}, False), ({'status': 'unknown', 'filled': 10.0}, False), ({'result': 'testest123'}, False), - ]) +]) def test_check_order_canceled_empty(mocker, default_conf, exchange_name, order, result): exchange = get_patched_exchange(mocker, default_conf, id=exchange_name) assert exchange.check_order_canceled_empty(order) == result diff --git a/tests/exchange/test_upbit.py b/tests/exchange/test_upbit.py new file mode 100644 index 000000000..cf9e736c6 --- /dev/null +++ b/tests/exchange/test_upbit.py @@ -0,0 +1,107 @@ +from random import randint +from unittest.mock import MagicMock + +import ccxt +import pytest + +from freqtrade.exceptions import DependencyException, InvalidOrderException, OperationalException +from tests.conftest import get_patched_exchange +from tests.exchange.test_exchange import ccxt_exceptionhandlers + + +@pytest.mark.parametrize('limitratio,expected', [ + (None, 220 * 0.99), + (0.99, 220 * 0.99), + (0.98, 220 * 0.98), +]) +def test_stoploss_order_upbit(default_conf, mocker, limitratio, expected): + api_mock = MagicMock() + order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6)) + order_type = 'stop_loss_limit' + + api_mock.create_order = MagicMock(return_value={ + 'id': order_id, + 'info': { + 'foo': 'bar' + } + }) + default_conf['dry_run'] = False + mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'upbit') + + with pytest.raises(OperationalException): + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + + api_mock.create_order.reset_mock() + order_types = {} if limitratio is None else {'stoploss_on_exchange_limit_ratio': limitratio} + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types=order_types) + + assert 'id' in order + assert 'info' in order + assert order['id'] == order_id + assert api_mock.create_order.call_args_list[0][1]['symbol'] == 'ETH/BTC' + assert api_mock.create_order.call_args_list[0][1]['type'] == order_type + assert api_mock.create_order.call_args_list[0][1]['side'] == 'sell' + assert api_mock.create_order.call_args_list[0][1]['amount'] == 1 + # Price should be 1% below stopprice + assert api_mock.create_order.call_args_list[0][1]['price'] == expected + assert api_mock.create_order.call_args_list[0][1]['params'] == {'stopPrice': 220} + + # test exception handling + with pytest.raises(DependencyException): + api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'upbit') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + with pytest.raises(InvalidOrderException): + api_mock.create_order = MagicMock( + side_effect=ccxt.InvalidOrder("upbit Order would trigger immediately.")) + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'upbit') + exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + ccxt_exceptionhandlers(mocker, default_conf, api_mock, "upbit", + "stoploss", "create_order", retries=1, + pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + +def test_stoploss_order_dry_run_upbit(default_conf, mocker): + api_mock = MagicMock() + order_type = 'stop_loss_limit' + default_conf['dry_run'] = True + mocker.patch('freqtrade.exchange.Exchange.amount_to_precision', lambda s, x, y: y) + mocker.patch('freqtrade.exchange.Exchange.price_to_precision', lambda s, x, y: y) + + exchange = get_patched_exchange(mocker, default_conf, api_mock, 'upbit') + + with pytest.raises(OperationalException): + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=190, + order_types={'stoploss_on_exchange_limit_ratio': 1.05}) + + api_mock.create_order.reset_mock() + + order = exchange.stoploss(pair='ETH/BTC', amount=1, stop_price=220, order_types={}) + + assert 'id' in order + assert 'info' in order + assert 'type' in order + + assert order['type'] == order_type + assert order['price'] == 220 + assert order['amount'] == 1 + + +def test_stoploss_adjust_upbit(mocker, default_conf): + exchange = get_patched_exchange(mocker, default_conf, id='upbit') + order = { + 'type': 'stop_loss_limit', + 'price': 1500, + 'info': {'stopPrice': 1500}, + } + assert exchange.stoploss_adjust(1501, order) + assert not exchange.stoploss_adjust(1499, order) + # Test with invalid order case + order['type'] = 'stop_loss' + assert not exchange.stoploss_adjust(1501, order)