diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index c565b891f..6f841b608 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -27,11 +27,10 @@ class Bybit(Exchange): """ _ft_has: Dict = { - "ohlcv_candle_limit": 1000, + "ohlcv_candle_limit": 200, "ohlcv_has_history": False, } _ft_has_futures: Dict = { - "ohlcv_candle_limit": 200, "ohlcv_has_history": True, "mark_ohlcv_timeframe": "4h", "funding_fee_timeframe": "8h", diff --git a/freqtrade/persistence/trade_model.py b/freqtrade/persistence/trade_model.py index 0ae5fba25..21fe80819 100644 --- a/freqtrade/persistence/trade_model.py +++ b/freqtrade/persistence/trade_model.py @@ -120,8 +120,9 @@ class Order(ModelBase): def __repr__(self): - return (f'Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, ' - f'side={self.side}, order_type={self.order_type}, status={self.status})') + return (f"Order(id={self.id}, order_id={self.order_id}, trade_id={self.ft_trade_id}, " + f"side={self.side}, filled={self.safe_filled}, price={self.safe_price}, " + f"order_type={self.order_type}, status={self.status})") def update_from_ccxt_object(self, order): """ @@ -518,6 +519,8 @@ class LocalTrade(): 'close_timestamp': int(self.close_date.replace( tzinfo=timezone.utc).timestamp() * 1000) if self.close_date else None, 'realized_profit': self.realized_profit or 0.0, + # Close-profit corresponds to relative realized_profit ratio + 'realized_profit_ratio': self.close_profit or None, 'close_rate': self.close_rate, 'close_rate_requested': self.close_rate_requested, 'close_profit': self.close_profit, # Deprecated diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index 562c9aa7d..a751179b2 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -250,6 +250,7 @@ class TradeSchema(BaseModel): profit_fiat: Optional[float] realized_profit: float + realized_profit_ratio: Optional[float] exit_reason: Optional[str] exit_order_status: Optional[str] diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 6f82a7316..1a96b1671 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -321,31 +321,33 @@ class Telegram(RPCHandler): and self._rpc._fiat_converter): msg['profit_fiat'] = self._rpc._fiat_converter.convert_amount( msg['profit_amount'], msg['stake_currency'], msg['fiat_currency']) - msg['profit_extra'] = ( - f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}") + msg['profit_extra'] = f" / {msg['profit_fiat']:.3f} {msg['fiat_currency']}" else: msg['profit_extra'] = '' msg['profit_extra'] = ( f" ({msg['gain']}: {msg['profit_amount']:.8f} {msg['stake_currency']}" f"{msg['profit_extra']})") + is_fill = msg['type'] == RPCMessageType.EXIT_FILL is_sub_trade = msg.get('sub_trade') is_sub_profit = msg['profit_amount'] != msg.get('cumulative_profit') - profit_prefix = ('Sub ' if is_sub_profit - else 'Cumulative ') if is_sub_trade else '' + profit_prefix = ('Sub ' if is_sub_profit else 'Cumulative ') if is_sub_trade else '' cp_extra = '' + exit_wording = 'Exited' if is_fill else 'Exiting' if is_sub_profit and is_sub_trade: if self._rpc._fiat_converter: cp_fiat = self._rpc._fiat_converter.convert_amount( msg['cumulative_profit'], msg['stake_currency'], msg['fiat_currency']) cp_extra = f" / {cp_fiat:.3f} {msg['fiat_currency']}" - else: - cp_extra = '' - cp_extra = f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " \ - f"{msg['stake_currency']}{cp_extra}`)\n" + exit_wording = f"Partially {exit_wording.lower()}" + cp_extra = ( + f"*Cumulative Profit:* (`{msg['cumulative_profit']:.8f} " + f"{msg['stake_currency']}{cp_extra}`)\n" + ) + message = ( f"{msg['emoji']} *{self._exchange_from_msg(msg)}:* " - f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n" + f"{exit_wording} {msg['pair']} (#{msg['trade_id']})\n" f"{self._add_analyzed_candle(msg['pair'])}" f"*{f'{profit_prefix}Profit' if is_fill else f'Unrealized {profit_prefix}Profit'}:* " f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n" @@ -364,7 +366,7 @@ class Telegram(RPCHandler): elif msg['type'] == RPCMessageType.EXIT_FILL: message += f"*Exit Rate:* `{msg['close_rate']:.8f}`" - if msg.get('sub_trade'): + if is_sub_trade: if self._rpc._fiat_converter: msg['stake_amount_fiat'] = self._rpc._fiat_converter.convert_amount( msg['stake_amount'], msg['stake_currency'], msg['fiat_currency']) @@ -486,7 +488,9 @@ class Telegram(RPCHandler): if order_nr == 1: lines.append(f"*{wording} #{order_nr}:*") lines.append( - f"*Amount:* {cur_entry_amount} ({order['cost']:.8f} {quote_currency})") + f"*Amount:* {cur_entry_amount} " + f"({round_coin_value(order['cost'], quote_currency)})" + ) lines.append(f"*Average Price:* {cur_entry_average}") else: sum_stake = 0 @@ -582,7 +586,7 @@ class Telegram(RPCHandler): if position_adjust: max_buy_str = (f"/{max_entries + 1}" if (max_entries > 0) else "") - lines.append("*Number of Entries:* `{num_entries}`" + max_buy_str) + lines.append("*Number of Entries:* `{num_entries}" + max_buy_str + "`") lines.append("*Number of Exits:* `{num_exits}`") lines.extend([ @@ -597,7 +601,8 @@ class Telegram(RPCHandler): if r['is_open']: if r.get('realized_profit'): - lines.append("*Realized Profit:* `{realized_profit_r}`") + lines.append( + "*Realized Profit:* `{realized_profit_r} {realized_profit_ratio:.2%}`") lines.append("*Total Profit:* `{total_profit_abs_r}` ") if (r['stop_loss_abs'] != r['initial_stop_loss_abs'] diff --git a/tests/exchange/test_ccxt_compat.py b/tests/exchange/test_ccxt_compat.py index f06a53308..872cf5059 100644 --- a/tests/exchange/test_ccxt_compat.py +++ b/tests/exchange/test_ccxt_compat.py @@ -463,7 +463,9 @@ class TestCCXTExchange(): if exchangename == 'gate': # TODO: Gate is unstable here at the moment, ignoring the limit partially. return - for val in [1, 2, 5, 25, 100]: + for val in [1, 2, 5, 25, 50, 100]: + if val > 50 and exchangename == 'bybit': + continue l2 = exch.fetch_l2_order_book(pair, val) if not l2_limit_range or val in l2_limit_range: if val > 50: diff --git a/tests/persistence/test_persistence.py b/tests/persistence/test_persistence.py index 6d907ccf0..0598d4134 100644 --- a/tests/persistence/test_persistence.py +++ b/tests/persistence/test_persistence.py @@ -1362,6 +1362,7 @@ def test_to_json(fee): 'trade_duration': None, 'trade_duration_s': None, 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, @@ -1438,6 +1439,7 @@ def test_to_json(fee): 'initial_stop_loss_pct': None, 'initial_stop_loss_ratio': None, 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'close_profit': None, 'close_profit_pct': None, 'close_profit_abs': None, diff --git a/tests/rpc/test_rpc.py b/tests/rpc/test_rpc.py index d368107df..cd72da763 100644 --- a/tests/rpc/test_rpc.py +++ b/tests/rpc/test_rpc.py @@ -76,6 +76,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None: 'stoploss_entry_dist_ratio': -0.10376381, 'open_order': None, 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'total_profit_abs': -4.09e-06, 'total_profit_fiat': ANY, 'exchange': 'binance', diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index f898dd476..e140a43f1 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -1013,6 +1013,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets, is_short, 'total_profit_abs': ANY, 'total_profit_fiat': ANY, 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'current_rate': current_rate, 'open_date': ANY, 'open_timestamp': ANY, @@ -1243,6 +1244,7 @@ def test_api_force_entry(botclient, mocker, fee, endpoint): 'profit_abs': None, 'profit_fiat': None, 'realized_profit': 0.0, + 'realized_profit_ratio': None, 'fee_close': 0.0025, 'fee_close_cost': None, 'fee_close_currency': None, diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 26cb93821..69d0f805d 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -2012,7 +2012,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None: 'sub_trade': True, }) assert msg_mock.call_args[0][0] == ( - '\N{WARNING SIGN} *Binance (dry):* Exiting KEY/ETH (#1)\n' + '\N{WARNING SIGN} *Binance (dry):* Partially exiting KEY/ETH (#1)\n' '*Unrealized Sub Profit:* `-57.41% (loss: -0.05746268 ETH / -24.812 USD)`\n' '*Cumulative Profit:* (`-0.15746268 ETH / -24.812 USD`)\n' '*Enter Tag:* `buy_signal1`\n' diff --git a/tests/strategy/test_strategy_helpers.py b/tests/strategy/test_strategy_helpers.py index 36e997f7b..cb79ac171 100644 --- a/tests/strategy/test_strategy_helpers.py +++ b/tests/strategy/test_strategy_helpers.py @@ -169,6 +169,36 @@ def test_stoploss_from_open(side, profitrange): assert pytest.approx(stop_price) == expected_stop_price +@pytest.mark.parametrize("side,rel_stop,curr_profit,leverage,expected", [ + # profit range for long is [-1, inf] while for shorts is [-inf, 1] + ("long", 0, -1, 1, 1), + ("long", 0, 0.1, 1, 0.09090909), + ("long", -0.1, 0.1, 1, 0.18181818), + ("long", 0.1, 0.2, 1, 0.08333333), + ("long", 0.1, 0.5, 1, 0.266666666), + ("long", 0.1, 5, 1, 0.816666666), # 500% profit, set stoploss to 10% above open price + + ("short", 0, 0.1, 1, 0.1111111), + ("short", -0.1, 0.1, 1, 0.2222222), + ("short", 0.1, 0.2, 1, 0.125), + ("short", 0.1, 1, 1, 1), +]) +def test_stoploss_from_open_leverage(side, rel_stop, curr_profit, leverage, expected): + + stoploss = stoploss_from_open(rel_stop, curr_profit, side == 'short') + assert pytest.approx(stoploss) == expected + open_rate = 100 + if stoploss != 1: + if side == 'long': + current_rate = open_rate * (1 + curr_profit) + stop = current_rate * (1 - stoploss) + assert pytest.approx(stop) == open_rate * (1 + rel_stop) + else: + current_rate = open_rate * (1 - curr_profit) + stop = current_rate * (1 + stoploss) + assert pytest.approx(stop) == open_rate * (1 - rel_stop) + + def test_stoploss_from_absolute(): assert pytest.approx(stoploss_from_absolute(90, 100)) == 1 - (90 / 100) assert pytest.approx(stoploss_from_absolute(90, 100)) == 0.1