Merge remote-tracking branch 'origin/develop' into develop

This commit is contained in:
hippocritical 2023-03-22 12:44:29 +01:00
commit f57787882d
18 changed files with 268 additions and 69 deletions

View File

@ -425,7 +425,7 @@ jobs:
python setup.py sdist bdist_wheel python setup.py sdist bdist_wheel
- name: Publish to PyPI (Test) - 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') if: (github.event_name == 'release')
with: with:
user: __token__ user: __token__
@ -433,7 +433,7 @@ jobs:
repository_url: https://test.pypi.org/legacy/ repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI - 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') if: (github.event_name == 'release')
with: with:
user: __token__ user: __token__

View File

@ -18,7 +18,7 @@ repos:
- types-requests==2.28.11.15 - types-requests==2.28.11.15
- types-tabulate==0.9.0.1 - types-tabulate==0.9.0.1
- types-python-dateutil==2.8.19.10 - types-python-dateutil==2.8.19.10
- SQLAlchemy==2.0.5.post1 - SQLAlchemy==2.0.7
# stages: [push] # stages: [push]
- repo: https://github.com/pycqa/isort - repo: https://github.com/pycqa/isort

View File

@ -1,6 +1,6 @@
markdown==3.3.7 markdown==3.3.7
mkdocs==1.4.2 mkdocs==1.4.2
mkdocs-material==9.1.2 mkdocs-material==9.1.3
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==9.10 pymdown-extensions==9.10
jinja2==3.1.2 jinja2==3.1.2

View File

@ -114,7 +114,7 @@ class Bybit(Exchange):
data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data] data = [[x['timestamp'], x['fundingRate'], 0, 0, 0, 0] for x in data]
return 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: if self.trading_mode != TradingMode.SPOT:
params = {'leverage': leverage} params = {'leverage': leverage}
self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params) self.set_margin_mode(pair, self.margin_mode, accept_fail=True, params=params)

View File

@ -1018,10 +1018,10 @@ class Exchange:
# Order handling # 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: if self.trading_mode != TradingMode.SPOT:
self.set_margin_mode(pair, self.margin_mode) self.set_margin_mode(pair, self.margin_mode, accept_fail)
self._set_leverage(leverage, pair) self._set_leverage(leverage, pair, accept_fail)
def _get_params( def _get_params(
self, self,
@ -1202,7 +1202,7 @@ class Exchange:
amount = self.amount_to_precision(pair, self._amount_to_contracts(pair, amount)) 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, order = self._api.create_order(symbol=pair, type=ordertype, side=side,
amount=amount, price=limit_rate, params=params) amount=amount, price=limit_rate, params=params)
self._log_exchange_response('create_stoploss_order', order) self._log_exchange_response('create_stoploss_order', order)
@ -2527,7 +2527,6 @@ class Exchange:
self, self,
leverage: float, leverage: float,
pair: Optional[str] = None, pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False, accept_fail: bool = False,
): ):
""" """
@ -2545,7 +2544,7 @@ class Exchange:
self._log_exchange_response('set_leverage', res) self._log_exchange_response('set_leverage', res)
except ccxt.DDoSProtection as e: except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e raise DDosProtection(e) from e
except ccxt.BadRequest as e: except (ccxt.BadRequest, ccxt.InsufficientFunds) as e:
if not accept_fail: if not accept_fail:
raise TemporaryError( raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e

View File

@ -158,7 +158,6 @@ class Kraken(Exchange):
self, self,
leverage: float, leverage: float,
pair: Optional[str] = None, pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None,
accept_fail: bool = False, accept_fail: bool = False,
): ):
""" """

View File

