diff --git a/freqtrade/rpc/api_server/api_schemas.py b/freqtrade/rpc/api_server/api_schemas.py index ff1915fca..113e93b5a 100644 --- a/freqtrade/rpc/api_server/api_schemas.py +++ b/freqtrade/rpc/api_server/api_schemas.py @@ -154,6 +154,7 @@ class TradeSchema(BaseModel): trade_id: int pair: str is_open: bool + is_short: bool exchange: str amount: float amount_requested: float diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py index e2b655651..4a44ebe77 100644 --- a/freqtrade/rpc/rpc.py +++ b/freqtrade/rpc/rpc.py @@ -156,8 +156,9 @@ class RPC: # calculate profit and send message to user if trade.is_open: try: + closing_side = "buy" if trade.is_short else "sell" current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side="sell") + trade.pair, refresh=False, side=closing_side) except (ExchangeError, PricingError): current_rate = NAN else: @@ -216,8 +217,9 @@ class RPC: for trade in trades: # calculate profit and send message to user try: + closing_side = "buy" if trade.is_short else "sell" current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side="sell") + trade.pair, refresh=False, side=closing_side) except (PricingError, ExchangeError): current_rate = NAN trade_percent = (100 * trade.calc_profit_ratio(current_rate)) @@ -376,8 +378,9 @@ class RPC: else: # Get current rate try: + closing_side = "buy" if trade.is_short else "sell" current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side="sell") + trade.pair, refresh=False, side=closing_side) except (PricingError, ExchangeError): current_rate = NAN profit_ratio = trade.calc_profit_ratio(rate=current_rate) @@ -572,8 +575,9 @@ class RPC: if not fully_canceled: # Get current rate and execute sell + closing_side = "buy" if trade.is_short else "sell" current_rate = self._freqtrade.exchange.get_rate( - trade.pair, refresh=False, side="sell") + trade.pair, refresh=False, side=closing_side) sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL) self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason) # ---- EOF def _exec_forcesell ---- diff --git a/tests/conftest.py b/tests/conftest.py index 7e5dd47e7..2a6ff9f22 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -248,33 +248,35 @@ def patch_get_signal( freqtrade.exchange.refresh_latest_ohlcv = lambda p: None -def create_mock_trades(fee, is_short: bool = False, use_db: bool = True): +def create_mock_trades(fee, is_short: Optional[bool] = False, use_db: bool = True): """ Create some fake trades ... + :param is_short: Optional bool, None creates a mix of long and short trades. """ def add_trade(trade): if use_db: Trade.query.session.add(trade) else: LocalTrade.add_bt_trade(trade) - + is_short1 = is_short if is_short is not None else True + is_short2 = is_short if is_short is not None else False # Simulate dry_run entries - trade = mock_trade_1(fee, is_short) + trade = mock_trade_1(fee, is_short1) add_trade(trade) - trade = mock_trade_2(fee, is_short) + trade = mock_trade_2(fee, is_short1) add_trade(trade) - trade = mock_trade_3(fee, is_short) + trade = mock_trade_3(fee, is_short2) add_trade(trade) - trade = mock_trade_4(fee, is_short) + trade = mock_trade_4(fee, is_short2) add_trade(trade) - trade = mock_trade_5(fee, is_short) + trade = mock_trade_5(fee, is_short2) add_trade(trade) - trade = mock_trade_6(fee, is_short) + trade = mock_trade_6(fee, is_short1) add_trade(trade) if use_db: diff --git a/tests/conftest_trades.py b/tests/conftest_trades.py index dcd5e70df..0ad01e72f 100644 --- a/tests/conftest_trades.py +++ b/tests/conftest_trades.py @@ -317,6 +317,7 @@ def mock_trade_6(fee, is_short: bool): buy_tag='TEST2', open_order_id=f"prod_sell_{direc(is_short)}_6", timeframe=5, + is_short=is_short ) o = Order.parse_from_ccxt_object(mock_order_6(is_short), 'LTC/BTC', enter_side(is_short)) trade.orders.append(o) diff --git a/tests/rpc/test_rpc_apiserver.py b/tests/rpc/test_rpc_apiserver.py index e0e73f6f1..4fe2f8daa 100644 --- a/tests/rpc/test_rpc_apiserver.py +++ b/tests/rpc/test_rpc_apiserver.py @@ -476,7 +476,7 @@ def test_api_count(botclient, mocker, ticker, fee, markets, is_short): assert rc.json()["max"] == 1 # Create some test data - create_mock_trades(fee, is_short) + create_mock_trades(fee, is_short=is_short) rc = client_get(client, f"{BASE_URI}/count") assert_response(rc) assert rc.json()["current"] == 4 @@ -571,7 +571,7 @@ def test_api_trades(botclient, mocker, fee, markets, is_short): assert rc.json()['trades_count'] == 0 assert rc.json()['total_trades'] == 0 - create_mock_trades(fee, is_short) + create_mock_trades(fee, is_short=is_short) Trade.query.session.flush() rc = client_get(client, f"{BASE_URI}/trades") @@ -579,6 +579,7 @@ def test_api_trades(botclient, mocker, fee, markets, is_short): assert len(rc.json()['trades']) == 2 assert rc.json()['trades_count'] == 2 assert rc.json()['total_trades'] == 2 + assert rc.json()['trades'][0]['is_short'] == is_short rc = client_get(client, f"{BASE_URI}/trades?limit=1") assert_response(rc) assert len(rc.json()['trades']) == 1 @@ -605,6 +606,7 @@ def test_api_trade_single(botclient, mocker, fee, ticker, markets, is_short): rc = client_get(client, f"{BASE_URI}/trade/3") assert_response(rc) assert rc.json()['trade_id'] == 3 + assert rc.json()['is_short'] == is_short @pytest.mark.parametrize('is_short', [True, False]) @@ -623,7 +625,7 @@ def test_api_delete_trade(botclient, mocker, fee, markets, is_short): # Error - trade won't exist yet. assert_response(rc, 502) - create_mock_trades(fee, False) + create_mock_trades(fee, is_short=is_short) ftbot.strategy.order_types['stoploss_on_exchange'] = True trades = Trade.query.all() @@ -698,8 +700,46 @@ def test_api_edge_disabled(botclient, mocker, ticker, fee, markets): assert rc.json() == {"error": "Error querying /api/v1/edge: Edge is not enabled."} -# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) -def test_api_profit(botclient, mocker, ticker, fee, markets): +@pytest.mark.parametrize( + 'is_short,expected', + [( + True, + {'best_pair': 'ETC/BTC', 'best_rate': -0.5, 'profit_all_coin': 43.61269123, + 'profit_all_fiat': 538398.67323435, 'profit_all_percent_mean': 66.41, + 'profit_all_ratio_mean': 0.664109545, 'profit_all_percent_sum': 398.47, + 'profit_all_ratio_sum': 3.98465727, 'profit_all_percent': 4.36, + 'profit_all_ratio': 0.043612222872799825, 'profit_closed_coin': -0.00673913, + 'profit_closed_fiat': -83.19455985, 'profit_closed_ratio_mean': -0.0075, + 'profit_closed_percent_mean': -0.75, 'profit_closed_ratio_sum': -0.015, + 'profit_closed_percent_sum': -1.5, 'profit_closed_ratio': -6.739057628404269e-06, + 'profit_closed_percent': -0.0, 'winning_trades': 0, 'losing_trades': 2} + ), + ( + False, + {'best_pair': 'XRP/BTC', 'best_rate': 1.0, 'profit_all_coin': -44.0631579, + 'profit_all_fiat': -543959.6842755, 'profit_all_percent_mean': -66.41, + 'profit_all_ratio_mean': -0.6641100666666667, 'profit_all_percent_sum': -398.47, + 'profit_all_ratio_sum': -3.9846604, 'profit_all_percent': -4.41, + 'profit_all_ratio': -0.044063014216106644, 'profit_closed_coin': 0.00073913, + 'profit_closed_fiat': 9.124559849999999, 'profit_closed_ratio_mean': 0.0075, + 'profit_closed_percent_mean': 0.75, 'profit_closed_ratio_sum': 0.015, + 'profit_closed_percent_sum': 1.5, 'profit_closed_ratio': 7.391275897987988e-07, + 'profit_closed_percent': 0.0, 'winning_trades': 2, 'losing_trades': 0} + ), + ( + None, + {'best_pair': 'XRP/BTC', 'best_rate': 1.0, 'profit_all_coin': -14.43790415, + 'profit_all_fiat': -178235.92673175, 'profit_all_percent_mean': 0.08, + 'profit_all_ratio_mean': 0.000835751666666662, 'profit_all_percent_sum': 0.5, + 'profit_all_ratio_sum': 0.005014509999999972, 'profit_all_percent': -1.44, + 'profit_all_ratio': -0.014437768014451796, 'profit_closed_coin': -0.00542913, + 'profit_closed_fiat': -67.02260985, 'profit_closed_ratio_mean': 0.0025, + 'profit_closed_percent_mean': 0.25, 'profit_closed_ratio_sum': 0.005, + 'profit_closed_percent_sum': 0.5, 'profit_closed_ratio': -5.429078808526421e-06, + 'profit_closed_percent': -0.0, 'winning_trades': 1, 'losing_trades': 1} + ) + ]) +def test_api_profit(botclient, mocker, ticker, fee, markets, is_short, expected): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( @@ -714,39 +754,41 @@ def test_api_profit(botclient, mocker, ticker, fee, markets): assert_response(rc, 200) assert rc.json()['trade_count'] == 0 - create_mock_trades(fee, False) + create_mock_trades(fee, is_short=is_short) # Simulate fulfilled LIMIT_BUY order for trade rc = client_get(client, f"{BASE_URI}/profit") assert_response(rc) - assert rc.json() == {'avg_duration': ANY, - 'best_pair': 'XRP/BTC', - 'best_rate': 1.0, - 'first_trade_date': ANY, - 'first_trade_timestamp': ANY, - 'latest_trade_date': '5 minutes ago', - 'latest_trade_timestamp': ANY, - 'profit_all_coin': -44.0631579, - 'profit_all_fiat': -543959.6842755, - 'profit_all_percent_mean': -66.41, - 'profit_all_ratio_mean': -0.6641100666666667, - 'profit_all_percent_sum': -398.47, - 'profit_all_ratio_sum': -3.9846604, - 'profit_all_percent': -4.41, - 'profit_all_ratio': -0.044063014216106644, - 'profit_closed_coin': 0.00073913, - 'profit_closed_fiat': 9.124559849999999, - 'profit_closed_ratio_mean': 0.0075, - 'profit_closed_percent_mean': 0.75, - 'profit_closed_ratio_sum': 0.015, - 'profit_closed_percent_sum': 1.5, - 'profit_closed_ratio': 7.391275897987988e-07, - 'profit_closed_percent': 0.0, - 'trade_count': 6, - 'closed_trade_count': 2, - 'winning_trades': 2, - 'losing_trades': 0, - } + # raise ValueError(rc.json()) + assert rc.json() == { + 'avg_duration': ANY, + 'best_pair': expected['best_pair'], + 'best_rate': expected['best_rate'], + 'first_trade_date': ANY, + 'first_trade_timestamp': ANY, + 'latest_trade_date': '5 minutes ago', + 'latest_trade_timestamp': ANY, + 'profit_all_coin': expected['profit_all_coin'], + 'profit_all_fiat': expected['profit_all_fiat'], + 'profit_all_percent_mean': expected['profit_all_percent_mean'], + 'profit_all_ratio_mean': expected['profit_all_ratio_mean'], + 'profit_all_percent_sum': expected['profit_all_percent_sum'], + 'profit_all_ratio_sum': expected['profit_all_ratio_sum'], + 'profit_all_percent': expected['profit_all_percent'], + 'profit_all_ratio': expected['profit_all_ratio'], + 'profit_closed_coin': expected['profit_closed_coin'], + 'profit_closed_fiat': expected['profit_closed_fiat'], + 'profit_closed_ratio_mean': expected['profit_closed_ratio_mean'], + 'profit_closed_percent_mean': expected['profit_closed_percent_mean'], + 'profit_closed_ratio_sum': expected['profit_closed_ratio_sum'], + 'profit_closed_percent_sum': expected['profit_closed_percent_sum'], + 'profit_closed_ratio': expected['profit_closed_ratio'], + 'profit_closed_percent': expected['profit_closed_percent'], + 'trade_count': 6, + 'closed_trade_count': 2, + 'winning_trades': expected['winning_trades'], + 'losing_trades': expected['losing_trades'], + } @pytest.mark.parametrize('is_short', [True, False]) @@ -826,11 +868,12 @@ def test_api_performance(botclient, fee): 'profit_ratio': -0.05570419, 'profit_abs': -0.1150375}] -# TODO-lev: @pytest.mark.parametrize('is_short,side', [ -# (True, "short"), -# (False, "long") -# ]) -def test_api_status(botclient, mocker, ticker, fee, markets): +@pytest.mark.parametrize( + 'is_short,current_rate,open_order_id,open_trade_value', + [(True, 1.098e-05, 'dry_run_buy_short_12345', 15.0911775), + (False, 1.099e-05, 'dry_run_buy_long_12345', 15.1668225)]) +def test_api_status(botclient, mocker, ticker, fee, markets, is_short, + current_rate, open_order_id, open_trade_value): ftbot, client = botclient patch_get_signal(ftbot) mocker.patch.multiple( @@ -845,7 +888,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): rc = client_get(client, f"{BASE_URI}/status") assert_response(rc, 200) assert rc.json() == [] - create_mock_trades(fee, False) + create_mock_trades(fee, is_short=is_short) rc = client_get(client, f"{BASE_URI}/status") assert_response(rc) @@ -866,7 +909,7 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'profit_pct': ANY, 'profit_abs': ANY, 'profit_fiat': ANY, - 'current_rate': 1.099e-05, + 'current_rate': current_rate, 'open_date': ANY, 'open_timestamp': ANY, 'open_order': None, @@ -896,11 +939,12 @@ def test_api_status(botclient, mocker, ticker, fee, markets): 'fee_open_cost': None, 'fee_open_currency': None, 'is_open': True, + "is_short": is_short, 'max_rate': ANY, 'min_rate': ANY, - 'open_order_id': 'dry_run_buy_long_12345', + 'open_order_id': open_order_id, 'open_rate_requested': ANY, - 'open_trade_value': 15.1668225, + 'open_trade_value': open_trade_value, 'sell_reason': None, 'sell_order_status': None, 'strategy': CURRENT_TEST_STRATEGY, @@ -974,6 +1018,7 @@ def test_api_whitelist(botclient): } +# TODO -lev: add test for forcebuy (short) when feature is supported def test_api_forcebuy(botclient, mocker, fee): ftbot, client = botclient @@ -1003,6 +1048,7 @@ def test_api_forcebuy(botclient, mocker, fee): open_order_id="123456", open_date=datetime.utcnow(), is_open=False, + is_short=False, fee_close=fee.return_value, fee_open=fee.return_value, close_rate=0.265441, @@ -1051,6 +1097,7 @@ def test_api_forcebuy(botclient, mocker, fee): 'fee_open_cost': None, 'fee_open_currency': None, 'is_open': False, + 'is_short': False, 'max_rate': None, 'min_rate': None, 'open_order_id': '123456', diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 323eddd4d..07407d7f0 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -499,7 +499,7 @@ def test_telegram_stats(default_conf, update, ticker, ticker_sell_up, fee, msg_mock.reset_mock() # Create some test data - create_mock_trades(fee, is_short) + create_mock_trades(fee, is_short=is_short) telegram._stats(update=update, context=MagicMock()) assert msg_mock.call_count == 1 @@ -1261,8 +1261,10 @@ def test_edge_enabled(edge_conf, update, mocker) -> None: assert 'Winrate' not in msg_mock.call_args_list[0][0][0] -# TODO-lev: @pytest.mark.parametrize('is_short', [True, False]) -def test_telegram_trades(mocker, update, default_conf, fee): +@pytest.mark.parametrize('is_short,regex_pattern', + [(True, r"just now[ ]*XRP\/BTC \(#3\) -1.00% \("), + (False, r"just now[ ]*XRP\/BTC \(#3\) 1.00% \(")]) +def test_telegram_trades(mocker, update, default_conf, fee, is_short, regex_pattern): telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) @@ -1280,7 +1282,7 @@ def test_telegram_trades(mocker, update, default_conf, fee): assert "
" not in msg_mock.call_args_list[0][0][0]
     msg_mock.reset_mock()
 
