diff --git a/config.json.example b/config.json.example index 831d00f5b..671e29a4d 100644 --- a/config.json.example +++ b/config.json.example @@ -5,6 +5,11 @@ "fiat_display_currency": "USD", "ticker_interval" : "5m", "dry_run": false, + "disable_buy" : true, + "unfilledtimeout": { + "buy":10, + "sell":30 + } "trailing_stop": { "positive" : 0.005 }, diff --git a/config_full.json.example b/config_full.json.example index d99bec51b..8a031dd65 100644 --- a/config_full.json.example +++ b/config_full.json.example @@ -4,6 +4,7 @@ "stake_amount": 0.05, "fiat_display_currency": "USD", "dry_run": false, + "disable_buy" : true, "ticker_interval": "5m", "trailing_stop": true, "minimal_roi": { @@ -13,7 +14,10 @@ "0": 0.04 }, "stoploss": -0.10, - "unfilledtimeout": 600, + "unfilledtimeout": { + "buy":10, + "sell":30 + } "bid_strategy": { "ask_last_balance": 0.0, "use_book_order": true, diff --git a/freqtrade/constants.py b/freqtrade/constants.py index 9cfa21565..bc8a7223c 100644 --- a/freqtrade/constants.py +++ b/freqtrade/constants.py @@ -55,7 +55,14 @@ CONF_SCHEMA = { 'minProperties': 1 }, 'stoploss': {'type': 'number', 'maximum': 0, 'exclusiveMaximum': True}, - 'unfilledtimeout': {'type': 'integer', 'minimum': 0}, + 'unfilledtimeout': { + 'type': 'object', + 'properties': { + 'use_book_order': {'type': 'boolean'}, + 'buy': {'type': 'number', 'minimum': 3}, + 'sell': {'type': 'number', 'minimum': 10} + } + }, 'bid_strategy': { 'type': 'object', 'properties': { @@ -66,7 +73,7 @@ CONF_SCHEMA = { 'exclusiveMaximum': False }, 'use_book_order': {'type': 'boolean'}, - 'book_order_top': {'type': 'number', 'maximum':20,'minimum':1} + 'book_order_top': {'type': 'number', 'maximum': 20, 'minimum': 1} }, 'required': ['ask_last_balance'] }, @@ -74,9 +81,9 @@ CONF_SCHEMA = { 'type': 'object', 'properties': { 'use_book_order': {'type': 'boolean'}, - 'book_order_min': {'type': 'number', 'minimum':1}, - 'book_order_max': {'type': 'number', 'minimum':1} - }, + 'book_order_min': {'type': 'number', 'minimum': 1}, + 'book_order_max': {'type': 'number', 'minimum': 1} + } }, 'exchange': {'$ref': '#/definitions/exchange'}, 'experimental': { diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 26e828db3..c0fc32633 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -239,10 +239,11 @@ def get_balances() -> dict: except ccxt.BaseError as e: raise OperationalException(e) + @retrier -def get_order_book(pair: str, refresh: Optional[bool] = True) -> dict: +def get_order_book(pair: str, limit: Optional[int] = 1000) -> dict: try: - return _API.fetch_order_book(pair) + return _API.fetch_order_book(pair, limit) except ccxt.NotSupported as e: raise OperationalException( f'Exchange {_API.name} does not support fetching order book.' @@ -253,6 +254,7 @@ def get_order_book(pair: str, refresh: Optional[bool] = True) -> dict: except ccxt.BaseError as e: raise OperationalException(e) + @retrier def get_tickers() -> Dict: try: diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 56d6e9c81..ee8735d57 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -156,12 +156,15 @@ class FreqtradeBot(object): state_changed |= self.process_maybe_execute_sell(trade) # Then looking for buy opportunities - if len(trades) < self.config['max_open_trades']: - state_changed = self.process_maybe_execute_buy() + if (self.config['disable_buy']): + logger.info('Buy disabled...') + else: + if len(trades) < self.config['max_open_trades']: + state_changed = self.process_maybe_execute_buy() if 'unfilledtimeout' in self.config: # Check and handle any timed out open orders - self.check_handle_timedout(self.config['unfilledtimeout']) + self.check_handle_timedout() Trade.session.flush() except TemporaryError as error: @@ -241,17 +244,23 @@ class FreqtradeBot(object): :return: float: Price """ + ticker = exchange.get_ticker(pair) + if ticker['ask'] < ticker['last']: + return ticker['ask'] + balance = self.config['bid_strategy']['ask_last_balance'] + ticker_rate = ticker['ask'] + balance * (ticker['last'] - ticker['ask']) + if self.config['bid_strategy']['use_book_order']: logger.info('Getting price from Order Book') orderBook = exchange.get_order_book(pair) - return orderBook['bids'][self.config['bid_strategy']['book_order_top']][0] + orderBook_rate = orderBook['bids'][self.config['bid_strategy']['book_order_top']][0] + # if ticker has lower rate, then use ticker ( usefull if down trending ) + if ticker_rate < orderBook_rate: + return ticker_rate + return orderBook_rate else: logger.info('Using Ask / Last Price') - ticker = exchange.get_ticker(pair); - if ticker['ask'] < ticker['last']: - return ticker['ask'] - balance = self.config['bid_strategy']['ask_last_balance'] - return ticker['ask'] + balance * (ticker['last'] - ticker['ask']) + return ticker_rate def create_trade(self) -> bool: """ @@ -439,9 +448,15 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ logger.info('Using order book for selling...') orderBook = exchange.get_order_book(trade.pair) # logger.debug('Order book %s',orderBook) - for i in range(self.config['ask_strategy']['book_order_min'], - self.config['ask_strategy']['book_order_max'] + 1): - sell_rate = orderBook['asks'][i - 1][0] + orderBook_min = self.config['ask_strategy']['book_order_min'] + orderBook_max = self.config['ask_strategy']['book_order_max'] + for i in range(orderBook_min, orderBook_max+1): + orderBook_rate = orderBook['asks'][i-1][0] + # if orderbook has higher rate (high profit), + # use orderbook, otherwise just use sell rate + if (sell_rate < orderBook_rate): + sell_rate = orderBook_rate + if self.check_sell(trade, sell_rate, buy, sell): return True break @@ -458,19 +473,22 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ return True return False - def check_handle_timedout(self, timeoutvalue: int) -> None: + def check_handle_timedout(self) -> None: """ Check if any orders are timed out and cancel if neccessary :param timeoutvalue: Number of minutes until order is considered timed out :return: None """ - timeoutthreashold = arrow.utcnow().shift(minutes=-timeoutvalue).datetime + buy_timeout = self.config['unfilledtimeout']['buy'] + sell_timeout = self.config['unfilledtimeout']['sell'] + buy_timeoutthreashold = arrow.utcnow().shift(minutes=-buy_timeout).datetime + sell_timeoutthreashold = arrow.utcnow().shift(minutes=-sell_timeout).datetime for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): try: # FIXME: Somehow the query above returns results # where the open_order_id is in fact None. - # This is probably because the record got + # This is probably because the record get_trades_for_order # updated via /forcesell in a different thread. if not trade.open_order_id: continue @@ -485,13 +503,11 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \ print(order) # Check if trade is still actually open -# this makes no real sense and causes errors! -# -# if (filled(int(order['filled']) == 0) and (order['status'] == 'open'): - if order['side'] == 'buy' and ordertime < timeoutthreashold: - self.handle_timedout_limit_buy(trade, order) - elif order['side'] == 'sell' and ordertime < timeoutthreashold: - self.handle_timedout_limit_sell(trade, order) + if (int(order['filled']) == 0) and (order['status'] == 'open'): + if order['side'] == 'buy' and ordertime < buy_timeoutthreashold: + self.handle_timedout_limit_buy(trade, order) + elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold: + self.handle_timedout_limit_sell(trade, order) # FIX: 20180110, why is cancel.order unconditionally here, whereas # it is conditionally called in the diff --git a/freqtrade/tests/conftest.py b/freqtrade/tests/conftest.py index a4325dcef..ab5a6ce17 100644 --- a/freqtrade/tests/conftest.py +++ b/freqtrade/tests/conftest.py @@ -89,7 +89,11 @@ def default_conf(): "0": 0.04 }, "stoploss": -0.10, - "unfilledtimeout": 600, + "disable_buy": False, + "unfilledtimeout": { + "buy": 10, + "sell": 30 + }, "bid_strategy": { "use_book_order": False, "book_order_top": 6, @@ -252,7 +256,8 @@ def limit_buy_order(): 'price': 0.00001099, 'amount': 90.99181073, 'remaining': 0.0, - 'status': 'closed' + 'status': 'closed', + 'filled': 0.0 } @@ -267,7 +272,8 @@ def limit_buy_order_old(): 'price': 0.00001099, 'amount': 90.99181073, 'remaining': 90.99181073, - 'status': 'open' + 'status': 'open', + 'filled': 0.0 } @@ -282,7 +288,8 @@ def limit_sell_order_old(): 'price': 0.00001099, 'amount': 90.99181073, 'remaining': 90.99181073, - 'status': 'open' + 'status': 'open', + 'filled': 0.0 } @@ -297,7 +304,8 @@ def limit_buy_order_old_partial(): 'price': 0.00001099, 'amount': 90.99181073, 'remaining': 67.99181073, - 'status': 'open' + 'status': 'open', + 'filled': 0.0 } diff --git a/freqtrade/tests/test_freqtradebot.py b/freqtrade/tests/test_freqtradebot.py index b3de4354e..dc3fd388b 100644 --- a/freqtrade/tests/test_freqtradebot.py +++ b/freqtrade/tests/test_freqtradebot.py @@ -499,7 +499,8 @@ def test_balance_fully_ask_side(mocker) -> None: """ Test get_target_bid() method """ - freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 0.0}}) + param = {'use_book_order': False, 'book_order_top': 6, 'ask_last_balance': 0.0} + freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': param}) assert freqtrade.get_target_bid('ETH/BTC') >= 0.07 @@ -508,7 +509,8 @@ def test_balance_fully_last_side(mocker) -> None: """ Test get_target_bid() method """ - freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 1.0}}) + param = {'use_book_order': False, 'book_order_top': 6, 'ask_last_balance': 0.0} + freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': param}) assert freqtrade.get_target_bid('ETH/BTC') >= 0.07 @@ -517,7 +519,8 @@ def test_balance_bigger_last_ask(mocker) -> None: """ Test get_target_bid() method """ - freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 1.0}}) + param = {'use_book_order': False, 'book_order_top': 6, 'ask_last_balance': 0.0} + freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': param}) assert freqtrade.get_target_bid('ETH/BTC') >= 0.07 @@ -849,7 +852,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe Trade.session.add(trade_buy) # check it does cancel buy orders over the time limit - freqtrade.check_handle_timedout(600) + freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() @@ -890,7 +893,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old, Trade.session.add(trade_sell) # check it does cancel sell orders over the time limit - freqtrade.check_handle_timedout(600) + freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 assert trade_sell.is_open is True @@ -930,7 +933,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old # check it does cancel buy orders over the time limit # note this is for a partially-complete buy order - freqtrade.check_handle_timedout(600) + freqtrade.check_handle_timedout() assert cancel_order_mock.call_count == 1 assert rpc_mock.call_count == 1 trades = Trade.query.filter(Trade.open_order_id.is_(trade_buy.open_order_id)).all() @@ -981,7 +984,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) - 'recent call last):\n.*' ) - freqtrade.check_handle_timedout(600) + freqtrade.check_handle_timedout() assert filter(regexp.match, caplog.record_tuples)