@ -1,14 +1,16 @@
import logging import logging
from typing import Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import ccxt import ccxt
from freqtrade.constants import BuySell from freqtrade.constants import BuySell
from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.enums.pricetype import PriceType 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 import Exchange, date_minus_candles
from freqtrade.exchange.common import retrier from freqtrade.exchange.common import retrier
from freqtrade.misc import safe_value_fallback2
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -24,11 +26,13 @@ class Okx(Exchange):
"ohlcv_candle_limit": 100, # Warning, special case with data prior to X months "ohlcv_candle_limit": 100, # Warning, special case with data prior to X months
"mark_ohlcv_timeframe": "4h", "mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h", "funding_fee_timeframe": "8h",
"stoploss_order_types": {"limit": "limit"},
"stoploss_on_exchange": True,
} }
_ft_has_futures: Dict = { _ft_has_futures: Dict = {
"tickers_have_quoteVolume": False, "tickers_have_quoteVolume": False,
"fee_cost_in_contracts": True, "fee_cost_in_contracts": True,
"stop_price_type_field": "tpTriggerPxType", "stop_price_type_field": "slTriggerPxType",
"stop_price_type_value_mapping": { "stop_price_type_value_mapping": {
PriceType.LAST: "last", PriceType.LAST: "last",
PriceType.MARK: "index", PriceType.MARK: "index",
@ -121,10 +125,9 @@ class Okx(Exchange):
return params return params
@retrier @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: if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
try: try:
# TODO-lev: Test me properly (check mgnMode passed)
res = self._api.set_leverage( res = self._api.set_leverage(
leverage=leverage, leverage=leverage,
symbol=pair, symbol=pair,
@ -157,3 +160,78 @@ class Okx(Exchange):
pair_tiers = self._leverage_tiers[pair] pair_tiers = self._leverage_tiers[pair]
return pair_tiers[-1]['maxNotional'] / leverage 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,
)

View File

@ -2,7 +2,9 @@
This module contains the class to persist trades into SQLite This module contains the class to persist trades into SQLite
""" """
import logging 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 import create_engine, inspect
from sqlalchemy.exc import NoSuchModuleError from sqlalchemy.exc import NoSuchModuleError
@ -19,6 +21,22 @@ from freqtrade.persistence.trade_model import Order, Trade
logger = logging.getLogger(__name__) 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' _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 # https://docs.sqlalchemy.org/en/13/orm/contextual.html#thread-local-scope
# Scoped sessions proxy requests to the appropriate thread-local session. # Scoped sessions proxy requests to the appropriate thread-local session.
# We should use the scoped_session object - not a seperately initialized version # 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)) Trade.session = scoped_session(sessionmaker(
bind=engine, autoflush=False), scopefunc=get_request_or_thread_id)
Order.session = Trade.session Order.session = Trade.session
PairLock.session = Trade.session PairLock.session = Trade.session

View File

@ -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 fastapi import Depends
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.persistence.models import _request_id_ctx_var
from freqtrade.rpc.rpc import RPC, RPCException from freqtrade.rpc.rpc import RPC, RPCException
from .webserver import ApiServer from .webserver import ApiServer
@ -15,12 +17,19 @@ def get_rpc_optional() -> Optional[RPC]:
return None return None
def get_rpc() -> Optional[Iterator[RPC]]: async def get_rpc() -> Optional[AsyncIterator[RPC]]:
_rpc = get_rpc_optional() _rpc = get_rpc_optional()
if _rpc: if _rpc:
request_id = str(uuid4())
ctx_token = _request_id_ctx_var.set(request_id)
Trade.rollback() Trade.rollback()
yield _rpc try:
Trade.rollback() yield _rpc
finally:
Trade.session.remove()
_request_id_ctx_var.reset(ctx_token)
else: else:
raise RPCException('Bot is not in the correct state') raise RPCException('Bot is not in the correct state')

View File

@ -83,6 +83,8 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
self._send_msg(str(e)) self._send_msg(str(e))
except BaseException: except BaseException:
logger.exception('Exception occurred within Telegram module') logger.exception('Exception occurred within Telegram module')
finally:
Trade.session.remove()
return wrapper return wrapper

View File

@ -1,3 +1,7 @@
[build-system]
requires = ["setuptools >= 46.4.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.black] [tool.black]
line-length = 100 line-length = 100
exclude = ''' exclude = '''
@ -48,10 +52,6 @@ ignore_errors = true
module = "telegram.*" module = "telegram.*"
implicit_optional = true implicit_optional = true
[build-system]
requires = ["setuptools >= 46.4.0", "wheel"]
build-backend = "setuptools.build_meta"
[tool.pyright] [tool.pyright]
include = ["freqtrade"] include = ["freqtrade"]
exclude = [ exclude = [

View File

@ -7,11 +7,11 @@
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
coveralls==3.3.1 coveralls==3.3.1
ruff==0.0.255 ruff==0.0.257
mypy==1.1.1 mypy==1.1.1
pre-commit==3.1.1 pre-commit==3.2.0
pytest==7.2.2 pytest==7.2.2
pytest-asyncio==0.20.3 pytest-asyncio==0.21.0
pytest-cov==4.0.0 pytest-cov==4.0.0
pytest-mock==3.10.0 pytest-mock==3.10.0
pytest-random-order==1.1.0 pytest-random-order==1.1.0
@ -22,7 +22,7 @@ time-machine==2.9.0
httpx==0.23.3 httpx==0.23.3
# Convert jupyter notebooks to markdown documents # Convert jupyter notebooks to markdown documents
nbconvert==7.2.9 nbconvert==7.2.10
# mypy types # mypy types
types-cachetools==5.3.0.4 types-cachetools==5.3.0.4

View File

@ -5,5 +5,5 @@
scipy==1.10.1 scipy==1.10.1
scikit-learn==1.1.3 scikit-learn==1.1.3
scikit-optimize==0.9.0 scikit-optimize==0.9.0
filelock==3.9.0 filelock==3.10.0
progressbar2==4.2.0 progressbar2==4.2.0

View File

@ -2,10 +2,10 @@ numpy==1.24.2
pandas==1.5.3 pandas==1.5.3
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==2.9.12 ccxt==3.0.23
cryptography==39.0.2 cryptography==39.0.2
aiohttp==3.8.4 aiohttp==3.8.4
SQLAlchemy==2.0.5.post1 SQLAlchemy==2.0.7
python-telegram-bot==13.15 python-telegram-bot==13.15
arrow==1.2.3 arrow==1.2.3
cachetools==4.2.2 cachetools==4.2.2
@ -26,7 +26,7 @@ pyarrow==11.0.0; platform_machine != 'armv7l'
py_find_1st==1.1.5 py_find_1st==1.1.5
# Load ticker files 30% faster # Load ticker files 30% faster
python-rapidjson==1.9 python-rapidjson==1.10
# Properly format api responses # Properly format api responses
orjson==3.8.7 orjson==3.8.7
@ -34,9 +34,9 @@ orjson==3.8.7
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.94.0 fastapi==0.95.0
pydantic==1.10.6 pydantic==1.10.6
uvicorn==0.21.0 uvicorn==0.21.1
pyjwt==2.6.0 pyjwt==2.6.0
aiofiles==23.1.0 aiofiles==23.1.0
psutil==5.9.4 psutil==5.9.4
@ -56,4 +56,4 @@ schedule==1.1.0
websockets==10.4 websockets==10.4
janus==1.0.0 janus==1.0.0
ast-comments==1.0.0 ast-comments==1.0.1

View File

@ -555,7 +555,6 @@ def test__set_leverage_binance(mocker, default_conf):
"set_leverage", "set_leverage",
pair="XRP/USDT", pair="XRP/USDT",
leverage=5.0, leverage=5.0,
trading_mode=TradingMode.FUTURES
) )

View File

@ -1039,9 +1039,9 @@ def test_validate_ordertypes(default_conf, mocker):
('bybit', 'last', True), ('bybit', 'last', True),
('bybit', 'mark', True), ('bybit', 'mark', True),
('bybit', 'index', True), ('bybit', 'index', True),
# ('okx', 'last', True), ('okx', 'last', True),
# ('okx', 'mark', True), ('okx', 'mark', True),
# ('okx', 'index', True), ('okx', 'index', True),
('gate', 'last', True), ('gate', 'last', True),
('gate', 'mark', True), ('gate', 'mark', True),
('gate', 'index', True), ('gate', 'index', True),
@ -3868,29 +3868,6 @@ def test_get_stake_amount_considering_leverage(
stake_amount, leverage) == min_stake_with_lev 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", [ @pytest.mark.parametrize("margin_mode", [
(MarginMode.CROSS), (MarginMode.CROSS),
(MarginMode.ISOLATED) (MarginMode.ISOLATED)

View File

@ -2,11 +2,13 @@ from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, PropertyMock from unittest.mock import MagicMock, PropertyMock
import ccxt
import pytest import pytest
from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import RetryableOrderError
from freqtrade.exchange.exchange import timeframe_to_minutes 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 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() exchange.load_leverage_tiers()
assert log_has(logmsg, caplog) 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)

View File

@ -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</b>:' in msg_mock.call_args_list[0][0][0] assert 'Monthly Profit over the last 6 months</b>:' in msg_mock.call_args_list[0][0][0]
def test_profit_handle(default_conf_usdt, update, ticker_usdt, ticker_sell_up, fee, def test_telegram_profit_handle(
limit_sell_order_usdt, mocker) -> None: 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('freqtrade.rpc.rpc.CryptoToFiatConverter._find_price', return_value=1.1)
mocker.patch.multiple( mocker.patch.multiple(
EXMS, 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 # Update the ticker with a market going up
mocker.patch(f'{EXMS}.fetch_ticker', ticker_sell_up) mocker.patch(f'{EXMS}.fetch_ticker', ticker_sell_up)
# Simulate fulfilled LIMIT_SELL order for trade # Simulate fulfilled LIMIT_SELL order for trade
trade = Trade.session.scalars(select(Trade)).first()
oobj = Order.parse_from_ccxt_object( oobj = Order.parse_from_ccxt_object(
limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell') limit_sell_order_usdt, limit_sell_order_usdt['symbol'], 'sell')
trade.orders.append(oobj) trade.orders.append(oobj)