Merge remote-tracking branch 'origin/develop' into develop
This commit is contained in:
commit
f57787882d
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -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__
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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 = [
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user