diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 614e8ad68..b356a8147 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -5,6 +5,7 @@ from freqtrade.exchange.exchange import Exchange # isort: on from freqtrade.exchange.bibox import Bibox from freqtrade.exchange.binance import Binance +from freqtrade.exchange.bitpanda import Bitpanda from freqtrade.exchange.bittrex import Bittrex from freqtrade.exchange.bybit import Bybit from freqtrade.exchange.coinbasepro import Coinbasepro diff --git a/freqtrade/exchange/bitpanda.py b/freqtrade/exchange/bitpanda.py new file mode 100644 index 000000000..4cac35ce8 --- /dev/null +++ b/freqtrade/exchange/bitpanda.py @@ -0,0 +1,37 @@ +""" Bitpanda exchange subclass """ +import logging +from datetime import datetime, timezone +from typing import Dict, List, Optional + +from freqtrade.exchange import Exchange + + +logger = logging.getLogger(__name__) + + +class Bitpanda(Exchange): + """ + Bitpanda exchange class. Contains adjustments needed for Freqtrade to work + with this exchange. + """ + + def get_trades_for_order(self, order_id: str, pair: str, since: datetime, + params: Optional[Dict] = None) -> List: + """ + Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id. + The "since" argument passed in is coming from the database and is in UTC, + as timezone-native datetime object. + From the python documentation: + > Naive datetime instances are assumed to represent local time + Therefore, calling "since.timestamp()" will get the UTC timestamp, after applying the + transformation from local timezone to UTC. + This works for timezones UTC+ since then the result will contain trades from a few hours + instead of from the last 5 seconds, however fails for UTC- timezones, + since we're then asking for trades with a "since" argument in the future. + + :param order_id order_id: Order-id as given when creating the order + :param pair: Pair the order is for + :param since: datetime object of the order creation time. Assumes object is in UTC. + """ + params = {'to': int(datetime.now(timezone.utc).timestamp() * 1000)} + return super().get_trades_for_order(order_id, pair, since, params) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index 66d6b3b55..813938f99 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1091,7 +1091,8 @@ class Exchange: # Fee handling @retrier - def get_trades_for_order(self, order_id: str, pair: str, since: datetime) -> List: + def get_trades_for_order(self, order_id: str, pair: str, since: datetime, + params: Optional[Dict] = None) -> List: """ Fetch Orders using the "fetch_my_trades" endpoint and filter them by order-id. The "since" argument passed in is coming from the database and is in UTC, @@ -1115,8 +1116,10 @@ class Exchange: try: # Allow 5s offset to catch slight time offsets (discovered in #1185) # since needs to be int in milliseconds + _params = params if params else {} my_trades = self._api.fetch_my_trades( - pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000)) + pair, int((since.replace(tzinfo=timezone.utc).timestamp() - 5) * 1000), + params=_params) matched_trades = [trade for trade in my_trades if trade['order'] == order_id] self._log_exchange_response('get_trades_for_order', matched_trades) diff --git a/tests/exchange/test_bitpanda.py b/tests/exchange/test_bitpanda.py new file mode 100644 index 000000000..4bd168e7e --- /dev/null +++ b/tests/exchange/test_bitpanda.py @@ -0,0 +1,47 @@ +from datetime import datetime +from unittest.mock import MagicMock + +from tests.conftest import get_patched_exchange + + +def test_get_trades_for_order(default_conf, mocker): + exchange_name = 'bitpanda' + order_id = 'ABCD-ABCD' + since = datetime(2018, 5, 5, 0, 0, 0) + default_conf["dry_run"] = False + mocker.patch('freqtrade.exchange.Exchange.exchange_has', return_value=True) + api_mock = MagicMock() + + api_mock.fetch_my_trades = MagicMock(return_value=[{'id': 'TTR67E-3PFBD-76IISV', + 'order': 'ABCD-ABCD', + 'info': {'pair': 'XLTCZBTC', + 'time': 1519860024.4388, + 'type': 'buy', + 'ordertype': 'limit', + 'price': '20.00000', + 'cost': '38.62000', + 'fee': '0.06179', + 'vol': '5', + 'id': 'ABCD-ABCD'}, + 'timestamp': 1519860024438, + 'datetime': '2018-02-28T23:20:24.438Z', + 'symbol': 'LTC/BTC', + 'type': 'limit', + 'side': 'buy', + 'price': 165.0, + 'amount': 0.2340606, + 'fee': {'cost': 0.06179, 'currency': 'BTC'} + }]) + exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) + + orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since) + assert len(orders) == 1 + assert orders[0]['price'] == 165 + assert api_mock.fetch_my_trades.call_count == 1 + # since argument should be + assert isinstance(api_mock.fetch_my_trades.call_args[0][1], int) + assert api_mock.fetch_my_trades.call_args[0][0] == 'LTC/BTC' + # Same test twice, hardcoded number and doing the same calculation + assert api_mock.fetch_my_trades.call_args[0][1] == 1525478395000 + # bitpanda requires "to" argument. + assert 'to' in api_mock.fetch_my_trades.call_args[1]['params'] diff --git a/tests/optimize/test_hyperopt.py b/tests/optimize/test_hyperopt.py index a43c62376..a5a131e45 100644 --- a/tests/optimize/test_hyperopt.py +++ b/tests/optimize/test_hyperopt.py @@ -169,6 +169,7 @@ def test_start_no_hyperopt_allowed(mocker, hyperopt_conf, caplog) -> None: def test_start_no_data(mocker, hyperopt_conf) -> None: + hyperopt_conf['user_data_dir'] = Path("tests") patched_configuration_load_config_file(mocker, hyperopt_conf) mocker.patch('freqtrade.data.history.load_pair_history', MagicMock(return_value=pd.DataFrame)) mocker.patch( @@ -189,6 +190,12 @@ def test_start_no_data(mocker, hyperopt_conf) -> None: with pytest.raises(OperationalException, match='No data found. Terminating.'): start_hyperopt(pargs) + # Cleanup since that failed hyperopt start leaves a lockfile. + try: + Path(Hyperopt.get_lock_filename(hyperopt_conf)).unlink() + except Exception: + pass + def test_start_filelock(mocker, hyperopt_conf, caplog) -> None: hyperopt_mock = MagicMock(side_effect=Timeout(Hyperopt.get_lock_filename(hyperopt_conf)))