mirror of
https://github.com/freqtrade/freqtrade.git
synced 2026-04-29 05:20:10 +00:00
Merge pull request #8560 from freqtrade/feat/recoverTrades
Recover trades after selling on exchange
This commit is contained in:
@@ -1779,6 +1779,71 @@ def test_fetch_positions(default_conf, mocker, exchange_name):
|
||||
"fetch_positions", "fetch_positions")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_fetch_orders(default_conf, mocker, exchange_name, limit_order):
|
||||
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_orders = MagicMock(return_value=[
|
||||
limit_order['buy'],
|
||||
limit_order['sell'],
|
||||
])
|
||||
api_mock.fetch_open_orders = MagicMock(return_value=[limit_order['buy']])
|
||||
api_mock.fetch_closed_orders = MagicMock(return_value=[limit_order['buy']])
|
||||
|
||||
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
||||
start_time = datetime.now(timezone.utc) - timedelta(days=5)
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
# Not available in dry-run
|
||||
assert exchange.fetch_orders('mocked', start_time) == []
|
||||
assert api_mock.fetch_orders.call_count == 0
|
||||
default_conf['dry_run'] = False
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
res = exchange.fetch_orders('mocked', start_time)
|
||||
assert api_mock.fetch_orders.call_count == 1
|
||||
assert api_mock.fetch_open_orders.call_count == 0
|
||||
assert api_mock.fetch_closed_orders.call_count == 0
|
||||
assert len(res) == 2
|
||||
|
||||
res = exchange.fetch_orders('mocked', start_time)
|
||||
|
||||
api_mock.fetch_orders.reset_mock()
|
||||
|
||||
def has_resp(_, endpoint):
|
||||
if endpoint == 'fetchOrders':
|
||||
return False
|
||||
if endpoint == 'fetchClosedOrders':
|
||||
return True
|
||||
if endpoint == 'fetchOpenOrders':
|
||||
return True
|
||||
|
||||
mocker.patch(f'{EXMS}.exchange_has', has_resp)
|
||||
|
||||
# happy path without fetchOrders
|
||||
res = exchange.fetch_orders('mocked', start_time)
|
||||
assert api_mock.fetch_orders.call_count == 0
|
||||
assert api_mock.fetch_open_orders.call_count == 1
|
||||
assert api_mock.fetch_closed_orders.call_count == 1
|
||||
|
||||
mocker.patch(f'{EXMS}.exchange_has', return_value=True)
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
"fetch_orders", "fetch_orders", retries=1,
|
||||
pair='mocked', since=start_time)
|
||||
|
||||
# Unhappy path - first fetch-orders call fails.
|
||||
api_mock.fetch_orders = MagicMock(side_effect=ccxt.NotSupported())
|
||||
api_mock.fetch_open_orders.reset_mock()
|
||||
api_mock.fetch_closed_orders.reset_mock()
|
||||
|
||||
res = exchange.fetch_orders('mocked', start_time)
|
||||
|
||||
assert api_mock.fetch_orders.call_count == 1
|
||||
assert api_mock.fetch_open_orders.call_count == 1
|
||||
assert api_mock.fetch_closed_orders.call_count == 1
|
||||
|
||||
|
||||
def test_fetch_trading_fees(default_conf, mocker):
|
||||
api_mock = MagicMock()
|
||||
tick = {
|
||||
|
||||
@@ -740,6 +740,33 @@ def test_api_delete_open_order(botclient, mocker, fee, markets, ticker, is_short
|
||||
assert cancel_mock.call_count == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize('is_short', [True, False])
|
||||
def test_api_trade_reload_trade(botclient, mocker, fee, markets, ticker, is_short):
|
||||
ftbot, client = botclient
|
||||
patch_get_signal(ftbot, enter_long=not is_short, enter_short=is_short)
|
||||
stoploss_mock = MagicMock()
|
||||
cancel_mock = MagicMock()
|
||||
ftbot.handle_onexchange_order = MagicMock()
|
||||
mocker.patch.multiple(
|
||||
EXMS,
|
||||
markets=PropertyMock(return_value=markets),
|
||||
fetch_ticker=ticker,
|
||||
cancel_order=cancel_mock,
|
||||
cancel_stoploss_order=stoploss_mock,
|
||||
)
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/trades/10/reload")
|
||||
assert_response(rc, 502)
|
||||
assert 'Could not find trade with id 10.' in rc.json()['error']
|
||||
assert ftbot.handle_onexchange_order.call_count == 0
|
||||
|
||||
create_mock_trades(fee, is_short=is_short)
|
||||
Trade.commit()
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/trades/5/reload")
|
||||
assert ftbot.handle_onexchange_order.call_count == 1
|
||||
|
||||
|
||||
def test_api_logs(botclient):
|
||||
ftbot, client = botclient
|
||||
rc = client_get(client, f"{BASE_URI}/logs")
|
||||
|
||||
@@ -143,8 +143,8 @@ def test_telegram_init(default_conf, mocker, caplog) -> None:
|
||||
message_str = ("rpc.telegram is listening for following commands: [['status'], ['profit'], "
|
||||
"['balance'], ['start'], ['stop'], "
|
||||
"['forceexit', 'forcesell', 'fx'], ['forcebuy', 'forcelong'], ['forceshort'], "
|
||||
"['trades'], ['delete'], ['cancel_open_order', 'coo'], ['performance'], "
|
||||
"['buys', 'entries'], ['exits', 'sells'], ['mix_tags'], "
|
||||
"['reload_trade'], ['trades'], ['delete'], ['cancel_open_order', 'coo'], "
|
||||
"['performance'], ['buys', 'entries'], ['exits', 'sells'], ['mix_tags'], "
|
||||
"['stats'], ['daily'], ['weekly'], ['monthly'], "
|
||||
"['count'], ['locks'], ['delete_locks', 'unlock'], "
|
||||
"['reload_conf', 'reload_config'], ['show_conf', 'show_config'], "
|
||||
@@ -1763,6 +1763,25 @@ async def test_telegram_delete_trade(mocker, update, default_conf, fee, is_short
|
||||
assert "Please make sure to take care of this asset" in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('is_short', [True, False])
|
||||
async def test_telegram_reload_trade_from_exchange(mocker, update, default_conf, fee, is_short):
|
||||
|
||||
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
|
||||
context = MagicMock()
|
||||
context.args = []
|
||||
|
||||
await telegram._reload_trade_from_exchange(update=update, context=context)
|
||||
assert "Trade-id not set." in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
msg_mock.reset_mock()
|
||||
create_mock_trades(fee, is_short=is_short)
|
||||
|
||||
context.args = [5]
|
||||
|
||||
await telegram._reload_trade_from_exchange(update=update, context=context)
|
||||
assert "Status: `Reloaded from orders from exchange`" in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
@pytest.mark.parametrize('is_short', [True, False])
|
||||
async def test_telegram_delete_open_order(mocker, update, default_conf, fee, is_short, ticker):
|
||||
|
||||
|
||||
@@ -5552,6 +5552,51 @@ def test_handle_insufficient_funds(mocker, default_conf_usdt, fee, is_short, cap
|
||||
assert log_has(f"Error updating {order['id']}.", caplog)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("init_persistence")
|
||||
@pytest.mark.parametrize("is_short", [False, True])
|
||||
def test_handle_onexchange_order(mocker, default_conf_usdt, limit_order, is_short, caplog):
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf_usdt)
|
||||
mock_uts = mocker.spy(freqtrade, 'update_trade_state')
|
||||
|
||||
entry_order = limit_order[entry_side(is_short)]
|
||||
exit_order = limit_order[exit_side(is_short)]
|
||||
mock_fo = mocker.patch(f'{EXMS}.fetch_orders', return_value=[
|
||||
entry_order,
|
||||
exit_order,
|
||||
])
|
||||
|
||||
order_id = entry_order['id']
|
||||
|
||||
trade = Trade(
|
||||
open_order_id=order_id,
|
||||
pair='ETH/USDT',
|
||||
fee_open=0.001,
|
||||
fee_close=0.001,
|
||||
open_rate=entry_order['price'],
|
||||
open_date=arrow.utcnow().datetime,
|
||||
stake_amount=entry_order['cost'],
|
||||
amount=entry_order['amount'],
|
||||
exchange="binance",
|
||||
is_short=is_short,
|
||||
leverage=1,
|
||||
)
|
||||
|
||||
trade.orders.append(Order.parse_from_ccxt_object(
|
||||
entry_order, 'ADA/USDT', entry_side(is_short))
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
freqtrade.handle_onexchange_order(trade)
|
||||
assert log_has_re(r"Found previously unknown order .*", caplog)
|
||||
assert mock_uts.call_count == 1
|
||||
assert mock_fo.call_count == 1
|
||||
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
|
||||
assert len(trade.orders) == 2
|
||||
assert trade.is_open is False
|
||||
assert trade.exit_reason == ExitType.SOLD_ON_EXCHANGE.value
|
||||
|
||||
|
||||
def test_get_valid_price(mocker, default_conf_usdt) -> None:
|
||||
patch_RPCManager(mocker)
|
||||
patch_exchange(mocker)
|
||||
|
||||
@@ -75,8 +75,9 @@ def test_may_execute_exit_stoploss_on_exchange_multi(default_conf, ticker, fee,
|
||||
_notify_exit=MagicMock(),
|
||||
)
|
||||
mocker.patch("freqtrade.strategy.interface.IStrategy.should_exit", should_sell_mock)
|
||||
wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update", MagicMock())
|
||||
mocker.patch("freqtrade.wallets.Wallets.get_free", MagicMock(return_value=1000))
|
||||
wallets_mock = mocker.patch("freqtrade.wallets.Wallets.update")
|
||||
mocker.patch("freqtrade.wallets.Wallets.get_free", return_value=1000)
|
||||
mocker.patch("freqtrade.wallets.Wallets.check_exit_amount", return_value=True)
|
||||
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
freqtrade.strategy.order_types['stoploss_on_exchange'] = True
|
||||
|
||||
@@ -3,9 +3,11 @@ from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
|
||||
from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
|
||||
from freqtrade.exceptions import DependencyException
|
||||
from freqtrade.persistence import Trade
|
||||
from tests.conftest import EXMS, create_mock_trades, get_patched_freqtradebot, patch_wallet
|
||||
|
||||
|
||||
@@ -364,3 +366,48 @@ def test_sync_wallet_futures_dry(mocker, default_conf, fee):
|
||||
free = freqtrade.wallets.get_free('BTC')
|
||||
used = freqtrade.wallets.get_used('BTC')
|
||||
assert free + used == total
|
||||
|
||||
|
||||
def test_check_exit_amount(mocker, default_conf, fee):
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
update_mock = mocker.patch("freqtrade.wallets.Wallets.update")
|
||||
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=123)
|
||||
|
||||
create_mock_trades(fee, is_short=None)
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
assert trade.amount == 123
|
||||
|
||||
assert freqtrade.wallets.check_exit_amount(trade) is True
|
||||
assert update_mock.call_count == 0
|
||||
assert total_mock.call_count == 1
|
||||
|
||||
update_mock.reset_mock()
|
||||
# Reduce returned amount to below the trade amount - which should
|
||||
# trigger a wallet update and return False, triggering "order refinding"
|
||||
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=100)
|
||||
assert freqtrade.wallets.check_exit_amount(trade) is False
|
||||
assert update_mock.call_count == 1
|
||||
assert total_mock.call_count == 2
|
||||
|
||||
|
||||
def test_check_exit_amount_futures(mocker, default_conf, fee):
|
||||
default_conf['trading_mode'] = 'futures'
|
||||
default_conf['margin_mode'] = 'isolated'
|
||||
freqtrade = get_patched_freqtradebot(mocker, default_conf)
|
||||
total_mock = mocker.patch("freqtrade.wallets.Wallets.get_total", return_value=123)
|
||||
|
||||
create_mock_trades(fee, is_short=None)
|
||||
trade = Trade.session.scalars(select(Trade)).first()
|
||||
trade.trading_mode = 'futures'
|
||||
assert trade.amount == 123
|
||||
|
||||
assert freqtrade.wallets.check_exit_amount(trade) is True
|
||||
assert total_mock.call_count == 0
|
||||
|
||||
update_mock = mocker.patch("freqtrade.wallets.Wallets.update")
|
||||
trade.amount = 150
|
||||
# Reduce returned amount to below the trade amount - which should
|
||||
# trigger a wallet update and return False, triggering "order refinding"
|
||||
assert freqtrade.wallets.check_exit_amount(trade) is False
|
||||
assert total_mock.call_count == 0
|
||||
assert update_mock.call_count == 1
|
||||
|
||||
Reference in New Issue
Block a user