diff --git a/freqtrade/enums/signaltype.py b/freqtrade/enums/signaltype.py index d2995d57a..fc57e1ce7 100644 --- a/freqtrade/enums/signaltype.py +++ b/freqtrade/enums/signaltype.py @@ -3,7 +3,7 @@ from enum import Enum class SignalType(Enum): """ - Enum to distinguish between buy and sell signals + Enum to distinguish between enter and exit signals """ BUY = "buy" SELL = "sell" diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index f2e8e3aa0..ec2745b03 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -66,6 +66,7 @@ class FreqtradeBot(LoggingMixin): init_db(self.config.get('db_url', None), clean_open_orders=self.config['dry_run']) + # TODO-lev: Do anything with this? self.wallets = Wallets(self.config, self.exchange) PairLocks.timeframe = self.config['timeframe'] @@ -77,6 +78,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-lev: Do I need to consider the rpc, pairlists or dataprovider? self.rpc: RPCManager = RPCManager(self) self.pairlists = PairListManager(self.exchange, self.config) @@ -98,7 +100,7 @@ class FreqtradeBot(LoggingMixin): initial_state = self.config.get('initial_state') self.state = State[initial_state.upper()] if initial_state else State.STOPPED - # Protect sell-logic from forcesell and vice versa + # Protect exit-logic from forcesell and vice versa self._sell_lock = Lock() LoggingMixin.__init__(self, logger, timeframe_to_seconds(self.strategy.timeframe)) @@ -170,9 +172,9 @@ class FreqtradeBot(LoggingMixin): # Check and handle any timed out open orders self.check_handle_timedout() - # Protect from collisions with forcesell. + # Protect from collisions with forceexit. # 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 exiting is in process, since telegram messages arrive in an different thread. with self._sell_lock: trades = Trade.get_open_trades() # First process current opened trades (positions) @@ -289,8 +291,8 @@ class FreqtradeBot(LoggingMixin): 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: @@ -312,7 +314,7 @@ class FreqtradeBot(LoggingMixin): 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}") @@ -323,7 +325,7 @@ class FreqtradeBot(LoggingMixin): 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 + # 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, @@ -350,7 +352,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 @@ -366,7 +368,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('*') @@ -512,7 +514,9 @@ class FreqtradeBot(LoggingMixin): order_type = self.strategy.order_types['buy'] if forcebuy: # Forcebuy can define a different ordertype + # TODO-lev: get a forceshort? What is this order_type = self.strategy.order_types.get('forcebuy', order_type) + # TODO-lev: Will this work for shorting? if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)( pair=pair, order_type=order_type, amount=amount, rate=buy_limit_requested, @@ -596,7 +600,7 @@ class FreqtradeBot(LoggingMixin): def _notify_buy(self, trade: Trade, order_type: str) -> None: """ - Sends rpc notification when a buy occurred. + Sends rpc notification when a buy/short occurred. """ msg = { 'trade_id': trade.id, @@ -619,7 +623,7 @@ class FreqtradeBot(LoggingMixin): def _notify_buy_cancel(self, trade: Trade, order_type: str, reason: str) -> None: """ - Sends rpc notification when a buy cancel occurred. + Sends rpc notification when a buy/short cancel occurred. """ current_rate = self.exchange.get_rate(trade.pair, refresh=False, side="buy") @@ -665,7 +669,7 @@ class FreqtradeBot(LoggingMixin): def exit_positions(self, trades: List[Any]) -> int: """ - Tries to execute sell orders for open trades (positions) + Tries to execute sell/exit_short orders for open trades (positions) """ trades_closed = 0 for trade in trades: @@ -691,8 +695,8 @@ class FreqtradeBot(LoggingMixin): def handle_trade(self, trade: Trade) -> bool: """ - Sells the current pair if the threshold is reached and updates the trade record. - :return: True if trade has been sold, False otherwise + Sells/exits_short the current pair if the threshold is reached and updates the trade record. + :return: True if trade has been sold/exited_short, False otherwise """ if not trade.is_open: raise DependencyException(f'Attempt to handle closed trade: {trade}') @@ -700,7 +704,7 @@ class FreqtradeBot(LoggingMixin): logger.debug('Handling %s ...', trade) (buy, sell) = (False, False) - + # TODO-lev: change to use_exit_signal, ignore_roi_if_enter_signal if (self.config.get('use_sell_signal', True) or self.config.get('ignore_roi_if_buy_signal', False)): analyzed_df, _ = self.dataprovider.get_analyzed_dataframe(trade.pair, @@ -744,7 +748,7 @@ class FreqtradeBot(LoggingMixin): except InvalidOrderException as e: trade.stoploss_order_id = None logger.error(f'Unable to place a stoploss order on exchange. {e}') - logger.warning('Selling the trade forcefully') + logger.warning('Exiting the trade forcefully') self.execute_trade_exit(trade, trade.stop_loss, sell_reason=SellCheckTuple( sell_type=SellType.EMERGENCY_SELL)) @@ -758,6 +762,8 @@ class FreqtradeBot(LoggingMixin): Check if trade is fulfilled in which case the stoploss on exchange should be added immediately if stoploss on exchange is enabled. + # TODO-lev: liquidation price will always be on exchange, even though + # TODO-lev: stoploss_on_exchange might not be enabled """ logger.debug('Handling stoploss on exchange %s ...', trade) @@ -776,6 +782,7 @@ class FreqtradeBot(LoggingMixin): # We check if stoploss order is fulfilled if stoploss_order and stoploss_order['status'] in ('closed', 'triggered'): + # TODO-lev: Update to exit reason trade.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value self.update_trade_state(trade, trade.stoploss_order_id, stoploss_order, stoploss_order=True) @@ -791,7 +798,7 @@ class FreqtradeBot(LoggingMixin): # The trade can be closed already (sell-order fill confirmation came in this iteration) return False - # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange + # If enter order is fulfilled but there is no stoploss, we add a stoploss on exchange if not stoploss_order: stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss stop_price = trade.open_rate * (1 + stoploss) @@ -942,6 +949,7 @@ class FreqtradeBot(LoggingMixin): Buy cancel - cancel order :return: True if order was fully cancelled """ + # TODO-lev: Pay back borrowed/interest and transfer back on leveraged trades was_trade_fully_canceled = False # Cancelled orders may have the status of 'canceled' or 'closed' @@ -986,6 +994,8 @@ class FreqtradeBot(LoggingMixin): # to the order dict acquired before cancelling. # we need to fall back to the values from order if corder does not contain these keys. trade.amount = filled_amount + # TODO-lev: Check edge cases, we don't want to make leverage > 1.0 if we don't have to + trade.stake_amount = trade.amount * trade.open_rate self.update_trade_state(trade, trade.open_order_id, corder) @@ -994,13 +1004,15 @@ class FreqtradeBot(LoggingMixin): reason += f", {constants.CANCEL_REASON['PARTIALLY_FILLED']}" self.wallets.update() + # TODO-lev: Should short and exit_short be an order type? + self._notify_buy_cancel(trade, order_type=self.strategy.order_types['buy'], reason=reason) return was_trade_fully_canceled def handle_cancel_sell(self, trade: Trade, order: Dict, reason: str) -> str: """ - Sell cancel - cancel order and update trade + Sell/exit_short cancel - cancel order and update trade :return: Reason for cancel """ # if trade is not partially completed, just cancel the order @@ -1050,6 +1062,7 @@ class FreqtradeBot(LoggingMixin): :return: amount to sell :raise: DependencyException: if available balance is not within 2% of the available amount. """ + # TODO-lev Maybe update? # Update wallets to ensure amounts tied up in a stoploss is now free! self.wallets.update() trade_base_currency = self.exchange.get_pair_base_currency(pair) @@ -1072,7 +1085,7 @@ class FreqtradeBot(LoggingMixin): :param sell_reason: Reason the sell was triggered :return: True if it succeeds (supported) False (not supported) """ - sell_type = 'sell' + sell_type = 'sell' # TODO-lev: Update to exit if sell_reason.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS): sell_type = 'stoploss' @@ -1112,22 +1125,25 @@ class FreqtradeBot(LoggingMixin): order_type = self.strategy.order_types.get("forcesell", order_type) amount = self._safe_sell_amount(trade.pair, trade.amount) - time_in_force = self.strategy.order_time_in_force['sell'] + time_in_force = self.strategy.order_time_in_force['sell'] # TODO-lev update to exit if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)( pair=trade.pair, trade=trade, order_type=order_type, amount=amount, rate=limit, time_in_force=time_in_force, sell_reason=sell_reason.sell_reason, - current_time=datetime.now(timezone.utc)): - logger.info(f"User requested abortion of selling {trade.pair}") + current_time=datetime.now(timezone.utc)): # TODO-lev: Update to exit + logger.info(f"User requested abortion of exiting {trade.pair}") return False try: # Execute sell and update trade record - order = self.exchange.create_order(pair=trade.pair, - ordertype=order_type, side="sell", - amount=amount, rate=limit, - time_in_force=time_in_force - ) + order = self.exchange.create_order( + pair=trade.pair, + ordertype=order_type, + side="sell", + amount=amount, + rate=limit, + time_in_force=time_in_force + ) except InsufficientFundsError as e: logger.warning(f"Unable to place order {e}.") # Try to figure out what went wrong @@ -1138,15 +1154,15 @@ class FreqtradeBot(LoggingMixin): trade.orders.append(order_obj) trade.open_order_id = order['id'] - trade.sell_order_status = '' + trade.sell_order_status = '' # TODO-lev: Update to exit_order_status trade.close_rate_requested = limit - trade.sell_reason = sell_reason.sell_reason + trade.sell_reason = sell_reason.sell_reason # TODO-lev: Update to exit_reason # In case of market sell orders the order can be closed immediately if order.get('status', 'unknown') in ('closed', 'expired'): self.update_trade_state(trade, trade.open_order_id, order) Trade.commit() - # Lock pair for one candle to prevent immediate re-buys + # Lock pair for one candle to prevent immediate re-trading self.strategy.lock_pair(trade.pair, datetime.now(timezone.utc), reason='Auto lock') @@ -1181,7 +1197,7 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, + 'sell_reason': trade.sell_reason, # TODO-lev: change to exit_reason 'open_date': trade.open_date, 'close_date': trade.close_date or datetime.utcnow(), 'stake_currency': self.config['stake_currency'], @@ -1200,10 +1216,10 @@ class FreqtradeBot(LoggingMixin): """ Sends rpc notification when a sell cancel occurred. """ - if trade.sell_order_status == reason: + if trade.sell_order_status == reason: # TODO-lev: Update to exit_order_status return else: - trade.sell_order_status = reason + trade.sell_order_status = reason # TODO-lev: Update to exit_order_status profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested profit_trade = trade.calc_profit(rate=profit_rate) @@ -1224,7 +1240,7 @@ class FreqtradeBot(LoggingMixin): 'current_rate': current_rate, 'profit_amount': profit_trade, 'profit_ratio': profit_ratio, - 'sell_reason': trade.sell_reason, + 'sell_reason': trade.sell_reason, # TODO-lev: trade to exit_reason 'open_date': trade.open_date, 'close_date': trade.close_date, 'stake_currency': self.config['stake_currency'], @@ -1310,6 +1326,7 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() if fee_abs != 0 and self.wallets.get_free(trade_base_currency) >= amount: # Eat into dust if we own more than base currency + # TODO-lev: won't be in "base"(quote) currency for shorts logger.info(f"Fee amount for {trade} was in base currency - " f"Eating Fee {fee_abs} into dust.") elif fee_abs != 0: @@ -1386,6 +1403,7 @@ class FreqtradeBot(LoggingMixin): trade.update_fee(fee_cost, fee_currency, fee_rate, order.get('side', '')) if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC): + # TODO-lev: leverage? logger.warning(f"Amount {amount} does not match amount {trade.amount}") raise DependencyException("Half bought? Amounts don't match") diff --git a/freqtrade/optimize/backtesting.py b/freqtrade/optimize/backtesting.py index 99d4c60d0..084142646 100644 --- a/freqtrade/optimize/backtesting.py +++ b/freqtrade/optimize/backtesting.py @@ -386,7 +386,7 @@ class Backtesting: detail_data = detail_data.loc[ (detail_data['date'] >= sell_candle_time) & (detail_data['date'] < sell_candle_end) - ] + ] if len(detail_data) == 0: # Fall back to "regular" data if no detail data was found for this candle return self._get_sell_trade_entry_for_candle(trade, sell_row) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index b73611c1b..a57cf0821 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -549,7 +549,7 @@ class LocalTrade(): if self.is_open: payment = "BUY" if self.is_short else "SELL" # TODO-lev: On shorts, you buy a little bit more than the amount (amount + interest) - # This wll only print the original amount + # TODO-lev: This wll only print the original amount logger.info(f'{order_type.upper()}_{payment} has been fulfilled for {self}.') # TODO-lev: Double check this self.close(safe_value_fallback(order, 'average', 'price')) diff --git a/freqtrade/plugins/pairlistmanager.py b/freqtrade/plugins/pairlistmanager.py index face79729..93b5e90e2 100644 --- a/freqtrade/plugins/pairlistmanager.py +++ b/freqtrade/plugins/pairlistmanager.py @@ -127,7 +127,7 @@ class PairListManager(): :return: pairlist - whitelisted pairs """ try: - + # TODO-lev: 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/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 95a37452b..8c57237ec 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -36,6 +36,7 @@ class RPCException(Exception): raise RPCException('*Status:* `no active trade`') """ + # TODO-lev: Add new configuration options introduced with leveraged/short trading def __init__(self, message: str) -> None: super().__init__(self) diff --git a/tests/test_persistence.py b/tests/test_persistence.py index 911d7d6c2..1250e7b92 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -90,7 +90,7 @@ def test_enter_exit_side(fee): @pytest.mark.usefixtures("init_persistence") -def test__set_stop_loss_isolated_liq(fee): +def test_set_stop_loss_isolated_liq(fee): trade = Trade( id=2, pair='ADA/USDT',