From ec8e0b1fcb8eec8e30c5f0fc1fe1acb9d7201c1f Mon Sep 17 00:00:00 2001 From: Kavinkumar <33546454+mkavinkumar1@users.noreply.github.com> Date: Thu, 24 Feb 2022 11:48:52 +0530 Subject: [PATCH] telegram message fix --- freqtrade/freqtradebot.py | 32 ++-- freqtrade/persistence/models.py | 40 +++-- freqtrade/rpc/rpc.py | 24 ++- freqtrade/rpc/telegram.py | 11 +- tests/rpc/test_rpc_telegram.py | 16 +- tests/test_freqtradebot.py | 280 ++++++++++++++++++++++++++++++++ 6 files changed, 359 insertions(+), 44 deletions(-) diff --git a/freqtrade/freqtradebot.py b/freqtrade/freqtradebot.py index d6460f144..c6101cbbe 100644 --- a/freqtrade/freqtradebot.py +++ b/freqtrade/freqtradebot.py @@ -1238,16 +1238,26 @@ class FreqtradeBot(LoggingMixin): return True def _notify_exit(self, trade: Trade, order_type: str, fill: bool = False, - sub_trade: bool = False) -> None: + sub_trade: bool = False, order: Dict = None) -> None: """ Sends rpc notification when a sell occurred. """ - profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested - profit_trade = trade.calc_profit(rate=profit_rate) # Use cached rates here - it was updated seconds ago. current_rate = self.exchange.get_rate( trade.pair, refresh=False, side="sell") if not fill else None - profit_ratio = trade.calc_profit_ratio(profit_rate) + if order: + profit_rate = safe_value_fallback(order, 'average', 'price') + amount = safe_value_fallback(order, 'filled', 'amount') + profit = trade.process_sell_sub_trade(order, is_closed=False) + open_rate = trade.get_open_rate(profit, profit_rate, amount) + open_cost=open_rate * amount * (1+ trade.fee_open) + profit_ratio = ( open_cost + profit)/open_cost - 1 + else: + profit_rate = trade.close_rate if trade.close_rate else trade.close_rate_requested + profit = trade.calc_profit(rate=profit_rate) + profit_ratio = trade.calc_profit_ratio(profit_rate) + amount = trade.amount + open_rate = trade.open_rate gain = "profit" if profit_ratio > 0 else "loss" msg = { @@ -1259,11 +1269,11 @@ class FreqtradeBot(LoggingMixin): 'gain': gain, 'limit': profit_rate, 'order_type': order_type, - 'amount': trade.amount, - 'open_rate': trade.open_rate, + 'amount': amount, + 'open_rate': open_rate, 'close_rate': trade.close_rate, 'current_rate': current_rate, - 'profit_amount': profit_trade, + 'profit_amount': profit, 'profit_ratio': profit_ratio, 'buy_tag': trade.buy_tag, 'sell_reason': trade.sell_reason, @@ -1275,10 +1285,7 @@ class FreqtradeBot(LoggingMixin): 'sub_trade': sub_trade, } - if 'fiat_display_currency' in self.config: - msg.update({ - 'fiat_currency': self.config['fiat_display_currency'], - }) + # Send the message self.rpc.send_msg(msg) @@ -1382,9 +1389,10 @@ class FreqtradeBot(LoggingMixin): self.wallets.update() if not trade.is_open: + self.handle_protections(trade.pair) + if order.get('side', None) == 'sell': if send_msg and not stoploss_order and not trade.open_order_id: self._notify_exit(trade, '', True, sub_trade=sub_trade) - self.handle_protections(trade.pair) elif send_msg and not trade.open_order_id: # Buy fill self._notify_enter(trade, order, fill=True, sub_trade=sub_trade) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 42dfb8ac5..26dc3ec42 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -495,10 +495,16 @@ class LocalTrade(): raise ValueError(f'Unknown order type: {order_type}') Trade.commit() - def process_sell_sub_trade(self, order: Dict) -> None: + def process_sell_sub_trade(self, order: Dict, is_closed: bool = True) -> float: orders = (self.select_filled_orders('buy')) - sell_rate = float(safe_value_fallback(order, 'average', 'price')) + # is_closed = order['ft_is_open'] sell_amount = float(safe_value_fallback(order, 'filled', 'amount')) + if is_closed: + if sell_amount == self.amount: + self.close(safe_value_fallback(order, 'average', 'price')) + Trade.commit() + return + sell_rate = float(safe_value_fallback(order, 'average', 'price')) profit = 0.0 idx = -1 while sell_amount: @@ -507,25 +513,25 @@ class LocalTrade(): buy_rate = b_order.average or b_order.price if sell_amount < buy_amount: amount = sell_amount - b_order.filled -= amount + if is_closed: + b_order.filled -= amount else: - if sell_amount == self.amount: - self.close(safe_value_fallback(order, 'average', 'price')) - Trade.commit() - return - b_order.is_fully_realized = True - self.update_order(b_order) + if is_closed: + b_order.is_fully_realized = True + self.update_order(b_order) idx -= 1 amount = buy_amount sell_amount -= amount profit += self.calc_profit2(buy_rate, sell_rate, amount) b_order2 = orders[idx] amount2 = b_order2.filled or b_order2.amount - b_order2.average = (b_order2.average * amount2 - profit) / amount2 - self.update_order(b_order2) - Order.query.session.commit() - self.recalc_trade_from_orders() - Trade.commit() + if is_closed : + b_order2.average = (b_order2.average * amount2 - profit) / amount2 + self.update_order(b_order2) + Order.query.session.commit() + self.recalc_trade_from_orders() + Trade.commit() + return profit def calc_profit2(self, open_rate: float, close_rate: float, amount: float) -> float: @@ -533,6 +539,12 @@ class LocalTrade(): (Decimal(1 - self.fee_close) * Decimal(close_rate) - Decimal(1 + self.fee_open) * Decimal(open_rate))) + def get_open_rate(self, profit: float, close_rate: float, + amount: float) -> float: + return float(Decimal(amount) * + (Decimal(1 - self.fee_close) * Decimal(close_rate)) - + profit)/(Decimal(amount) * Decimal(1 + self.fee_open)) + def close(self, rate: float, *, show_msg: bool = True) -> None: """ Sets close_rate to the given rate, calculates total profit diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 5912a0ecd..5cf0359fd 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -218,7 +218,7 @@ class RPC: return results def _rpc_status_table(self, stake_currency: str, - fiat_display_currency: str) -> Tuple[List, List, float]: + fiat_display_currency: str, show_order: bool = False) -> Tuple[List, List, float]: trades = Trade.get_open_trades() if not trades: raise RPCException('no active trade') @@ -232,8 +232,17 @@ class RPC: trade.pair, refresh=False, side="sell") except (PricingError, ExchangeError): current_rate = NAN - trade_profit = trade.calc_profit(current_rate) - profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' + if show_order: + b_order = trade.select_order('buy', is_open=False) + amount = b_order.filled or b_order.amount + open_rate = b_order.average or b_order.price + open_cost=open_rate * amount * (1+ trade.fee_open) + trade_profit = trade.calc_profit2(open_rate, current_rate, amount) + profit_pct = ( open_cost + profit)/open_cost - 1 + profit_str = f'{profit_pct:.2%}' + else: + trade_profit = trade.calc_profit(current_rate) + profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}' if self._fiat_converter: fiat_profit = self._fiat_converter.convert_amount( trade_profit, @@ -244,11 +253,14 @@ class RPC: profit_str += f" ({fiat_profit:.2f})" fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \ else fiat_profit_sum + fiat_profit + last_sell_order = trade.select_order('sell') + last_sell_order_id = last_sell_order.order_id if last_sell_order else None detail_trade = [ trade.id, - trade.pair + ('*' if (trade.open_order_id is not None - and trade.close_rate_requested is None) else '') - + ('**' if (trade.close_rate_requested is not None) else ''), + trade.pair + ('*' if (trade.open_order_id == + trade.select_order('buy').order_id) else '') + + ('**' if (trade.open_order_id == + last_sell_order_id) else ''), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), profit_str ] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index ecbacf1e0..4aa401bb3 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -291,7 +291,6 @@ class Telegram(RPCHandler): f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" f"*Buy Tag:* `{msg['buy_tag']}`\n" f"*Sell Reason:* `{msg['sell_reason']}`\n" - f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n" f"*Amount:* `{msg['amount']:.8f}`\n" f"*Open Rate:* `{msg['open_rate']:.8f}`\n") @@ -314,6 +313,8 @@ class Telegram(RPCHandler): message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}" message += ")`" + else: + message += f"\n*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`" return message def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str: @@ -453,7 +454,9 @@ class Telegram(RPCHandler): if context.args and 'table' in context.args: self._status_table(update, context) return - + if context.args and 'order' in context.args: + self._status_table(update, context, show_order=True) + return try: # Check if there's at least one numerical ID provided. @@ -525,7 +528,7 @@ class Telegram(RPCHandler): self._send_msg(str(e)) @authorized_only - def _status_table(self, update: Update, context: CallbackContext) -> None: + def _status_table(self, update: Update, context: CallbackContext, show_order: bool = False) -> None: """ Handler for /status table. Returns the current TradeThread status in table format @@ -536,7 +539,7 @@ class Telegram(RPCHandler): try: fiat_currency = self._config.get('fiat_display_currency', '') statlist, head, fiat_profit_sum = self._rpc._rpc_status_table( - self._config['stake_currency'], fiat_currency) + self._config['stake_currency'], fiat_currency, show_order) show_total = not isnan(fiat_profit_sum) and len(statlist) > 1 max_trades_per_msg = 50 diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index eaff9891a..3d8b8903c 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -1879,11 +1879,11 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Unrealized Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1:00:00 (60.0 min)`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Close Rate:* `0.00003201`\n' + '*Duration:* `1:00:00 (60.0 min)`' ) msg_mock.reset_mock() @@ -1911,11 +1911,11 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: '*Unrealized Profit:* `-57.41%`\n' '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Close Rate:* `0.00003201`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`' ) # Reset singleton function to avoid random breaks telegram._rpc._fiat_converter.convert_amount = old_convamount @@ -1982,10 +1982,10 @@ def test_send_msg_sell_fill_notification(default_conf, mocker) -> None: '*Profit:* `-57.41%`\n' '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `1 day, 2:30:00 (1590.0 min)`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' - '*Close Rate:* `0.00003201`' + '*Close Rate:* `0.00003201`\n' + '*Duration:* `1 day, 2:30:00 (1590.0 min)`' ) @@ -2081,11 +2081,11 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None: '*Unrealized Profit:* `-57.41%`\n' '*Buy Tag:* `buy_signal1`\n' '*Sell Reason:* `stop_loss`\n' - '*Duration:* `2:35:03 (155.1 min)`\n' '*Amount:* `1333.33333333`\n' '*Open Rate:* `0.00007500`\n' '*Current Rate:* `0.00003201`\n' - '*Close Rate:* `0.00003201`' + '*Close Rate:* `0.00003201`\n' + '*Duration:* `2:35:03 (155.1 min)`' ) diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py index bdb56bac8..461cad2cc 100644 --- a/tests/test_freqtradebot.py +++ b/tests/test_freqtradebot.py @@ -4559,6 +4559,8 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: trade = Trade.query.first() assert trade assert trade.open_order_id is None + assert trade.is_open + print(trade.is_open) assert trade.amount == 22 assert trade.stake_amount == 203.5625 assert pytest.approx(trade.open_rate) == 9.252840909090908 @@ -4570,7 +4572,285 @@ def test_position_adjust(mocker, default_conf_usdt, fee) -> None: # Make sure the closed order is found as the second order. order = trade.select_order('sell', False) assert order.order_id == '653' + +def test_position_adjust2(mocker, default_conf_usdt, fee) -> None: + patch_RPCManager(mocker) + patch_exchange(mocker) + patch_wallet(mocker, free=10000) + default_conf_usdt.update({ + "position_adjustment_enable": True, + "dry_run": False, + "stake_amount": 10.0, + "dry_run_wallet": 1000.0, + }) + freqtrade = FreqtradeBot(default_conf_usdt) + freqtrade.strategy.confirm_trade_entry = MagicMock(return_value=True) + bid = 11 + stake_amount = 10 + buy_rate_mock = MagicMock(return_value=bid) + mocker.patch.multiple( + 'freqtrade.exchange.Exchange', + get_rate=buy_rate_mock, + fetch_ticker=MagicMock(return_value={ + 'bid': 10, + 'ask': 12, + 'last': 11 + }), + get_min_pair_stake_amount=MagicMock(return_value=1), + get_fee=fee, + ) + pair = 'ETH/USDT' + # Initial buy + closed_successful_buy_order = { + 'pair': pair, + 'ft_pair': pair, + 'ft_order_side': 'buy', + 'side': 'buy', + 'type': 'limit', + 'status': 'closed', + 'price': bid, + 'average': bid, + 'cost': bid * stake_amount, + 'amount': stake_amount, + 'filled': stake_amount, + 'ft_is_open': False, + 'id': '650', + 'order_id': '650' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_successful_buy_order)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_successful_buy_order)) + assert freqtrade.execute_entry(pair, stake_amount) + # Should create an closed trade with an no open order id + # Order is filled and trade is open + orders = Order.query.all() + assert orders + assert len(orders) == 1 + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == 11 + assert trade.stake_amount == 110 + # Assume it does nothing since order is closed and trade is open + freqtrade.update_closed_trades_without_assigned_fees() + + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == 11 + assert trade.stake_amount == 110 + assert not trade.fee_updated('buy') + + freqtrade.check_handle_timedout() + + trade = Trade.query.first() + assert trade + assert trade.is_open is True + assert trade.open_order_id is None + assert trade.open_rate == 11 + assert trade.stake_amount == 110 + assert not trade.fee_updated('buy') + + # First position adjustment buy. + open_dca_order_1 = { + 'ft_pair': pair, + 'ft_order_side': 'buy', + 'side': 'buy', + 'type': 'limit', + 'status': None, + 'price': 9, + 'amount': 12, + 'cost': 108, + 'ft_is_open': True, + 'id': '651', + 'order_id': '651' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=open_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=open_dca_order_1)) + assert freqtrade.execute_entry(pair, stake_amount, trade=trade) + + orders = Order.query.all() + assert orders + assert len(orders) == 2 + trade = Trade.query.first() + assert trade + assert trade.open_order_id == '651' + assert trade.open_rate == 11 + assert trade.amount == 10 + assert trade.stake_amount == 110 + assert not trade.fee_updated('buy') + trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() + assert len(trades) == 1 + assert trade.is_open + assert not trade.fee_updated('buy') + order = trade.select_order('buy', False) + assert order + assert order.order_id == '650' + + def make_sure_its_651(*args, **kwargs): + + if args[0] == '650': + return closed_successful_buy_order + if args[0] == '651': + return open_dca_order_1 + return None + + # Assume it does nothing since order is still open + fetch_order_mm = MagicMock(side_effect=make_sure_its_651) + mocker.patch('freqtrade.exchange.Exchange.create_order', fetch_order_mm) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', fetch_order_mm) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', fetch_order_mm) + freqtrade.update_closed_trades_without_assigned_fees() + + orders = Order.query.all() + assert orders + assert len(orders) == 2 + # Assert that the trade is found as open and without fees + trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() + assert len(trades) == 1 + # Assert trade is as expected + trade = Trade.query.first() + assert trade + assert trade.open_order_id == '651' + assert trade.open_rate == 11 + assert trade.amount == 10 + assert trade.stake_amount == 110 + assert not trade.fee_updated('buy') + + # Make sure the closed order is found as the first order. + order = trade.select_order('buy', False) + assert order.order_id == '650' + + # Now close the order so it should update. + closed_dca_order_1 = { + 'ft_pair': pair, + 'ft_order_side': 'buy', + 'side': 'buy', + 'type': 'limit', + 'status': 'closed', + 'price': 9, + 'average': 9, + 'amount': 12, + 'filled': 12, + 'cost': 108, + 'ft_is_open': False, + 'id': '651', + 'order_id': '651' + } + + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_dca_order_1)) + freqtrade.check_handle_timedout() + + # Assert trade is as expected (averaged dca) + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert pytest.approx(trade.open_rate) == 9.90909090909 + assert trade.amount == 22 + assert trade.stake_amount == 218 + + orders = Order.query.all() + assert orders + assert len(orders) == 2 + + # Make sure the closed order is found as the second order. + order = trade.select_order('buy', False) + assert order.order_id == '651' + + # Assert that the trade is not found as open and without fees + trades: List[Trade] = Trade.get_open_trades_without_assigned_fees() + assert len(trades) == 1 + + # Add a second DCA + closed_dca_order_2 = { + 'ft_pair': pair, + 'status': 'closed', + 'ft_order_side': 'buy', + 'side': 'buy', + 'type': 'limit', + 'price': 7, + 'average': 7, + 'amount': 15, + 'filled': 15, + 'cost': 105, + 'ft_is_open': False, + 'id': '652', + 'order_id': '652' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_dca_order_2)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_dca_order_2)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_dca_order_2)) + assert freqtrade.execute_entry(pair, stake_amount, trade=trade) + + # Assert trade is as expected (averaged dca) + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert pytest.approx(trade.open_rate) == 8.729729729729 + assert trade.amount == 37 + assert trade.stake_amount == 323 + + orders = Order.query.all() + assert orders + assert len(orders) == 3 + + # Make sure the closed order is found as the second order. + order = trade.select_order('buy', False) + assert order.order_id == '652' + closed_sell_dca_order_1 = { + 'ft_pair': pair, + 'status': 'closed', + 'ft_order_side': 'sell', + 'side': 'sell', + 'type': 'limit', + 'price': 8, + 'average': 8, + 'amount': 15, + 'filled': 15, + 'cost': 120, + 'ft_is_open': False, + 'id': '653', + 'order_id': '653' + } + mocker.patch('freqtrade.exchange.Exchange.create_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order', + MagicMock(return_value=closed_sell_dca_order_1)) + mocker.patch('freqtrade.exchange.Exchange.fetch_order_or_stoploss_order', + MagicMock(return_value=closed_sell_dca_order_1)) + assert freqtrade.execute_trade_exit(trade=trade, limit=8, + sell_reason=SellCheckTuple(sell_type=SellType.STOP_LOSS), + sub_trade_amt=15) + + # Assert trade is as expected (averaged dca) + trade = Trade.query.first() + assert trade + assert trade.open_order_id is None + assert trade.amount == 22 + assert trade.stake_amount == 203.5625 + assert pytest.approx(trade.open_rate) == 9.252840909090908 + + orders = Order.query.all() + assert orders + assert len(orders) == 4 + + # Make sure the closed order is found as the second order. + order = trade.select_order('sell', False) + assert order.order_id == '653' def test_process_open_trade_positions_exception(mocker, default_conf_usdt, fee, caplog) -> None: default_conf_usdt.update({