-    create_mock_trades(fee, False)
+    create_mock_trades(fee, is_short=is_short)
 
     context = MagicMock()
     context.args = [5]
@@ -1290,8 +1292,7 @@ def test_telegram_trades(mocker, update, default_conf, fee):
     assert "Profit (" in msg_mock.call_args_list[0][0][0]
     assert "Close Date" in msg_mock.call_args_list[0][0][0]
     assert "
" in msg_mock.call_args_list[0][0][0]
-    assert bool(re.search(r"just now[ ]*XRP\/BTC \(#3\)  1.00% \(",
-                msg_mock.call_args_list[0][0][0]))
+    assert bool(re.search(regex_pattern, msg_mock.call_args_list[0][0][0]))
 
 
 @pytest.mark.parametrize('is_short', [True, False])
@@ -1305,7 +1306,7 @@ def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short):
     assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
 
     msg_mock.reset_mock()
-    create_mock_trades(fee, is_short)
+    create_mock_trades(fee, is_short=is_short)
 
     context = MagicMock()
     context.args = [1]
diff --git a/tests/test_freqtradebot.py b/tests/test_freqtradebot.py
index ccade9b2e..fbf700d94 100644
--- a/tests/test_freqtradebot.py
+++ b/tests/test_freqtradebot.py
@@ -4317,7 +4317,7 @@ def test_sync_wallet_dry_run(mocker, default_conf_usdt, ticker_usdt, fee, limit_
 @pytest.mark.usefixtures("init_persistence")
 @pytest.mark.parametrize("is_short,buy_calls,sell_calls", [
     (False, 1, 2),
-    (True, 2, 1),
+    (True, 1, 2),
 ])
 def test_cancel_all_open_orders(mocker, default_conf_usdt, fee, limit_order, limit_order_open,
                                 is_short, buy_calls, sell_calls):