diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index 4305fda91..ec02a0be8 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -67,6 +67,7 @@ class FreqtradeBot(LoggingMixin): init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) + # TODO-mg: Do anything with this? self.wallets = Wallets(self.config, self.exchange) PairLocks.timeframe = self.config['timeframe'] @@ -78,6 +79,7 @@ class FreqtradeBot(LoggingMixin): # so anything in the Freqtradebot instance should be ready (initialized), including # the initial state of the bot. # Keep this at the end of this initialization method. + # TODO-mg: Do I need to consider the rpc, pairlists or dataprovider? self.rpc: RPCManager = RPCManager(self) self.pairlists = PairListManager(self.exchange, self.config) @@ -100,6 +102,7 @@ class FreqtradeBot(LoggingMixin): self.state = State[initial_state.upper()] if initial_state else State.STOPPED # Protect sell-logic from forcesell and vice versa + # TODO-mg: update to _close_lock self._sell_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) @@ -174,13 +177,15 @@ class FreqtradeBot(LoggingMixin): self.strategy.analyze(self.active_pair_whitelist) + # TODO-mg: update to _close_lock with self._sell_lock: # Check and handle any timed out open orders self.check_handle_timedout() - # Protect from collisions with forcesell. + # Protect from collisions with forcesell(#TODO-mg: update to forceclose). # Without this, freqtrade my try to recreate stoploss_on_exchange orders - # while selling is in process, since telegram messages arrive in an different thread. + # while closing is in process, since telegram messages arrive in an different thread. + # TODO-mg: update to _close_lock with self._sell_lock: trades = Trade.get_open_trades() # First process current opened trades (positions) @@ -258,6 +263,7 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Updating {len(orders)} open orders.") for order in orders: try: + # TODO-mg: How to consider borrow orders? fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, order.ft_order_side == 'stoploss') @@ -278,49 +284,56 @@ class FreqtradeBot(LoggingMixin): trades: List[Trade] = Trade.get_closed_trades_without_assigned_fees() for trade in trades: - - if not trade.is_open and not trade.fee_updated('sell'): + if not trade.is_open and not trade.fee_updated(trade.exit_side): # Get sell fee - order = trade.select_order('sell', False) + order = trade.select_order(trade.exit_side, False) if order: - logger.info(f"Updating sell-fee on trade {trade} for order {order.order_id}.") + logger.info( + f"Updating {trade.exit_side}-fee on trade {trade}" + f"for order {order.order_id}." + ) self.update_trade_state(trade, order.order_id, stoploss_order=order.ft_order_side == 'stoploss') trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() for trade in trades: - if trade.is_open and not trade.fee_updated('buy'): - order = trade.select_order('buy', False) + if trade.is_open and not trade.fee_updated(trade.enter_side): + order = trade.select_order(trade.enter_side, False) if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") + logger.info( + f"Updating {trade.enter_side}-fee on trade {trade}" + f"for order {order.order_id}." + ) self.update_trade_state(trade, order.order_id) def handle_insufficient_funds(self, trade: Trade): """ - Determine if we ever opened a sell order for this trade. - If not, try update buy fees - otherwise "refind" the open order we obviously lost. + Determine if we ever opened a exiting order for this trade. + If not, try update entering fees - otherwise "refind" the open order we obviously lost. """ - sell_order = trade.select_order('sell', None) - if sell_order: + exit_order = trade.select_order(trade.exit_side, None) + if exit_order: self.refind_lost_order(trade) else: - self.reupdate_buy_order_fees(trade) + self.reupdate_enter_order_fees(trade) - def reupdate_buy_order_fees(self, trade: Trade): + def reupdate_enter_order_fees(self, trade: Trade): """ Get buy order from database, and try to reupdate. Handles trades where the initial fee-update did not work. """ - logger.info(f"Trying to reupdate buy fees for {trade}") - order = trade.select_order('buy', False) + + logger.info(f"Trying to reupdate {trade.enter_side} fees for {trade}") + order = trade.select_order(trade.enter_side, False) if order: - logger.info(f"Updating buy-fee on trade {trade} for order {order.order_id}.") + logger.info( + f"Updating {trade.enter_side}-fee on trade {trade} for order {order.order_id}.") self.update_trade_state(trade, order.order_id) def refind_lost_order(self, trade): """ Try refinding a lost trade. - Only used when InsufficientFunds appears on sell orders (stoploss or sell). + Only used when InsufficientFunds appears on exit orders (stoploss or long sell/short buy). Tries to walk the stored orders and sell them off eventually. """ logger.info(f"Trying to refind lost order for {trade}") @@ -330,8 +343,8 @@ class FreqtradeBot(LoggingMixin): if not order.ft_is_open: logger.debug(f"Order {order} is no longer open.") continue - if order.ft_order_side == 'buy': - # Skip buy side - this is handled by reupdate_buy_order_fees + if order.ft_order_side == trade.enter_side: + # Skip buy side - this is handled by reupdate_enter_order_fees continue try: fo = self.exchange.fetch_order_or_stoploss_order(order.order_id, order.ft_pair, @@ -340,7 +353,7 @@ class FreqtradeBot(LoggingMixin): if fo and fo['status'] == 'open': # Assume this as the open stoploss order trade.stoploss_order_id = order.order_id - elif order.ft_order_side == 'sell': + elif order.ft_order_side == trade.exit_side: if fo and fo['status'] == 'open': # Assume this as the open order trade.open_order_id = order.order_id @@ -358,7 +371,7 @@ class FreqtradeBot(LoggingMixin): def enter_positions(self) -> int: """ - Tries to execute buy orders for new trades (positions) + Tries to execute long buy/short sell orders for new trades (positions) """ trades_created = 0 @@ -374,7 +387,7 @@ class FreqtradeBot(LoggingMixin): if not whitelist: logger.info("No currency pair in active pair whitelist, " - "but checking to sell open trades.") + "but checking to exit open trades.") return trades_created if PairLocks.is_global_lock(): lock = PairLocks.get_pair_longest_lock('*') @@ -393,16 +406,16 @@ class FreqtradeBot(LoggingMixin): logger.warning('Unable to create trade for %s: %s', pair, exception) if not trades_created: - logger.debug("Found no buy signals for whitelisted currencies. Trying again...") + logger.debug("Found no enter signals for whitelisted currencies. Trying again...") return trades_created def create_trade(self, pair: str) -> bool: """ - Check the implemented trading strategy for buy signals. + Check the implemented trading strategy for enter signals. - If the pair triggers the buy signal a new trade record gets created - and the buy-order opening the trade gets issued towards the exchange. + If the pair triggers the enter signal a new trade record gets created + and the enter-order opening the trade gets issued towards the exchange. :return: True if a trade has been created. """ @@ -433,17 +446,45 @@ class FreqtradeBot(LoggingMixin): self.strategy.timeframe, analyzed_df ) + (short, exit_short) = (False, False) + # = self.strategy.get_signal(pair, self.strategy.timeframe, analyzed_df) + # #TODO: Update strategy class to get these values - if buy and not sell: + open_buy_signal = buy and not sell + open_short_signal = short and not exit_short + + # TODO: For a market making strategy, this condition should be removed + if (open_buy_signal or open_short_signal): stake_amount = self.wallets.get_trade_stake_amount(pair, self.edge) + if not stake_amount: + logger.debug(f"Stake amount is 0, ignoring possible trade for {pair}.") + return False + # leverage = 1.0 # TODO: get leverage from somewhere + logger.info( + f"{'Buy' if open_buy_signal else 'Short'} signal found: " + f"about create a new trade for {pair} with stake_amount: " + f"{stake_amount} ..." + ) - bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) - if ((bid_check_dom.get('enabled', False)) and - (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): - if self._check_depth_of_market_buy(pair, bid_check_dom): - return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) - else: - return False + # TODO: All later code in this function + if open_buy_signal: + bid_check_dom = self.config.get('bid_strategy', {}).get('check_depth_of_market', {}) + if ((bid_check_dom.get('enabled', False)) and + (bid_check_dom.get('bids_to_ask_delta', 0) > 0)): + if self._check_depth_of_market_buy(pair, bid_check_dom): + return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) + else: + return False + else: + # TODO-mg: Will check_depth_of_market_buy work for shorts? + short_check_dom = self.config.get( + 'short_strategy', {}).get('check_depth_of_market', {}) + if ((short_check_dom.get('enabled', False)) and + (short_check_dom.get('shorts_to_ask_delta', 0) > 0)): + if self._check_depth_of_market_short(pair, short_check_dom): + return self.execute_short(pair, stake_amount) + else: + return False return self.execute_buy(pair, stake_amount, buy_tag=buy_tag) else: @@ -473,6 +514,10 @@ class FreqtradeBot(LoggingMixin): logger.info(f"Bids to asks delta for {pair} does not satisfy condition.") return False + def _check_depth_of_market_short(self, pair: str, conf: Dict) -> bool: + # TODO-mg: implement, use _check_depth_of_market_buy instead if possible + return False + def execute_buy(self, pair: str, stake_amount: float, price: Optional[float] = None, forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool: """ @@ -596,6 +641,10 @@ class FreqtradeBot(LoggingMixin): return True + def execute_short(self, pair: str, stake_amount: float, price: Optional[float] = None, + forcesell: bool = False) -> bool: + return False # TODO-mg: implement + def _notify_buy(self, trade: Trade, order_type: str) -> None: """ Sends rpc notification when a buy occurred. diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index 03f4760b8..ee93a1354 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -28,13 +28,13 @@ class PairListManager(): self._tickers_needed = False for pairlist_handler_config in self._config.get('pairlists', None): pairlist_handler = PairListResolver.load_pairlist( - pairlist_handler_config['method'], - exchange=exchange, - pairlistmanager=self, - config=config, - pairlistconfig=pairlist_handler_config, - pairlist_pos=len(self._pairlist_handlers) - ) + pairlist_handler_config['method'], + exchange=exchange, + pairlistmanager=self, + config=config, + pairlistconfig=pairlist_handler_config, + pairlist_pos=len(self._pairlist_handlers) + ) self._tickers_needed |= pairlist_handler.needstickers self._pairlist_handlers.append(pairlist_handler) @@ -127,7 +127,7 @@ class PairListManager(): :return: pairlist - whitelisted pairs """ try: - + # TODO-mg: filter for pairlists that are able to trade at the desired leverage whitelist = expand_pairlist(pairlist, self._exchange.get_markets().keys(), keep_invalid) except ValueError as err: logger.error(f"Pair whitelist contains an invalid Wildcard: {err}") diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index 7c37bb269..1cac9736a 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -1625,7 +1625,7 @@ def test_enter_positions(mocker, default_conf, caplog) -> None: MagicMock(return_value=False)) n = freqtrade.enter_positions() assert n == 0 - assert log_has('Found no buy signals for whitelisted currencies. Trying again...', caplog) + assert log_has('Found no enter signals for whitelisted currencies. Trying again...', caplog) # create_trade should be called once for every pair in the whitelist. assert mock_ct.call_count == len(default_conf['exchange']['pair_whitelist']) @@ -4332,14 +4332,14 @@ def test_update_closed_trades_without_assigned_fees(mocker, default_conf, fee): @pytest.mark.usefixtures("init_persistence") -def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): +def test_reupdate_enter_order_fees(mocker, default_conf, fee, caplog): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_uts = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.update_trade_state') create_mock_trades(fee) trades = Trade.get_trades().all() - freqtrade.reupdate_buy_order_fees(trades[0]) + freqtrade.reupdate_enter_order_fees(trades[0]) assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 1 assert mock_uts.call_args_list[0][0][0] == trades[0] @@ -4362,7 +4362,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): ) Trade.query.session.add(trade) - freqtrade.reupdate_buy_order_fees(trade) + freqtrade.reupdate_enter_order_fees(trade) assert log_has_re(r"Trying to reupdate buy fees for .*", caplog) assert mock_uts.call_count == 0 assert not log_has_re(r"Updating buy-fee on trade .* for order .*\.", caplog) @@ -4372,7 +4372,7 @@ def test_reupdate_buy_order_fees(mocker, default_conf, fee, caplog): def test_handle_insufficient_funds(mocker, default_conf, fee): freqtrade = get_patched_freqtradebot(mocker, default_conf) mock_rlo = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.refind_lost_order') - mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_buy_order_fees') + mock_bof = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.reupdate_enter_order_fees') create_mock_trades(fee) trades = Trade.get_trades().all()