diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 663cfb1be..904387fb2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -425,7 +425,7 @@ jobs: python setup.py sdist bdist_wheel - name: Publish to PyPI (Test) - uses: pypa/gh-action-pypi-publish@v1.7.1 + uses: pypa/gh-action-pypi-publish@v1.8.1 if: (github.event_name == 'release') with: user: __token__ @@ -433,7 +433,7 @@ jobs: repository_url: https://test.pypi.org/legacy/ - name: Publish to PyPI - uses: pypa/gh-action-pypi-publish@v1.7.1 + uses: pypa/gh-action-pypi-publish@v1.8.1 if: (github.event_name == 'release') with: user: __token__ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bc2e0bc0d..ca3da8e90 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: - types-requests==2.28.11.15 - types-tabulate==0.9.0.1 - types-python-dateutil==2.8.19.10 - - SQLAlchemy==2.0.5.post1 + - SQLAlchemy==2.0.7 # stages: [push] - repo: https://github.com/pycqa/isort diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index d384a7ec5..110373844 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,6 +1,6 @@ markdown==3.3.7 mkdocs==1.4.2 -mkdocs-material==9.1.2 +mkdocs-material==9.1.3 mdx_truly_sane_lists==1.3 pymdown-extensions==9.10 jinja2==3.1.2 diff --git a/freqtrade/exchange/bybit.py b/freqtrade/exchange/bybit.py index 6f841b608..a4b070741 100644 --- a/freqtrade/exchange/bybit.py +++ b/freqtrade/exchange/bybit.py @@ -114,7 +114,7 @@ class Bybit(Exchange): data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data] return data - def _lev_prep(self, pair: str, leverage: float, side: BuySell): + def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): if self.trading_mode != TradingMode.SPOT: params = {'leverage': leverage} self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params) diff --git a/freqtrade/exchange/exchange.py b/freqtrade/exchange/exchange.py index e5f897c2a..99551b054 100644 --- a/freqtrade/exchange/exchange.py +++ b/freqtrade/exchange/exchange.py @@ -1018,10 +1018,10 @@ class Exchange: # Order handling - def _lev_prep(self, pair: str, leverage: float, side: BuySell): + def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): if self.trading_mode != TradingMode.SPOT: - self.set_margin_mode(pair, self.margin_mode) - self._set_leverage(leverage, pair) + self.set_margin_mode(pair, self.margin_mode, accept_fail) + self._set_leverage(leverage, pair, accept_fail) def _get_params( self, @@ -1202,7 +1202,7 @@ class Exchange: amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) - self._lev_prep(pair, leverage, side) + self._lev_prep(pair, leverage, side, accept_fail=True) order = self._api.create_order(symbol=pair, type=ordertype, side=side, amount=amount, price=limit_rate, params=params) self._log_exchange_response('create_stoploss_order', order) @@ -2527,7 +2527,6 @@ class Exchange: self, leverage: float, pair: Optional[str] = None, - trading_mode: Optional[TradingMode] = None, accept_fail: bool = False, ): """ @@ -2545,7 +2544,7 @@ class Exchange: self._log_exchange_response('set_leverage', res) except ccxt.DDoSProtection as e: raise DDosProtection(e) from e - except ccxt.BadRequest as e: + except (ccxt.BadRequest, ccxt.InsufficientFunds) as e: if not accept_fail: raise TemporaryError( f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e diff --git a/freqtrade/exchange/kraken.py b/freqtrade/exchange/kraken.py index 8a4f7f7e0..b1a19fa69 100644 --- a/freqtrade/exchange/kraken.py +++ b/freqtrade/exchange/kraken.py @@ -158,7 +158,6 @@ class Kraken(Exchange): self, leverage: float, pair: Optional[str] = None, - trading_mode: Optional[TradingMode] = None, accept_fail: bool = False, ): """ diff --git a/freqtrade/exchange/okx.py b/freqtrade/exchange/okx.py index e7d658d24..a4fcaeca0 100644 --- a/freqtrade/exchange/okx.py +++ b/freqtrade/exchange/okx.py @@ -1,14 +1,16 @@ import logging -from typing import Dict, List, Optional, Tuple +from typing import Any, Dict, List, Optional, Tuple import ccxt from freqtrade.constants import BuySell from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.enums.pricetype import PriceType -from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError +from freqtrade.exceptions import (DDosProtection, OperationalException, RetryableOrderError, + TemporaryError) from freqtrade.exchange import Exchange, date_minus_candles from freqtrade.exchange.common import retrier +from freqtrade.misc import safe_value_fallback2 logger = logging.getLogger(__name__) @@ -24,11 +26,13 @@ class Okx(Exchange): "ohlcv_candle_limit": 100, # Warning, special case with data prior to X months "mark_ohlcv_timeframe": "4h", "funding_fee_timeframe": "8h", + "stoploss_order_types": {"limit": "limit"}, + "stoploss_on_exchange": True, } _ft_has_futures: Dict = { "tickers_have_quoteVolume": False, "fee_cost_in_contracts": True, - "stop_price_type_field": "tpTriggerPxType", + "stop_price_type_field": "slTriggerPxType", "stop_price_type_value_mapping": { PriceType.LAST: "last", PriceType.MARK: "index", @@ -121,10 +125,9 @@ class Okx(Exchange): return params @retrier - def _lev_prep(self, pair: str, leverage: float, side: BuySell): + def _lev_prep(self, pair: str, leverage: float, side: BuySell, accept_fail: bool = False): if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None: try: - # TODO-lev: Test me properly (check mgnMode passed) res = self._api.set_leverage( leverage=leverage, symbol=pair, @@ -157,3 +160,78 @@ class Okx(Exchange): pair_tiers = self._leverage_tiers[pair] return pair_tiers[-1]['maxNotional'] / leverage + + def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: + + params = self._params.copy() + # Verify if stopPrice works for your exchange! + params.update({'stopLossPrice': stop_price}) + + if self.trading_mode == TradingMode.FUTURES and self.margin_mode: + params['tdMode'] = self.margin_mode.value + params['posSide'] = self._get_posSide(side, True) + return params + + def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool: + """ + OKX uses non-default stoploss price naming. + """ + if not self._ft_has.get('stoploss_on_exchange'): + raise OperationalException(f"stoploss is not implemented for {self.name}.") + + return ( + order.get('stopLossPrice', None) is None + or ((side == "sell" and stop_loss > float(order['stopLossPrice'])) or + (side == "buy" and stop_loss < float(order['stopLossPrice']))) + ) + + def fetch_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + if self._config['dry_run']: + return self.fetch_dry_run_order(order_id) + + try: + params1 = {'stop': True} + order_reg = self._api.fetch_order(order_id, pair, params=params1) + self._log_exchange_response('fetch_stoploss_order', order_reg) + return order_reg + except ccxt.OrderNotFound: + pass + params2 = {'stop': True, 'ordType': 'conditional'} + for method in (self._api.fetch_open_orders, self._api.fetch_closed_orders, + self._api.fetch_canceled_orders): + try: + orders = method(pair, params=params2) + orders_f = [order for order in orders if order['id'] == order_id] + if orders_f: + order = orders_f[0] + if (order['status'] == 'closed' + and (real_order_id := order.get('info', {}).get('ordId')) is not None): + # Once a order triggered, we fetch the regular followup order. + order_reg = self.fetch_order(real_order_id, pair) + self._log_exchange_response('fetch_stoploss_order1', order_reg) + order_reg['id_stop'] = order_reg['id'] + order_reg['id'] = order_id + order_reg['type'] = 'stoploss' + order_reg['status_stop'] = 'triggered' + return order_reg + order['type'] = 'stoploss' + return order + except ccxt.BaseError: + pass + raise RetryableOrderError( + f'StoplossOrder not found (pair: {pair} id: {order_id}).') + + def get_order_id_conditional(self, order: Dict[str, Any]) -> str: + if order['type'] == 'stop': + return safe_value_fallback2(order, order, 'id_stop', 'id') + return order['id'] + + def cancel_stoploss_order(self, order_id: str, pair: str, params: Dict = {}) -> Dict: + params1 = {'stop': True} + # 'ordType': 'conditional' + # + return self.cancel_order( + order_id=order_id, + pair=pair, + params=params1, + ) diff --git a/freqtrade/persistence/models.py b/freqtrade/persistence/models.py index eee07e61c..2315c0acc 100644 --- a/freqtrade/persistence/models.py +++ b/freqtrade/persistence/models.py @@ -2,7 +2,9 @@ This module contains the class to persist trades into SQLite """ import logging -from typing import Any, Dict +import threading +from contextvars import ContextVar +from typing import Any, Dict, Final, Optional from sqlalchemy import create_engine, inspect from sqlalchemy.exc import NoSuchModuleError @@ -19,6 +21,22 @@ from freqtrade.persistence.trade_model import Order, Trade logger = logging.getLogger(__name__) +REQUEST_ID_CTX_KEY: Final[str] = 'request_id' +_request_id_ctx_var: ContextVar[Optional[str]] = ContextVar(REQUEST_ID_CTX_KEY, default=None) + + +def get_request_or_thread_id() -> Optional[str]: + """ + Helper method to get either async context (for fastapi requests), or thread id + """ + id = _request_id_ctx_var.get() + if id is None: + # when not in request context - use thread id + id = str(threading.current_thread().ident) + + return id + + _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' @@ -53,8 +71,9 @@ def init_db(db_url: str) -> None: # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope # Scoped sessions proxy requests to the appropriate thread-local session. - # We should use the scoped_session object - not a seperately initialized version - Trade.session = scoped_session(sessionmaker(bind=engine, autoflush=False)) + # Since we also use fastAPI, we need to make it aware of the request id, too + Trade.session = scoped_session(sessionmaker( + bind=engine, autoflush=False), scopefunc=get_request_or_thread_id) Order.session = Trade.session PairLock.session = Trade.session diff --git a/freqtrade/rpc/api_server/deps.py b/freqtrade/rpc/api_server/deps.py index aed97367b..f5b1bcd74 100644 --- a/freqtrade/rpc/api_server/deps.py +++ b/freqtrade/rpc/api_server/deps.py @@ -1,9 +1,11 @@ -from typing import Any, Dict, Iterator, Optional +from typing import Any, AsyncIterator, Dict, Optional +from uuid import uuid4 from fastapi import Depends from freqtrade.enums import RunMode from freqtrade.persistence import Trade +from freqtrade.persistence.models import _request_id_ctx_var from freqtrade.rpc.rpc import RPC, RPCException from .webserver import ApiServer @@ -15,12 +17,19 @@ def get_rpc_optional() -> Optional[RPC]: return None -def get_rpc() -> Optional[Iterator[RPC]]: +async def get_rpc() -> Optional[AsyncIterator[RPC]]: + _rpc = get_rpc_optional() if _rpc: + request_id = str(uuid4()) + ctx_token = _request_id_ctx_var.set(request_id) Trade.rollback() - yield _rpc - Trade.rollback() + try: + yield _rpc + finally: + Trade.session.remove() + _request_id_ctx_var.reset(ctx_token) + else: raise RPCException('Bot is not in the correct state') diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index cb219eef1..962c5e058 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -83,6 +83,8 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]: self._send_msg(str(e)) except BaseException: logger.exception('Exception occurred within Telegram module') + finally: + Trade.session.remove() return wrapper diff --git a/pyproject.toml b/pyproject.toml index c3ca9e1b0..baf707c68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[build-system] +requires = ["setuptools >= 46.4.0", "wheel"] +build-backend = "setuptools.build_meta" + [tool.black] line-length = 100 exclude = ''' @@ -48,10 +52,6 @@ ignore_errors = true module = "telegram.*" implicit_optional = true -[build-system] -requires = ["setuptools >= 46.4.0", "wheel"] -build-backend = "setuptools.build_meta" - [tool.pyright] include = ["freqtrade"] exclude = [ diff --git a/requirements-dev.txt b/requirements-dev.txt index 6d076777f..8312e2820 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,11 +7,11 @@ -r docs/requirements-docs.txt coveralls==3.3.1 -ruff==0.0.255 +ruff==0.0.257 mypy==1.1.1 -pre-commit==3.1.1 +pre-commit==3.2.0 pytest==7.2.2 -pytest-asyncio==0.20.3 +pytest-asyncio==0.21.0 pytest-cov==4.0.0 pytest-mock==3.10.0 pytest-random-order==1.1.0 @@ -22,7 +22,7 @@ time-machine==2.9.0 httpx==0.23.3 # Convert jupyter notebooks to markdown documents -nbconvert==7.2.9 +nbconvert==7.2.10 # mypy types types-cachetools==5.3.0.4 diff --git a/requirements-hyperopt.txt b/requirements-hyperopt.txt index 904b5d661..4d86da2b6 100644 --- a/requirements-hyperopt.txt +++ b/requirements-hyperopt.txt @@ -5,5 +5,5 @@ scipy==1.10.1 scikit-learn==1.1.3 scikit-optimize==0.9.0 -filelock==3.9.0 +filelock==3.10.0 progressbar2==4.2.0 diff --git a/requirements.txt b/requirements.txt index 9e17424f5..ad34883ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,10 +2,10 @@ numpy==1.24.2 pandas==1.5.3 pandas-ta==0.3.14b -ccxt==2.9.12 +ccxt==3.0.23 cryptography==39.0.2 aiohttp==3.8.4 -SQLAlchemy==2.0.5.post1 +SQLAlchemy==2.0.7 python-telegram-bot==13.15 arrow==1.2.3 cachetools==4.2.2 @@ -26,7 +26,7 @@ pyarrow==11.0.0; platform_machine != 'armv7l' py_find_1st==1.1.5 # Load ticker files 30% faster -python-rapidjson==1.9 +python-rapidjson==1.10 # Properly format api responses orjson==3.8.7 @@ -34,9 +34,9 @@ orjson==3.8.7 sdnotify==0.3.2 # API Server -fastapi==0.94.0 +fastapi==0.95.0 pydantic==1.10.6 -uvicorn==0.21.0 +uvicorn==0.21.1 pyjwt==2.6.0 aiofiles==23.1.0 psutil==5.9.4 @@ -56,4 +56,4 @@ schedule==1.1.0 websockets==10.4 janus==1.0.0 -ast-comments==1.0.0 +ast-comments==1.0.1 diff --git a/tests/exchange/test_binance.py b/tests/exchange/test_binance.py index ba786bb3b..8ada089bd 100644 --- a/tests/exchange/test_binance.py +++ b/tests/exchange/test_binance.py @@ -555,7 +555,6 @@ def test__set_leverage_binance(mocker, default_conf): "set_leverage", pair="XRP/USDT", leverage=5.0, - trading_mode=TradingMode.FUTURES ) diff --git a/tests/exchange/test_exchange.py b/tests/exchange/test_exchange.py index 7c48f1c9d..586f023b4 100644 --- a/tests/exchange/test_exchange.py +++ b/tests/exchange/test_exchange.py @@ -1039,9 +1039,9 @@ def test_validate_ordertypes(default_conf, mocker): ('bybit', 'last', True), ('bybit', 'mark', True), ('bybit', 'index', True), - # ('okx', 'last', True), - # ('okx', 'mark', True), - # ('okx', 'index', True), + ('okx', 'last', True), + ('okx', 'mark', True), + ('okx', 'index', True), ('gate', 'last', True), ('gate', 'mark', True), ('gate', 'index', True), @@ -3868,29 +3868,6 @@ def test_get_stake_amount_considering_leverage( stake_amount, leverage) == min_stake_with_lev -@pytest.mark.parametrize("exchange_name,trading_mode", [ - ("binance", TradingMode.FUTURES), -]) -def test__set_leverage(mocker, default_conf, exchange_name, trading_mode): - - api_mock = MagicMock() - api_mock.set_leverage = MagicMock() - type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) - default_conf['dry_run'] = False - - ccxt_exceptionhandlers( - mocker, - default_conf, - api_mock, - exchange_name, - "_set_leverage", - "set_leverage", - pair="XRP/USDT", - leverage=5.0, - trading_mode=trading_mode - ) - - @pytest.mark.parametrize("margin_mode", [ (MarginMode.CROSS), (MarginMode.ISOLATED) diff --git a/tests/exchange/test_okx.py b/tests/exchange/test_okx.py index fce77f4c7..7a3fa22f0 100644 --- a/tests/exchange/test_okx.py +++ b/tests/exchange/test_okx.py @@ -2,11 +2,13 @@ from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import MagicMock, PropertyMock +import ccxt import pytest from freqtrade.enums import CandleType, MarginMode, TradingMode +from freqtrade.exceptions import RetryableOrderError from freqtrade.exchange.exchange import timeframe_to_minutes -from tests.conftest import get_mock_coro, get_patched_exchange, log_has +from tests.conftest import EXMS, get_mock_coro, get_patched_exchange, log_has from tests.exchange.test_exchange import ccxt_exceptionhandlers @@ -476,3 +478,116 @@ def test_load_leverage_tiers_okx(default_conf, mocker, markets, tmpdir, caplog, exchange.load_leverage_tiers() assert log_has(logmsg, caplog) + + +def test__set_leverage_okx(mocker, default_conf): + + api_mock = MagicMock() + api_mock.set_leverage = MagicMock() + type(api_mock).has = PropertyMock(return_value={'setLeverage': True}) + default_conf['dry_run'] = False + default_conf['trading_mode'] = TradingMode.FUTURES + default_conf['margin_mode'] = MarginMode.ISOLATED + + exchange = get_patched_exchange(mocker, default_conf, api_mock, id="okx") + exchange._lev_prep('BTC/USDT:USDT', 3.2, 'buy') + assert api_mock.set_leverage.call_count == 1 + # Leverage is rounded to 3. + assert api_mock.set_leverage.call_args_list[0][1]['leverage'] == 3.2 + assert api_mock.set_leverage.call_args_list[0][1]['symbol'] == 'BTC/USDT:USDT' + assert api_mock.set_leverage.call_args_list[0][1]['params'] == { + 'mgnMode': 'isolated', + 'posSide': 'net'} + + ccxt_exceptionhandlers( + mocker, + default_conf, + api_mock, + "okx", + "_lev_prep", + "set_leverage", + pair="XRP/USDT:USDT", + leverage=5.0, + side='buy' + ) + + +@pytest.mark.usefixtures("init_persistence") +def test_fetch_stoploss_order_okx(default_conf, mocker): + default_conf['dry_run'] = False + api_mock = MagicMock() + api_mock.fetch_order = MagicMock() + + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='okx') + + exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert api_mock.fetch_order.call_count == 1 + assert api_mock.fetch_order.call_args_list[0][0][0] == '1234' + assert api_mock.fetch_order.call_args_list[0][0][1] == 'ETH/BTC' + assert api_mock.fetch_order.call_args_list[0][1]['params'] == {'stop': True} + + api_mock.fetch_order = MagicMock(side_effect=ccxt.OrderNotFound) + api_mock.fetch_open_orders = MagicMock(return_value=[]) + api_mock.fetch_closed_orders = MagicMock(return_value=[]) + api_mock.fetch_canceled_orders = MagicMock(creturn_value=[]) + + with pytest.raises(RetryableOrderError): + exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert api_mock.fetch_order.call_count == 1 + assert api_mock.fetch_open_orders.call_count == 1 + assert api_mock.fetch_closed_orders.call_count == 1 + assert api_mock.fetch_canceled_orders.call_count == 1 + + api_mock.fetch_order.reset_mock() + api_mock.fetch_open_orders.reset_mock() + api_mock.fetch_closed_orders.reset_mock() + api_mock.fetch_canceled_orders.reset_mock() + + api_mock.fetch_closed_orders = MagicMock(return_value=[ + { + 'id': '1234', + 'status': 'closed', + 'info': {'ordId': '123455'} + } + ]) + mocker.patch(f"{EXMS}.fetch_order", MagicMock(return_value={'id': '123455'})) + resp = exchange.fetch_stoploss_order('1234', 'ETH/BTC') + assert api_mock.fetch_order.call_count == 1 + assert api_mock.fetch_open_orders.call_count == 1 + assert api_mock.fetch_closed_orders.call_count == 1 + assert api_mock.fetch_canceled_orders.call_count == 0 + + assert resp['id'] == '1234' + assert resp['id_stop'] == '123455' + assert resp['type'] == 'stoploss' + + default_conf['dry_run'] = True + exchange = get_patched_exchange(mocker, default_conf, api_mock, id='okx') + dro_mock = mocker.patch(f"{EXMS}.fetch_dry_run_order", MagicMock(return_value={'id': '123455'})) + + api_mock.fetch_order.reset_mock() + api_mock.fetch_open_orders.reset_mock() + api_mock.fetch_closed_orders.reset_mock() + api_mock.fetch_canceled_orders.reset_mock() + resp = exchange.fetch_stoploss_order('1234', 'ETH/BTC') + + assert api_mock.fetch_order.call_count == 0 + assert api_mock.fetch_open_orders.call_count == 0 + assert api_mock.fetch_closed_orders.call_count == 0 + assert api_mock.fetch_canceled_orders.call_count == 0 + assert dro_mock.call_count == 1 + + +@pytest.mark.parametrize('sl1,sl2,sl3,side', [ + (1501, 1499, 1501, "sell"), + (1499, 1501, 1499, "buy") +]) +def test_stoploss_adjust_okx(mocker, default_conf, sl1, sl2, sl3, side): + exchange = get_patched_exchange(mocker, default_conf, id='okx') + order = { + 'type': 'stoploss', + 'price': 1500, + 'stopLossPrice': 1500, + } + assert exchange.stoploss_adjust(sl1, order, side=side) + assert not exchange.stoploss_adjust(sl2, order, side=side) diff --git a/tests/rpc/test_rpc_telegram.py b/tests/rpc/test_rpc_telegram.py index 7dabe9dfe..54f612c59 100644 --- a/tests/rpc/test_rpc_telegram.py +++ b/tests/rpc/test_rpc_telegram.py @@ -674,8 +674,9 @@ def test_monthly_handle(default_conf_usdt, update, ticker, fee, mocker, time_mac assert 'Monthly Profit over the last 6 months:' in msg_mock.call_args_list[0][0][0] -def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee, - limit_sell_order_usdt, mocker) -> None: +def test_telegram_profit_handle( + default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee, + limit_sell_order_usdt, mocker) -> None: mocker.patch('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1) mocker.patch.multiple( EXMS, @@ -710,6 +711,7 @@ def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, f # Update the ticker with a market going up mocker.patch(f'{EXMS}.fetch_ticker', ticker_sell_up) # Simulate fulfilled LIMIT_SELL order for trade + trade = Trade.session.scalars(select(Trade)).first() oobj = Order.parse_from_ccxt_object( limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell') trade.orders.append(oobj)