diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index 98a5329ba..275a2f949 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -282,6 +282,20 @@ class LocalTrade(): return self.close_date.replace(tzinfo=timezone.utc) def to_json(self) -> Dict[str, Any]: + fill_buy = self.select_filled_orders('buy') + buys_json = dict() + if len(fill_buy) > 0: + for x in range(len(fill_buy)): + buy = dict( + cost=fill_buy[x].cost if fill_buy[x].cost else 0.0, + amount=fill_buy[x].amount, + price=fill_buy[x].price, + average=round(fill_buy[x].average, 8) if fill_buy[x].average else 0.0, + order_filled_date=fill_buy[x].order_filled_date.strftime(DATETIME_PRINT_FORMAT) + if fill_buy[x].order_filled_date else None + ) + buys_json[str(x)] = buy + return { 'trade_id': self.id, 'pair': self.pair, @@ -345,6 +359,7 @@ class LocalTrade(): 'max_rate': self.max_rate, 'open_order_id': self.open_order_id, + 'filled_buys': buys_json, } @staticmethod diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index 41fd37e51..bd6373cce 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -208,6 +208,8 @@ class RPC: order['type'], order['side'], order['remaining'] ) if order else None, )) + cp_cfg = self._config + trade_dict['position_adjustment_enable'] = cp_cfg['position_adjustment_enable'] results.append(trade_dict) return results @@ -242,7 +244,7 @@ class RPC: 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 ''), + + ('**' if (trade.close_rate_requested is not None) 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 716694a81..03eb836fa 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -369,6 +369,47 @@ class Telegram(RPCHandler): else: return "\N{CROSS MARK}" + def _prepare_buy_details(self, filled_trades, base_currency): + """ + Prepare details of trade with buy adjustment enabled + """ + lines = [] + for x in range(len(filled_trades)): + cur_buy_date = arrow.get(filled_trades[str(x)]["order_filled_date"]) + cur_buy_amount = filled_trades[str(x)]["amount"] + cur_buy_average = filled_trades[str(x)]["average"] + lines.append(" ") + if x == 0: + lines.append("*Buy #{}:*".format(x+1)) + lines.append("*Buy Amount:* {} ({:.8f} {})" + .format(cur_buy_amount, filled_trades[str(x)]["cost"], base_currency)) + lines.append("*Average Buy Price:* {}".format(cur_buy_average)) + else: + sumA = 0 + sumB = 0 + for y in range(x): + sumA += (filled_trades[str(y)]["amount"] * filled_trades[str(y)]["average"]) + sumB += filled_trades[str(y)]["amount"] + prev_avg_price = sumA/sumB + price_to_1st_buy = (cur_buy_average - filled_trades["0"]["average"]) \ + / filled_trades["0"]["average"] + minus_on_buy = (cur_buy_average - prev_avg_price)/prev_avg_price + dur_buys = cur_buy_date - arrow.get(filled_trades[str(x-1)]["order_filled_date"]) + days = dur_buys.days + hours, remainder = divmod(dur_buys.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + lines.append("*Buy #{}:* at {:.2%} avg profit".format(x+1, minus_on_buy)) + lines.append("({})".format(cur_buy_date + .humanize(granularity=["day", "hour", "minute"]))) + lines.append("*Buy Amount:* {} ({:.8f} {})" + .format(cur_buy_amount, filled_trades[str(x)]["cost"], base_currency)) + lines.append("*Average Buy Price:* {} ({:.2%} from 1st buy rate)" + .format(cur_buy_average, price_to_1st_buy)) + lines.append("*Filled at:* {}".format(filled_trades[str(x)]["order_filled_date"])) + lines.append("({}d {}h {}m {}s from previous buy)" + .format(days, hours, minutes, seconds)) + return lines + @authorized_only def _status(self, update: Update, context: CallbackContext) -> None: """ @@ -396,17 +437,31 @@ class Telegram(RPCHandler): messages = [] for r in results: r['open_date_hum'] = arrow.get(r['open_date']).humanize() + r['filled_buys'] = r.get('filled_buys', []) + r['num_buys'] = len(r['filled_buys']) + r['sell_reason'] = r.get('sell_reason', "") + r['position_adjustment_enable'] = r.get('position_adjustment_enable', False) lines = [ "*Trade ID:* `{trade_id}` `(since {open_date_hum})`", "*Current Pair:* {pair}", "*Amount:* `{amount} ({stake_amount} {base_currency})`", "*Buy Tag:* `{buy_tag}`" if r['buy_tag'] else "", + "*Sell Reason:* `{sell_reason}`" if r['sell_reason'] else "", + ] + + if r['position_adjustment_enable']: + lines.append("*Number of Buy(s):* `{num_buys}`") + + lines.extend([ "*Open Rate:* `{open_rate:.8f}`", - "*Close Rate:* `{close_rate}`" if r['close_rate'] else "", + "*Close Rate:* `{close_rate:.8f}`" if r['close_rate'] else "", + "*Open Date:* `{open_date}`", + "*Close Date:* `{close_date}`" if r['close_date'] else "", "*Current Rate:* `{current_rate:.8f}`", ("*Current Profit:* " if r['is_open'] else "*Close Profit: *") + "`{profit_ratio:.2%}`", - ] + ]) + if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] and r['initial_stop_loss_ratio'] is not None): # Adding initial stoploss only if it is different from stoploss @@ -424,6 +479,10 @@ class Telegram(RPCHandler): else: lines.append("*Open Order:* `{open_order}`") + if len(r['filled_buys']) > 1: + lines_detail = self._prepare_buy_details(r['filled_buys'], r['base_currency']) + lines.extend(lines_detail) + # Filter empty lines using list-comprehension messages.append("\n".join([line for line in lines if line]).format(**r)) diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index e86022a91..0ea147c0a 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -108,6 +108,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', + 'position_adjustment_enable': False, + 'filled_buys': {'0': {'amount': 91.07468123, 'average': 1.098e-05, + 'cost': 0.0009999999999054, 'order_filled_date': ANY, 'price': 1.098e-05}}, } mocker.patch('freqtrade.exchange.Exchange.get_rate', @@ -175,6 +178,9 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10448878, 'open_order': None, 'exchange': 'binance', + 'position_adjustment_enable': False, + 'filled_buys': {'0': {'amount': 91.07468123, 'average': 1.098e-05, + 'cost': 0.0009999999999054, 'order_filled_date': ANY, 'price': 1.098e-05}}, } diff --git a/tests/test_persistence.py b/tests/test_persistence.py index d98238f6f..d6b12ea8c 100644 --- a/tests/test_persistence.py +++ b/tests/test_persistence.py @@ -903,6 +903,7 @@ def test_to_json(default_conf, fee): 'buy_tag': None, 'timeframe': None, 'exchange': 'binance', + 'filled_buys': {} } # Simulate dry_run entries @@ -970,6 +971,7 @@ def test_to_json(default_conf, fee): 'buy_tag': 'buys_signal_001', 'timeframe': None, 'exchange': 'binance', + 'filled_buys': {} }